Skip to content

Commit fdc55cc

Browse files
419 Group forms in the sidebar (#426)
* group forms in the sidebar * create `SidebarGroup` component * return form description in endpoint * fix ts linter error --------- Co-authored-by: Daniel Townsend <dan@dantownsend.co.uk>
1 parent d8682b6 commit fdc55cc

File tree

10 files changed

+261
-56
lines changed

10 files changed

+261
-56
lines changed
Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,97 @@
11
<template>
2-
<ul>
3-
<li v-bind:key="formConfig.name" v-for="formConfig in formConfigs">
4-
<router-link
5-
:to="{ name: 'addForm', params: { formSlug: formConfig.slug } }"
6-
class="subtle"
7-
>
8-
<font-awesome-icon icon="level-up-alt" class="rotated90" />
9-
<span>{{ readable(formConfig.name) }}</span>
10-
</router-link>
11-
</li>
12-
</ul>
2+
<div>
3+
<ul class="table_list">
4+
<li v-for="form in formGroups.ungrouped" v-bind:key="form">
5+
<router-link
6+
:to="{ name: 'addForm', params: { formSlug: form.slug } }"
7+
:title="form.description"
8+
class="subtle"
9+
><font-awesome-icon icon="level-up-alt" class="rotated90" />
10+
<span>{{ form.name }}</span></router-link
11+
>
12+
</li>
13+
14+
<template v-for="(formNames, groupName) in formGroups.grouped">
15+
<SidebarGroup
16+
:name="String(groupName)"
17+
:collapsed="hiddenGroups.indexOf(String(groupName)) != -1"
18+
@toggled="toggleGroup(String(groupName))"
19+
/>
20+
21+
<template v-if="hiddenGroups.indexOf(String(groupName)) == -1">
22+
<li v-for="form in formNames" v-bind:key="form">
23+
<router-link
24+
:to="{
25+
name: 'addForm',
26+
params: { formSlug: form.slug }
27+
}"
28+
:title="form.description"
29+
class="subtle"
30+
><font-awesome-icon
31+
icon="level-up-alt"
32+
class="rotated90"
33+
/>
34+
<span>{{ form.name }}</span></router-link
35+
>
36+
</li>
37+
</template>
38+
</template>
39+
</ul>
40+
</div>
1341
</template>
1442

