Skip to content

Commit b133894

Browse files
authored
Merge pull request #220 from openscript-ch/22-design-data-export-interface-format-selection-filtering
22 design data export interface format selection filtering
2 parents 771672f + 4281239 commit b133894

33 files changed

+491
-400
lines changed

.changeset/fifty-seas-thank.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@quassel/frontend": patch
3+
"@quassel/ui": patch
4+
---
5+
6+
Add export ui

.changeset/many-snails-try.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@quassel/frontend": patch
3+
"@quassel/ui": patch
4+
---
5+
6+
Add content shell
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { isMatch, Link, useLocation, useMatches } from "@tanstack/react-router";
2+
import { Anchor, Breadcrumbs } from "@quassel/ui";
3+
4+
// Inspired by https://github.com/TanStack/router/blob/main/examples/react/kitchen-sink-file-based/src/components/Breadcrumbs.tsx
5+
export function BreadcrumbsNavigation() {
6+
const matches = useMatches();
7+
const location = useLocation();
8+
9+
if (matches.some((match) => match.status === "pending")) return null;
10+
11+
const matchesWithTitle = matches.filter((match) => isMatch(match, "context.title"));
12+
const entries = matchesWithTitle.map((match) => ({ label: match.context.title, to: match.fullPath }));
13+
const uniqueEntries = entries.filter((entry, index) => entries.findIndex((e) => e.label === entry.label) === index);
14+
15+
// Remove the last entry if it's the current page
16+
if (uniqueEntries.length > 0 && uniqueEntries[uniqueEntries.length - 1].to.startsWith(location.pathname)) {
17+
uniqueEntries.pop();
18+
}
19+
20+
return (
21+
<Breadcrumbs>
22+
{uniqueEntries.map((e) => (
23+
<Anchor key={e.label} renderRoot={(props) => <Link to={e.to} {...props} />}>
24+
{e.label}
25+
</Anchor>
26+
))}
27+
</Breadcrumbs>
28+
);
29+
}

apps/frontend/src/routes/__root.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,18 @@ import {
1717
Divider,
1818
FooterLogos,
1919
} from "@quassel/ui";
20-
import { createRootRouteWithContext, Link, Outlet, useNavigate } from "@tanstack/react-router";
20+
import { createRootRouteWithContext, Link, Outlet, RouteContext, useNavigate } from "@tanstack/react-router";
2121
import { version } from "../../package.json";
2222
import { $session } from "../stores/session";
2323
import { useStore } from "@nanostores/react";
2424
import { $layout } from "../stores/layout";
2525
import { $api } from "../stores/api";
26-
import { DefaultError, QueryClient, useQueryClient } from "@tanstack/react-query";
26+
import { DefaultError, useQueryClient } from "@tanstack/react-query";
27+
import { i18n } from "../stores/i18n";
28+
29+
const messages = i18n("RootRoute", {
30+
title: "Home",
31+
});
2732

2833
function Root() {
2934
const n = useNavigate();
@@ -106,6 +111,7 @@ function Root() {
106111
);
107112
}
108113

