@bygghammar/cms-platform 0.43.1 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.44.0
|
|
4
|
+
|
|
5
|
+
- New `backoffice/routes/media-forms`: package-owned `MediaRoute`,
|
|
6
|
+
`FormsIndexRoute`, `FormEditorRoute`, `FormSubmissionsRoute` and
|
|
7
|
+
`FormDeliveriesRoute`, plus `validateSubmissionSearch`.
|
|
8
|
+
- Host contract gains optional `media` and `forms` adapter groups
|
|
9
|
+
(`requireHostGroup`) and `backofficePaths()` now also derives `media`,
|
|
10
|
+
`formSubmissions` and `formDeliveries`.
|
|
11
|
+
|
|
3
12
|
## 0.42.0
|
|
4
13
|
|
|
5
14
|
- Backoffice host registry gains email-settings, locale-domain, shared-content,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bygghammar/cms-platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"description": "Shared CMS platform: blocks, block types, i18n core, UI primitives and design-token contract used by every Bygghammar/Voyara site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -158,6 +158,7 @@
|
|
|
158
158
|
"./backoffice/shell": "./src/backoffice/shell/index.ts",
|
|
159
159
|
"./backoffice/routes": "./src/backoffice/routes/index.tsx",
|
|
160
160
|
"./backoffice/routes/host": "./src/backoffice/routes/host.ts",
|
|
161
|
+
"./backoffice/routes/media-forms": "./src/backoffice/routes/media-forms.tsx",
|
|
161
162
|
"./backoffice/inspectors": "./src/backoffice/inspectors/index.tsx"
|
|
162
163
|
},
|
|
163
164
|
"dependencies": {
|
|
@@ -122,8 +122,67 @@ export type BackofficeHost = {
|
|
|
122
122
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
|
123
123
|
buildBrowserMediaArchive: (args: any) => Promise<any>;
|
|
124
124
|
};
|
|
125
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
126
|
+
/** Media library adapters: query options, server calls and site components. */
|
|
127
|
+
media?: {
|
|
128
|
+
/** TanStack Query options for the media library listing. */
|
|
129
|
+
libraryQueryOptions: any;
|
|
130
|
+
fns: {
|
|
131
|
+
deleteMedia: HostServerFn;
|
|
132
|
+
listTrash: HostServerFn;
|
|
133
|
+
restoreMedia: HostServerFn;
|
|
134
|
+
purgeMedia: HostServerFn;
|
|
135
|
+
emptyTrash: HostServerFn;
|
|
136
|
+
uploadMediaBundle: HostServerFn;
|
|
137
|
+
listMediaUsage: HostServerFn;
|
|
138
|
+
importUrlToMedia: HostServerFn;
|
|
139
|
+
};
|
|
140
|
+
components: {
|
|
141
|
+
FocalImage: any;
|
|
142
|
+
FocusPicker: any;
|
|
143
|
+
MediaMetaDialog: any;
|
|
144
|
+
MediaReplaceDialog: any;
|
|
145
|
+
};
|
|
146
|
+
/** Hook returning the site's media metadata map (alt/caption/credits). */
|
|
147
|
+
useMetaMap: () => any;
|
|
148
|
+
/** Superadmin maintenance tools rendered next to Upload. */
|
|
149
|
+
maintenanceTools?: (args: { onDone: () => Promise<void> }) => ReactNode;
|
|
150
|
+
};
|
|
151
|
+
/** Forms module adapters: server calls plus the editor panels. */
|
|
152
|
+
forms?: {
|
|
153
|
+
fns: {
|
|
154
|
+
createFormFromType: HostServerFn;
|
|
155
|
+
deleteForm: HostServerFn;
|
|
156
|
+
duplicateForm: HostServerFn;
|
|
157
|
+
getFormUsage: HostServerFn;
|
|
158
|
+
listDeletedForms: HostServerFn;
|
|
159
|
+
purgeForm: HostServerFn;
|
|
160
|
+
restoreForm: HostServerFn;
|
|
161
|
+
saveForm: HostServerFn;
|
|
162
|
+
syncSubmissionsToSheet: HostServerFn;
|
|
163
|
+
testWebhook: HostServerFn;
|
|
164
|
+
listSubmissions: HostServerFn;
|
|
165
|
+
setSubmissionStatus: HostServerFn;
|
|
166
|
+
pushSubmissionsToSheet: HostServerFn;
|
|
167
|
+
resendDelivery: HostServerFn;
|
|
168
|
+
listPartials: HostServerFn;
|
|
169
|
+
deletePartials: HostServerFn;
|
|
170
|
+
listDeliveryAttempts: HostServerFn;
|
|
171
|
+
retryDeliveryAttempts: HostServerFn;
|
|
172
|
+
resolveDeliveryAttempts: HostServerFn;
|
|
173
|
+
};
|
|
174
|
+
components: {
|
|
175
|
+
FieldEditor: any;
|
|
176
|
+
FormPreview: any;
|
|
177
|
+
TestDeliveryPanel: any;
|
|
178
|
+
VersionHistory: any;
|
|
179
|
+
ActivityLog: any;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
125
183
|
};
|
|
126
184
|
|
|
185
|
+
|
|
127
186
|
let current: BackofficeHost | null = null;
|
|
128
187
|
|
|
129
188
|
/** Registers the host adapters. Call once, from a module the routes import. */
|
|
@@ -164,5 +223,21 @@ export function backofficePaths(host: BackofficeHost = backofficeHost()) {
|
|
|
164
223
|
backup: `${base}/admin/backup`,
|
|
165
224
|
sections: `${base}/sections`,
|
|
166
225
|
globalSections: `${base}/global-sections`,
|
|
226
|
+
media: `${base}/media`,
|
|
227
|
+
formSubmissions: `${base}/forms/submissions`,
|
|
228
|
+
formDeliveries: `${base}/forms/deliveries`,
|
|
167
229
|
};
|
|
168
230
|
}
|
|
231
|
+
|
|
232
|
+
/** Reads a required optional host group, failing loudly when it is missing. */
|
|
233
|
+
export function requireHostGroup<K extends "media" | "forms">(
|
|
234
|
+
key: K,
|
|
235
|
+
): NonNullable<BackofficeHost[K]> {
|
|
236
|
+
const group = backofficeHost()[key];
|
|
237
|
+
if (!group) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
`Backoffice host is missing the "${key}" adapters — add them to registerBackofficeHost().`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return group as NonNullable<BackofficeHost[K]>;
|
|
243
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Package-owned media library and forms routes.
|
|
3
|
+
*
|
|
4
|
+
* Each component reads the media/forms adapters from the backoffice host
|
|
5
|
+
* registry, wires the queries and renders the shared view, so a site route file
|
|
6
|
+
* stays a two-line `createFileRoute(...)({ component: XRoute })`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
10
|
+
|
|
11
|
+
import { Link, useNavigate, useParams, useSearch } from "@tanstack/react-router";
|
|
12
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
13
|
+
import { useServerFn } from "@tanstack/react-start";
|
|
14
|
+
import { useMemo, useState } from "react";
|
|
15
|
+
|
|
16
|
+
import { MediaLibraryView, type MediaLibraryDeps } from "../MediaLibraryView";
|
|
17
|
+
import { FormsIndexView } from "../FormsIndexView";
|
|
18
|
+
import { FormEditorView } from "../FormEditorView";
|
|
19
|
+
import { FormSubmissionsView, SUBMISSIONS_PAGE_SIZE } from "../FormSubmissionsView";
|
|
20
|
+
import { FormDeliveriesView, type DeliveryChannel } from "../FormDeliveriesView";
|
|
21
|
+
import { backofficeHost, backofficePaths, requireHostGroup } from "./host";
|
|
22
|
+
|
|
23
|
+
/** Router-agnostic link that forwards loose `to`/`params`/`search` values. */
|
|
24
|
+
function HostLink({
|
|
25
|
+
to,
|
|
26
|
+
params,
|
|
27
|
+
search,
|
|
28
|
+
className,
|
|
29
|
+
onClick,
|
|
30
|
+
children,
|
|
31
|
+
}: {
|
|
32
|
+
to: string;
|
|
33
|
+
params?: unknown;
|
|
34
|
+
search?: unknown;
|
|
35
|
+
className?: string;
|
|
36
|
+
onClick?: () => void;
|
|
37
|
+
children?: React.ReactNode;
|
|
38
|
+
}) {
|
|
39
|
+
return (
|
|
40
|
+
<Link
|
|
41
|
+
to={to as any}
|
|
42
|
+
params={params as any}
|
|
43
|
+
search={search as any}
|
|
44
|
+
className={className}
|
|
45
|
+
onClick={onClick}
|
|
46
|
+
>
|
|
47
|
+
{children}
|
|
48
|
+
</Link>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Media library screen (library, trash, usage and maintenance tools). */
|
|
53
|
+
export function MediaRoute() {
|
|
54
|
+
const media = requireHostGroup("media");
|
|
55
|
+
const { fns: adminFns } = backofficeHost();
|
|
56
|
+
const queryClient = useQueryClient();
|
|
57
|
+
|
|
58
|
+
const { data: items = [], isLoading } = useQuery(media.libraryQueryOptions);
|
|
59
|
+
|
|
60
|
+
const del = useServerFn(media.fns.deleteMedia);
|
|
61
|
+
const fetchUsage = useServerFn(media.fns.listMediaUsage);
|
|
62
|
+
const runImport = useServerFn(media.fns.importUrlToMedia);
|
|
63
|
+
const uploadBundle = useServerFn(media.fns.uploadMediaBundle);
|
|
64
|
+
const fetchTrash = useServerFn(media.fns.listTrash);
|
|
65
|
+
const fetchRoles = useServerFn(adminFns.getMyRoles);
|
|
66
|
+
const restore = useServerFn(media.fns.restoreMedia);
|
|
67
|
+
const purge = useServerFn(media.fns.purgeMedia);
|
|
68
|
+
const empty = useServerFn(media.fns.emptyTrash);
|
|
69
|
+
|
|
70
|
+
const usageQ = useQuery({ queryKey: ["media-usage"], queryFn: () => fetchUsage() });
|
|
71
|
+
const trashQ = useQuery({ queryKey: ["media-trash"], queryFn: () => fetchTrash() });
|
|
72
|
+
const rolesQ = useQuery({ queryKey: ["admin", "my-roles"], queryFn: () => fetchRoles() });
|
|
73
|
+
|
|
74
|
+
const metaMap = media.useMetaMap();
|
|
75
|
+
|
|
76
|
+
const refreshAll = async () => {
|
|
77
|
+
await Promise.all([
|
|
78
|
+
queryClient.invalidateQueries({ queryKey: ["media-library"] }),
|
|
79
|
+
queryClient.invalidateQueries({ queryKey: ["media-trash"] }),
|
|
80
|
+
queryClient.invalidateQueries({ queryKey: ["media-usage"] }),
|
|
81
|
+
]);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const deps = useMemo<MediaLibraryDeps>(
|
|
85
|
+
() => ({
|
|
86
|
+
items: (items ?? []) as MediaLibraryDeps["items"],
|
|
87
|
+
isLoading,
|
|
88
|
+
trash: (trashQ.data ?? []) as MediaLibraryDeps["trash"],
|
|
89
|
+
trashLoading: trashQ.isLoading,
|
|
90
|
+
usage: usageQ.data as MediaLibraryDeps["usage"],
|
|
91
|
+
isSuperadmin: ((rolesQ.data ?? []) as string[]).includes("superadmin"),
|
|
92
|
+
metaMap,
|
|
93
|
+
deleteMedia: (input) => del(input as never),
|
|
94
|
+
restoreMedia: (input) => restore(input as never),
|
|
95
|
+
purgeMedia: (input) => purge(input as never),
|
|
96
|
+
emptyTrash: () => empty() as Promise<{ purged: number }>,
|
|
97
|
+
uploadMediaBundle: (input) => uploadBundle(input as never),
|
|
98
|
+
importUrlToMedia: (input) => runImport(input as never),
|
|
99
|
+
refreshAll,
|
|
100
|
+
invalidateLibrary: () => queryClient.invalidateQueries({ queryKey: ["media-library"] }),
|
|
101
|
+
FocalImageComponent: media.components.FocalImage,
|
|
102
|
+
MediaMetaDialogComponent: media.components.MediaMetaDialog,
|
|
103
|
+
FocusPickerComponent: media.components.FocusPicker,
|
|
104
|
+
MediaReplaceDialogComponent: media.components.MediaReplaceDialog,
|
|
105
|
+
maintenanceTools: media.maintenanceTools,
|
|
106
|
+
LinkComponent: HostLink as MediaLibraryDeps["LinkComponent"],
|
|
107
|
+
}),
|
|
108
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
109
|
+
[items, isLoading, trashQ.data, trashQ.isLoading, usageQ.data, rolesQ.data, metaMap],
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return <MediaLibraryView deps={deps} />;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Forms index: list, create, duplicate, delete and the trash bin. */
|
|
116
|
+
export function FormsIndexRoute() {
|
|
117
|
+
const forms = requireHostGroup("forms");
|
|
118
|
+
const { fns } = backofficeHost();
|
|
119
|
+
const qc = useQueryClient();
|
|
120
|
+
|
|
121
|
+
const list = useServerFn(fns.listForms);
|
|
122
|
+
const create = useServerFn(forms.fns.createFormFromType);
|
|
123
|
+
const remove = useServerFn(forms.fns.deleteForm);
|
|
124
|
+
const copy = useServerFn(forms.fns.duplicateForm);
|
|
125
|
+
const usage = useServerFn(forms.fns.getFormUsage);
|
|
126
|
+
const listBin = useServerFn(forms.fns.listDeletedForms);
|
|
127
|
+
const restore = useServerFn(forms.fns.restoreForm);
|
|
128
|
+
const purge = useServerFn(forms.fns.purgeForm);
|
|
129
|
+
|
|
130
|
+
const { data: rows, isLoading } = useQuery({
|
|
131
|
+
queryKey: ["backoffice-forms"],
|
|
132
|
+
queryFn: () => list(),
|
|
133
|
+
});
|
|
134
|
+
const { data: bin } = useQuery({ queryKey: ["backoffice-forms-bin"], queryFn: () => listBin() });
|
|
135
|
+
|
|
136
|
+
const refresh = async () => {
|
|
137
|
+
await qc.invalidateQueries({ queryKey: ["backoffice-forms"] });
|
|
138
|
+
await qc.invalidateQueries({ queryKey: ["backoffice-forms-bin"] });
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<FormsIndexView
|
|
143
|
+
deps={{
|
|
144
|
+
forms: (rows ?? []) as never,
|
|
145
|
+
isLoading,
|
|
146
|
+
bin: bin as never,
|
|
147
|
+
createForm: (input) => create(input as never),
|
|
148
|
+
duplicateForm: (input) => copy(input as never),
|
|
149
|
+
deleteForm: (input) => remove(input as never),
|
|
150
|
+
restoreForm: (input) => restore(input as never),
|
|
151
|
+
purgeForm: (input) => purge(input as never),
|
|
152
|
+
getFormUsage: (input) => usage(input as never),
|
|
153
|
+
refresh,
|
|
154
|
+
LinkComponent: HostLink as never,
|
|
155
|
+
}}
|
|
156
|
+
/>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Form editor for a single form key. */
|
|
161
|
+
export function FormEditorRoute() {
|
|
162
|
+
const forms = requireHostGroup("forms");
|
|
163
|
+
const { fns } = backofficeHost();
|
|
164
|
+
const params = useParams({ strict: false }) as { key?: string };
|
|
165
|
+
const key = params.key ?? "";
|
|
166
|
+
|
|
167
|
+
const list = useServerFn(fns.listForms);
|
|
168
|
+
const save = useServerFn(forms.fns.saveForm);
|
|
169
|
+
const sync = useServerFn(forms.fns.syncSubmissionsToSheet);
|
|
170
|
+
const dup = useServerFn(forms.fns.duplicateForm);
|
|
171
|
+
const pingWebhook = useServerFn(forms.fns.testWebhook);
|
|
172
|
+
const loadDefaults = useServerFn(fns.getFormDefaults);
|
|
173
|
+
|
|
174
|
+
const { data: rows, isLoading } = useQuery({
|
|
175
|
+
queryKey: ["backoffice-forms"],
|
|
176
|
+
queryFn: () => list(),
|
|
177
|
+
});
|
|
178
|
+
const { data: defaults } = useQuery({
|
|
179
|
+
queryKey: ["form-defaults"],
|
|
180
|
+
queryFn: () => loadDefaults(),
|
|
181
|
+
});
|
|
182
|
+
const form = useMemo(
|
|
183
|
+
() => ((rows ?? []) as { key: string }[]).find((f) => f.key === key),
|
|
184
|
+
[rows, key],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const C = forms.components;
|
|
188
|
+
return (
|
|
189
|
+
<FormEditorView
|
|
190
|
+
deps={{
|
|
191
|
+
form: form as never,
|
|
192
|
+
defaults: defaults as never,
|
|
193
|
+
isLoading,
|
|
194
|
+
saveForm: (input) => save(input as never),
|
|
195
|
+
duplicateForm: (input) => dup(input as never),
|
|
196
|
+
syncSubmissions: (input) => sync(input as never),
|
|
197
|
+
testWebhook: (input) => pingWebhook(input as never),
|
|
198
|
+
LinkComponent: HostLink as never,
|
|
199
|
+
FieldEditorComponent: C.FieldEditor,
|
|
200
|
+
FormPreviewComponent: C.FormPreview,
|
|
201
|
+
TestDeliveryPanelComponent: C.TestDeliveryPanel,
|
|
202
|
+
VersionHistoryComponent: C.VersionHistory,
|
|
203
|
+
ActivityLogComponent: C.ActivityLog,
|
|
204
|
+
}}
|
|
205
|
+
/>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
type SubmissionSearch = {
|
|
210
|
+
form?: string;
|
|
211
|
+
status?: string;
|
|
212
|
+
locale?: string;
|
|
213
|
+
q?: string;
|
|
214
|
+
from?: string;
|
|
215
|
+
to?: string;
|
|
216
|
+
page?: number;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/** Validator a site route reuses for the submissions screen search params. */
|
|
220
|
+
export function validateSubmissionSearch(search: Record<string, unknown>): SubmissionSearch {
|
|
221
|
+
const str = (v: unknown) => (typeof v === "string" && v ? v : undefined);
|
|
222
|
+
return {
|
|
223
|
+
form: str(search.form),
|
|
224
|
+
status: str(search.status),
|
|
225
|
+
locale: str(search.locale),
|
|
226
|
+
q: str(search.q),
|
|
227
|
+
from: str(search.from),
|
|
228
|
+
to: str(search.to),
|
|
229
|
+
page: typeof search.page === "number" ? search.page : undefined,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Submission inbox with filters, paging, exports and delivery retries. */
|
|
234
|
+
export function FormSubmissionsRoute() {
|
|
235
|
+
const forms = requireHostGroup("forms");
|
|
236
|
+
const host = backofficeHost();
|
|
237
|
+
const paths = backofficePaths(host);
|
|
238
|
+
const search = useSearch({ strict: false }) as SubmissionSearch;
|
|
239
|
+
const navigate = useNavigate();
|
|
240
|
+
const qc = useQueryClient();
|
|
241
|
+
|
|
242
|
+
const listF = useServerFn(host.fns.listForms);
|
|
243
|
+
const listS = useServerFn(forms.fns.listSubmissions);
|
|
244
|
+
const setStatus = useServerFn(forms.fns.setSubmissionStatus);
|
|
245
|
+
const push = useServerFn(forms.fns.pushSubmissionsToSheet);
|
|
246
|
+
const resend = useServerFn(forms.fns.resendDelivery);
|
|
247
|
+
const partials = useServerFn(forms.fns.listPartials);
|
|
248
|
+
const removePartials = useServerFn(forms.fns.deletePartials);
|
|
249
|
+
|
|
250
|
+
const page = search.page ?? 0;
|
|
251
|
+
const go = (next: SubmissionSearch) => navigate({ search: next as never });
|
|
252
|
+
|
|
253
|
+
const { data: rows } = useQuery({ queryKey: ["backoffice-forms"], queryFn: () => listF() });
|
|
254
|
+
const { data, isLoading, isFetching } = useQuery({
|
|
255
|
+
queryKey: ["form-submissions", search],
|
|
256
|
+
queryFn: () =>
|
|
257
|
+
listS({
|
|
258
|
+
data: {
|
|
259
|
+
formKey: search.form,
|
|
260
|
+
status: search.status,
|
|
261
|
+
locale: search.locale,
|
|
262
|
+
search: search.q,
|
|
263
|
+
from: search.from,
|
|
264
|
+
to: search.to,
|
|
265
|
+
page,
|
|
266
|
+
pageSize: SUBMISSIONS_PAGE_SIZE,
|
|
267
|
+
sort: "newest",
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
return (
|
|
273
|
+
<FormSubmissionsView
|
|
274
|
+
deps={{
|
|
275
|
+
filters: search as never,
|
|
276
|
+
setFilters: (patch) => go({ ...search, ...(patch as SubmissionSearch), page: 0 }),
|
|
277
|
+
clearFilters: () => go({}),
|
|
278
|
+
goToPage: (next) => go({ ...search, page: next }),
|
|
279
|
+
data: data as never,
|
|
280
|
+
isLoading,
|
|
281
|
+
isFetching,
|
|
282
|
+
forms: ((rows ?? []) as { key: string; name: unknown; fields: unknown }[]).map((f) => ({
|
|
283
|
+
key: f.key,
|
|
284
|
+
name: f.name,
|
|
285
|
+
fields: f.fields,
|
|
286
|
+
})) as never,
|
|
287
|
+
locales: host.locales as never,
|
|
288
|
+
refresh: () => qc.invalidateQueries({ queryKey: ["form-submissions"] }),
|
|
289
|
+
setStatus: (input) => setStatus(input as never),
|
|
290
|
+
pushToSheet: (input) => push(input as never),
|
|
291
|
+
resendDelivery: (input) => resend(input as never),
|
|
292
|
+
listPartials: (input) => partials(input as never),
|
|
293
|
+
deletePartials: (input) => removePartials(input as never),
|
|
294
|
+
BackLink: ({ className, children }) => (
|
|
295
|
+
<HostLink to={paths.forms} className={className}>
|
|
296
|
+
{children}
|
|
297
|
+
</HostLink>
|
|
298
|
+
),
|
|
299
|
+
}}
|
|
300
|
+
/>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Delivery attempt dashboard (retry / resolve failed notifications). */
|
|
305
|
+
export function FormDeliveriesRoute() {
|
|
306
|
+
const forms = requireHostGroup("forms");
|
|
307
|
+
const host = backofficeHost();
|
|
308
|
+
const paths = backofficePaths(host);
|
|
309
|
+
|
|
310
|
+
const list = useServerFn(forms.fns.listDeliveryAttempts);
|
|
311
|
+
const formsFn = useServerFn(host.fns.listForms);
|
|
312
|
+
const retry = useServerFn(forms.fns.retryDeliveryAttempts);
|
|
313
|
+
const resolve = useServerFn(forms.fns.resolveDeliveryAttempts);
|
|
314
|
+
|
|
315
|
+
const [filters, setFiltersState] = useState<{
|
|
316
|
+
formKey: string;
|
|
317
|
+
channel: "" | DeliveryChannel;
|
|
318
|
+
state: "open" | "resolved" | "all";
|
|
319
|
+
}>({ formKey: "", channel: "", state: "open" });
|
|
320
|
+
|
|
321
|
+
const { data: formList } = useQuery({
|
|
322
|
+
queryKey: ["backoffice-forms"],
|
|
323
|
+
queryFn: () => formsFn(),
|
|
324
|
+
});
|
|
325
|
+
const { data, isLoading, refetch } = useQuery({
|
|
326
|
+
queryKey: ["form-deliveries", filters],
|
|
327
|
+
queryFn: () =>
|
|
328
|
+
list({
|
|
329
|
+
data: {
|
|
330
|
+
formKey: filters.formKey || undefined,
|
|
331
|
+
channel: filters.channel || undefined,
|
|
332
|
+
state: filters.state,
|
|
333
|
+
limit: 100,
|
|
334
|
+
},
|
|
335
|
+
}),
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
return (
|
|
339
|
+
<FormDeliveriesView
|
|
340
|
+
deps={{
|
|
341
|
+
data: data as never,
|
|
342
|
+
isLoading,
|
|
343
|
+
forms: ((formList ?? []) as { key: string; name: unknown }[]).map((f) => ({
|
|
344
|
+
key: f.key,
|
|
345
|
+
name: f.name,
|
|
346
|
+
})) as never,
|
|
347
|
+
filters,
|
|
348
|
+
setFilters: (patch) => setFiltersState((prev) => ({ ...prev, ...patch })),
|
|
349
|
+
refetch: () => refetch(),
|
|
350
|
+
retryAttempts: (input) => retry(input as never),
|
|
351
|
+
resolveAttempts: (input) => resolve(input as never),
|
|
352
|
+
BackLink: ({ className, children }) => (
|
|
353
|
+
<HostLink to={paths.forms} className={className}>
|
|
354
|
+
{children}
|
|
355
|
+
</HostLink>
|
|
356
|
+
),
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
);
|
|
360
|
+
}
|