1543
<script lang="ts">
1644
import { defineComponent } from "vue"
17-
18-
import { readable } from "@/utils"
45+
import SidebarGroup from "./SidebarGroup.vue"
1946
2047
export default defineComponent({
48+
components: { SidebarGroup },
49+
data() {
50+
return {
51+
hiddenGroups: [] as string[]
52+
}
53+
},
2154
computed: {
22-
formConfigs() {
23-
return this.$store.state.formConfigs
55+
formGroups() {
56+
return this.$store.state.formGroups
2457
}
2558
},
26-
setup() {
27-
return {
28-
readable
59+
methods: {
60+
toggleGroup(groupName: string) {
61+
const hiddenGroups: string[] = this.hiddenGroups
62+
const index = hiddenGroups.indexOf(groupName)
63+
if (index == -1) {
64+
hiddenGroups.push(groupName)
65+
} else {
66+
hiddenGroups.splice(index, 1)
67+
}
68+
this.hiddenGroups = hiddenGroups
2969
}
3070
},
3171
async mounted() {
3272
await this.$store.dispatch("fetchFormConfigs")
73+
await this.$store.dispatch("fetchFormGroups")
3374
}
3475
})
3576
</script>
77+
78+
<style scoped lang="less">
79+
li.group {
80+
font-size: 0.7em;
81+
82+
a {
83+
text-transform: uppercase;
84+
85+
span {
86+
&.name {
87+
padding-left: 0;
88+
}
89+
90+
&.ellipsis {
91+
flex-grow: 1;
92+
text-align: right;
93+
}
94+
}
95+
}
96+
}
97+
</style>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<template>
2+
<li class="group">
3+
<a
4+
href="#"
5+
class="subtle"
6+
@click.prevent="$emit('toggled')"
7+
title="Click to toggle children."
8+
>
9+
<font-awesome-icon icon="layer-group" />
10+
<span class="name">{{ name }}</span>
11+
<span class="ellipsis" v-if="collapsed">...</span>
12+
</a>
13+
</li>
14+
</template>
15+
16+
<script setup lang="ts">
17+
import type { PropType } from "vue"
18+
19+
defineProps({
20+
name: {
21+
type: String as PropType<string>,
22+
required: true
23+
},
24+
collapsed: {
25+
type: Boolean as PropType<boolean>,
26+
required: true
27+
}
28+
})
29+
30+
defineEmits(["toggled"])
31+
</script>
32+
33+
<style scoped lang="less">
34+
li.group {
35+
font-size: 0.7em;
36+
37+
a {
38+
text-transform: uppercase;
39+
40+
span {
41+
&.name {
42+
padding-left: 0;
43+
}
44+
45+
&.ellipsis {
46+
flex-grow: 1;
47+
text-align: right;
48+
}
49+
}
50+
}
51+
}
52+
</style>

admin_ui/src/components/TableNav.vue

Lines changed: 7 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,11 @@
88
/>
99

1010
<template v-for="(tableNames, groupName) in tableGroups.grouped">
11-
<li class="group">
12-
<a
13-
href="#"
14-
class="subtle"
15-
@click.prevent="toggleGroup(String(groupName))"
16-
title="Click to toggle children."
17-
>
18-
<font-awesome-icon icon="layer-group" />
19-
<span class="name">{{ groupName }}</span>
20-
<span
21-
class="ellipsis"
22-
v-if="hiddenGroups.indexOf(String(groupName)) != -1"
23-
>...</span
24-
>
25-
</a>
26-
</li>
11+
<SidebarGroup
12+
:collapsed="hiddenGroups.indexOf(String(groupName)) != -1"
13+
:name="String(groupName)"
14+
@toggled="toggleGroup(String(groupName))"
15+
/>
2716

2817
<template v-if="hiddenGroups.indexOf(String(groupName)) == -1">
2918
<TableNavItem
@@ -39,10 +28,12 @@
3928

4029
<script lang="ts">
4130
import { defineComponent } from "vue"
31+
import SidebarGroup from "./SidebarGroup.vue"
4232
import TableNavItem from "./TableNavItem.vue"
4333
4434
export default defineComponent({
4535
components: {
36+
SidebarGroup,
4637
TableNavItem
4738
},
4839
data() {
@@ -75,24 +66,3 @@ export default defineComponent({
7566
}
7667
})
7768
</script>
78-
79-
<style scoped lang="less">
80-
li.group {
81-
font-size: 0.7em;
82-
83-
a {
84-
text-transform: uppercase;
85-
86-
span {
87-
&.name {
88-
padding-left: 0;
89-
}
90-
91-
&.ellipsis {
92-
flex-grow: 1;
93-
text-align: right;
94-
}
95-
}
96-
}
97-
}
98-
</style>

admin_ui/src/store.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export default createStore({
3333
tableNames: [],
3434
tableGroups: {},
3535
formConfigs: [] as i.FormConfig[],
36+
formGroups: {},
3637
user: undefined,
3738
loadingStatus: false,
3839
customLinks: {}
@@ -47,6 +48,9 @@ export default createStore({
4748
updateFormConfigs(state, value) {
4849
state.formConfigs = value
4950
},
51+
updateFormGroups(state, value) {
52+
state.formGroups = value
53+
},
5054
updateCurrentTablename(state, value) {
5155
state.currentTableName = value
5256
},
@@ -105,6 +109,10 @@ export default createStore({
105109
const response = await axios.get(`${BASE_URL}forms/`)
106110
context.commit("updateFormConfigs", response.data)
107111
},
112+
async fetchFormGroups(context) {
113+
const response = await axios.get(`${BASE_URL}forms/grouped/`)
114+
context.commit("updateFormGroups", response.data)
115+
},
108116
async fetchFormConfig(context, formSlug: string) {
109117
const response = await axios.get(`${BASE_URL}forms/${formSlug}/`)
110118
return response

docs/source/custom_forms/examples/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def calculator(request: Request, data: CalculatorModel):
2525
pydantic_model=CalculatorModel,
2626
endpoint=calculator,
2727
description=("Adds two numbers together."),
28+
form_group="Text forms",
2829
)
2930

3031

piccolo_admin/endpoints.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,11 +94,23 @@ class GenerateFileURLResponseModel(BaseModel):
9494
file_url: str = Field(description="A URL which the file is accessible on.")
9595

9696

97+
class GroupItem(BaseModel):
98+
name: str
99+
slug: str
100+
101+
97102
class GroupedTableNamesResponseModel(BaseModel):
98103
grouped: t.Dict[str, t.List[str]] = Field(default_factory=list)
99104
ungrouped: t.List[str] = Field(default_factory=list)
100105

101106

107+
class GroupedFormsResponseModel(BaseModel):
108+
grouped: t.Dict[str, t.List[FormConfigResponseModel]] = Field(
109+
default_factory=list
110+
)
111+
ungrouped: t.List[FormConfigResponseModel] = Field(default_factory=list)
112+
113+
102114
@dataclass
103115
class TableConfig:
104116
"""
@@ -340,6 +352,10 @@ class FormConfig:
340352
:param description:
341353
An optional description which is shown in the UI to explain to the user
342354
what the form is for.
355+
:param form_group:
356+
If specified, forms can be divided into groups in the form
357+
menu. This is useful when you have many forms that you
358+
can organize into groups for better visibility.
343359
344360
Here's a full example:
345361
@@ -364,7 +380,8 @@ def my_endpoint(request: Request, data: MyModel):
364380
config = FormConfig(
365381
name="My Form",
366382
pydantic_model=MyModel,
367-
endpoint=my_endpoint
383+
endpoint=my_endpoint,
384+
form_group="Text forms",
368385
)
369386
370387
"""
@@ -378,11 +395,13 @@ def __init__(
378395
t.Union[FormResponse, t.Coroutine[None, None, FormResponse]],
379396
],
380397
description: t.Optional[str] = None,
398+
form_group: t.Optional[str] = None,
381399
):
382400
self.name = name
383401
self.pydantic_model = pydantic_model
384402
self.endpoint = endpoint
385403
self.description = description
404+
self.form_group = form_group
386405
self.slug = self.name.replace(" ", "-").lower()
387406

388407

@@ -622,6 +641,14 @@ def __init__(
622641
response_model=t.List[FormConfigResponseModel],
623642
)
624643

644+
private_app.add_api_route(
645+
path="/forms/grouped/",
646+
endpoint=self.get_grouped_forms, # type: ignore
647+
methods=["GET"],
648+
response_model=GroupedFormsResponseModel,
649+
tags=["Forms"],
650+
)
651+
625652
private_app.add_api_route(
626653
path="/forms/{form_slug:str}/",
627654
endpoint=self.get_single_form, # type: ignore
@@ -937,6 +964,34 @@ def get_forms(self) -> t.List[FormConfigResponseModel]:
937964
for form in self.forms
938965
]
939966

967+
def get_grouped_forms(self) -> GroupedFormsResponseModel:
968+
"""
969+
Returns a list of custom forms registered with the admin, grouped using
970+
`form_group`.
971+
"""
972+
response = GroupedFormsResponseModel()
973+
group_names = sorted(
974+
{
975+
v.form_group
976+
for _, v in self.form_config_map.items()
977+
if v.form_group
978+
}
979+
)
980+
response.grouped = {i: [] for i in group_names}
981+
for _, form_config in self.form_config_map.items():
982+
form_group = form_config.form_group
983+
form_config_response = FormConfigResponseModel(
984+
name=form_config.name,
985+
slug=form_config.slug,
986+
description=form_config.description,
987+
)
988+
if form_group is None:
989+
response.ungrouped.append(form_config_response)
990+
else:
991+
response.grouped[form_group].append(form_config_response)
992+
993+
return response
994+
940995
def get_single_form(self, form_slug: str) -> FormConfigResponseModel:
941996
"""
942997
Returns the FormConfig for the given form.

piccolo_admin/example/forms/csv.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ async def download_movies(
4949
pydantic_model=DownloadMoviesModel,
5050
endpoint=download_movies,
5151
description="Download a list of movies for the director as a CSV file.",
52+
form_group="Download forms",
5253
)

piccolo_admin/example/forms/email.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,4 +44,5 @@ def booking_endpoint(request: Request, data: BookingModel) -> str:
4444
pydantic_model=BookingModel,
4545
endpoint=booking_endpoint,
4646
description="Make a booking for a customer.",
47+
form_group="Text forms",
4748
)

piccolo_admin/example/forms/image.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ def download_schedule(
3939
pydantic_model=DownloadScheduleModel,
4040
endpoint=download_schedule,
4141
description=("Download the schedule for the day."),
42+
form_group="Download forms",
4243
)

0 commit comments

Comments
 (0)