109-
export const Route = createRootRouteWithContext<{ queryClient: QueryClient }>()({
114+
export const Route = createRootRouteWithContext<RouteContext>()({
115+
beforeLoad: () => ({ title: messages.get().title }),
110116
component: Root,
111117
});
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,32 @@
1-
import { createFileRoute, Outlet } from "@tanstack/react-router";
1+
import { createFileRoute, Outlet, useMatches } from "@tanstack/react-router";
22
import { useEffect } from "react";
33
import { $layout } from "../../stores/layout";
4+
import { i18n } from "../../stores/i18n";
5+
import { ContentShell } from "@quassel/ui";
6+
import { BreadcrumbsNavigation } from "../../components/Breadcrumbs";
7+
8+
const messages = i18n("AdministrationRoute", {
9+
title: "Administration",
10+
});
411

512
function AdministrationLayout() {
613
useEffect(() => {
714
$layout.set({ admin: true });
815
return () => $layout.set({ admin: false });
916
}, []);
1017

18+
const matches = useMatches();
19+
const title = matches[matches.length - 2]?.context.title;
20+
const actions = matches[matches.length - 1]?.context.actions;
21+
1122
return (
12-
<>
23+
<ContentShell title={title || messages.get().title} breadcrumbs={<BreadcrumbsNavigation />} actions={actions}>
1324
<Outlet />
14-
</>
25+
</ContentShell>
1526
);
1627
}
1728

1829
export const Route = createFileRoute("/_auth/administration")({
30+
beforeLoad: () => ({ title: messages.get().title }),
1931
component: AdministrationLayout,
2032
});
Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import { Paper, Title } from "@quassel/ui";
21
import { createFileRoute, Outlet } from "@tanstack/react-router";
2+
import { i18n } from "../../../stores/i18n";
33

4-
function AdministrationCarers() {
5-
return (
6-
<>
7-
<Title>Carers</Title>
8-
<Paper my="lg">
9-
<Outlet />
10-
</Paper>
11-
</>
12-
);
13-
}
4+
const messages = i18n("AdministrationCarersRoute", {
5+
title: "Carers",
6+
});
147

158
export const Route = createFileRoute("/_auth/administration/carers")({
16-
component: AdministrationCarers,
9+
beforeLoad: () => ({ title: messages.get().title }),
10+
component: Outlet,
1711
});

apps/frontend/src/routes/_auth/administration/carers/index.tsx

Lines changed: 41 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -12,50 +12,52 @@ function AdministrationCarersIndex() {
1212
});
1313

1414
return (
15-
<>
16-
<Button variant="default" renderRoot={(props) => <Link to="/administration/carers/new" {...props} />}>
17-
New carer
18-
</Button>
19-
<Table>
20-
<Table.Thead>
21-
<Table.Tr>
22-
<Table.Th>Id</Table.Th>
23-
<Table.Th>Name</Table.Th>
24-
<Table.Th>Color</Table.Th>
25-
</Table.Tr>
26-
</Table.Thead>
27-
<Table.Tbody>
28-
{carers.data?.map((c) => (
29-
<Table.Tr key={c.id}>
30-
<Table.Td>{c.id}</Table.Td>
31-
<Table.Td>{c.name}</Table.Td>
32-
<Table.Td>{c.color && <ColorSwatch color={c.color} />}</Table.Td>
33-
<Table.Td>
34-
<Button variant="default" renderRoot={(props) => <Link to={`/administration/carers/edit/${c.id}`} {...props} />}>
35-
Edit
15+
<Table>
16+
<Table.Thead>
17+
<Table.Tr>
18+
<Table.Th>Id</Table.Th>
19+
<Table.Th>Name</Table.Th>
20+
<Table.Th>Color</Table.Th>
21+
</Table.Tr>
22+
</Table.Thead>
23+
<Table.Tbody>
24+
{carers.data?.map((c) => (
25+
<Table.Tr key={c.id}>
26+
<Table.Td>{c.id}</Table.Td>
27+
<Table.Td>{c.name}</Table.Td>
28+
<Table.Td>{c.color && <ColorSwatch color={c.color} />}</Table.Td>
29+
<Table.Td>
30+
<Button variant="default" renderRoot={(props) => <Link to={`/administration/carers/edit/${c.id}`} {...props} />}>
31+
Edit
32+
</Button>
33+
{sessionStore.role === "ADMIN" && (
34+
<Button
35+
variant="default"
36+
onClick={() =>
37+
deleteCarerMutation.mutate({
38+
params: { path: { id: c.id.toString() } },
39+
})
40+
}
41+
>
42+
Delete
3643
</Button>
37-
{sessionStore.role === "ADMIN" && (
38-
<Button
39-
variant="default"
40-
onClick={() =>
41-
deleteCarerMutation.mutate({
42-
params: { path: { id: c.id.toString() } },
43-
})
44-
}
45-
>
46-
Delete
47-
</Button>
48-
)}
49-
</Table.Td>
50-
</Table.Tr>
51-
))}
52-
</Table.Tbody>
53-
</Table>
54-
</>
44+
)}
45+
</Table.Td>
46+
</Table.Tr>
47+
))}
48+
</Table.Tbody>
49+
</Table>
5550
);
5651
}
5752

5853
export const Route = createFileRoute("/_auth/administration/carers/")({
54+
beforeLoad: () => ({
55+
actions: [
56+
<Button key="new-carer" variant="default" renderRoot={(props) => <Link to="/administration/carers/new" {...props} />}>
57+
New carer
58+
</Button>,
59+
],
60+
}),
5961
loader: ({ context: { queryClient } }) => queryClient.ensureQueryData($api.queryOptions("get", "/carers")),
6062
component: () => <AdministrationCarersIndex />,
6163
});
Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
1-
import { Paper, Title } from "@quassel/ui";
21
import { createFileRoute, Outlet } from "@tanstack/react-router";
2+
import { i18n } from "../../../stores/i18n";
33

4-
function AdministrationExport() {
5-
return (
6-
<>
7-
<Title>Export</Title>
8-
<Paper my="lg">
9-
<Outlet />
10-
</Paper>
11-
</>
12-
);
13-
}
4+
const messages = i18n("AdministrationExportRoute", {
5+
title: "Export",
6+
});
147

158
export const Route = createFileRoute("/_auth/administration/export")({
16-
component: AdministrationExport,
9+
beforeLoad: () => ({ title: messages.get().title }),
10+
component: Outlet,
1711
});
Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,61 @@
1-
import { Button } from "@quassel/ui";
1+
import { Button, Group, Radio, Select, Stack, useForm } from "@quassel/ui";
22
import { createFileRoute } from "@tanstack/react-router";
33
import { $api } from "../../../../stores/api";
4+
import { i18n } from "../../../../stores/i18n";
5+
import { useStore } from "@nanostores/react";
6+
import { useSuspenseQuery } from "@tanstack/react-query";
7+
8+
const messages = i18n("AdministrationExportIndexRoute", {
9+
title: "Carers",
10+
studyLabel: "Study",
11+
studyPlaceholder: "Select a study",
12+
formatLabel: "File format",
13+
csvLabel: "Comma-separated values (CSV)",
14+
sqlLabel: "Database export (SQL)",
15+
formAction: "Download",
16+
});
17+
18+
type FormValues = {
19+
fileType: "csv" | "sql";
20+
studyId?: string;
21+
};
422

523
function AdministrationExportIndex() {
24+
const t = useStore(messages);
25+
const f = useForm<FormValues>({
26+
mode: "uncontrolled",
27+
initialValues: {
28+
fileType: "csv",
29+
},
30+
});
31+
32+
const studies = useSuspenseQuery($api.queryOptions("get", "/studies"));
633
const { isDownloading, downloadFile } = $api.useDownload("/export", "dump.sql");
734
return (
8-
<div>
9-
<Button loading={isDownloading} onClick={() => downloadFile()}>
10-
Download
11-
</Button>
12-
</div>
35+
<form onSubmit={f.onSubmit(() => downloadFile())}>
36+
<Stack>
37+
<Select
38+
label={t.studyLabel}
39+
placeholder={t.studyPlaceholder}
40+
data={studies.data.map((s) => ({ label: s.title, value: s.id.toString() }))}
41+
/>
42+
<Radio.Group label={t.formatLabel} withAsterisk {...f.getInputProps("fileType")}>
43+
<Group>
44+
<Radio checked value="csv" label={t.csvLabel} />
45+
<Radio value="sql" label={t.sqlLabel} />
46+
</Group>
47+
</Radio.Group>
48+
<Button type="submit" loading={isDownloading}>
49+
{t.formAction}
50+
</Button>
51+
</Stack>
52+
</form>
1353
);
1454
}
1555

1656
export const Route = createFileRoute("/_auth/administration/export/")({
57+
loader: ({ context: { queryClient } }) => {
58+
queryClient.ensureQueryData($api.queryOptions("get", "/studies"));
59+
},
1760
component: AdministrationExportIndex,
1861
});
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { Title } from "@quassel/ui";
21
import { createFileRoute } from "@tanstack/react-router";
2+
import { i18n } from "../../../stores/i18n";
33

4-
export const Route = createFileRoute("/_auth/administration/")({
5-
component: Index,
4+
const messages = i18n("AdministrationDashboardRoute", {
5+
title: "Dashboard",
66
});
77

88
function Index() {
9-
return <Title>Welcome to the administration interface!</Title>;
9+
return null;
1010
}
11+
12+
export const Route = createFileRoute("/_auth/administration/")({
13+
beforeLoad: () => ({ title: messages.get().title }),
14+
component: Index,
15+
});

0 commit comments

Comments
 (0)