@jini-ai/admin 0.4.5 → 0.4.6
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/dist/agent-plugins/package-file-tree.d.ts +3 -1
- package/dist/agent-plugins/package-file-tree.js +7 -7
- package/dist/comments/controllers/comment-queue.controller.d.ts +5 -1
- package/dist/comments/controllers/comment-queue.controller.js +2 -2
- package/dist/comments/react/hooks/use-comment-queue.hooks.js +14 -3
- package/dist/consent/react/ConsentSettingsPanel.d.ts +27 -0
- package/dist/consent/react/ConsentSettingsPanel.js +24 -0
- package/dist/consent/react/index.d.ts +3 -0
- package/dist/consent/react/index.js +2 -0
- package/dist/consent/react/settings-panel.hooks.d.ts +19 -0
- package/dist/consent/react/settings-panel.hooks.js +30 -0
- package/dist/core/ports/forms.d.ts +23 -3
- package/dist/core/ports/forms.js +3 -3
- package/dist/core/ports/seo.d.ts +17 -0
- package/dist/forms/adapters/http.d.ts +5 -2
- package/dist/forms/adapters/http.js +7 -2
- package/dist/forms/controllers/field-attributes-dialog.controller.d.ts +59 -1
- package/dist/forms/controllers/field-attributes-dialog.controller.js +27 -2
- package/dist/forms/field-settings.d.ts +142 -0
- package/dist/forms/field-settings.js +327 -0
- package/dist/forms/form-editor-dirty.d.ts +42 -0
- package/dist/forms/form-editor-dirty.js +25 -0
- package/dist/forms/forms.module.d.ts +72 -0
- package/dist/forms/html-rules.d.ts +41 -3
- package/dist/forms/html-rules.js +118 -17
- package/dist/forms/messages.en.d.ts +72 -0
- package/dist/forms/messages.en.js +73 -0
- package/dist/forms/react/components/FieldAttributesDialog.js +20 -19
- package/dist/forms/react/components/FieldSettingsSection.d.ts +19 -0
- package/dist/forms/react/components/FieldSettingsSection.js +21 -0
- package/dist/forms/react/components/FormFieldsEditor.js +7 -8
- package/dist/forms/react/components/FormSubmissionDetail.js +5 -2
- package/dist/forms/react/hooks/forms-view.hooks.d.ts +75 -5
- package/dist/forms/react/hooks/forms-view.hooks.js +46 -2
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.d.ts +10 -0
- package/dist/forms/react/hooks/use-field-attributes-dialog.hooks.js +12 -2
- package/dist/forms/react/hooks/use-form-editor.hooks.d.ts +11 -0
- package/dist/forms/react/hooks/use-form-editor.hooks.js +25 -2
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.d.ts +2 -0
- package/dist/forms/react/hooks/use-form-fields-editor.hooks.js +5 -1
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.d.ts +4 -0
- package/dist/forms/react/hooks/use-form-submission-detail.hooks.js +1 -0
- package/dist/forms/react/index.d.ts +216 -0
- package/dist/forms/react/pages/FormEditor.js +3 -2
- package/dist/forms/rules.d.ts +10 -1
- package/dist/forms/rules.js +8 -1
- package/dist/media/controllers/library.controller.d.ts +4 -0
- package/dist/media/controllers/library.controller.js +13 -0
- package/dist/media/react/components/MediaPicker.js +2 -2
- package/dist/media/react/hooks/EditMediaPanel.hooks.js +4 -2
- package/dist/media/react/hooks/GallerySelection.hooks.d.ts +31 -0
- package/dist/media/react/hooks/GallerySelection.hooks.js +32 -0
- package/dist/media/react/hooks/LibraryTab.hooks.d.ts +4 -0
- package/dist/media/react/hooks/LibraryTab.hooks.js +9 -3
- package/dist/media/react/hooks/MediaCard.hooks.d.ts +2 -2
- package/dist/media/react/hooks/MediaGrid.hooks.d.ts +2 -2
- package/dist/media/react/hooks/MediaPage.hooks.d.ts +4 -0
- package/dist/media/react/hooks/MediaPage.hooks.js +10 -13
- package/dist/media/react/hooks/MediaPicker.hooks.js +5 -1
- package/dist/media/react/hooks/MediaQuery.hooks.d.ts +27 -0
- package/dist/media/react/hooks/MediaQuery.hooks.js +41 -0
- package/dist/media/react/hooks/ProvidersTab.hooks.js +21 -2
- package/dist/media/react/index.d.ts +1 -0
- package/dist/media/react/index.js +1 -0
- package/dist/react/components/InteractiveHtmlEditor/InteractiveHtmlEditor.js +5 -0
- package/dist/react/components/Sidebar.d.ts +6 -0
- package/dist/react/components/Sidebar.js +1 -0
- package/dist/react/entities/use-entity-edit.d.ts +1 -1
- package/dist/react/hooks/use-sidebar-rail.d.ts +10 -1
- package/dist/react/hooks/use-sidebar-rail.js +16 -3
- package/dist/seo/adapters/http.d.ts +11 -1
- package/dist/seo/adapters/http.js +11 -1
- package/dist/seo/controllers/entry-picker.controller.d.ts +2 -0
- package/dist/seo/controllers/entry-picker.controller.js +2 -0
- package/dist/seo/models.d.ts +8 -0
- package/dist/seo/react/components/MediaRefField.js +2 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.d.ts +4 -2
- package/dist/seo/react/hooks/MediaRefField.hooks.js +8 -3
- package/dist/seo/react/hooks/SeoQuery.hooks.d.ts +42 -0
- package/dist/seo/react/hooks/SeoQuery.hooks.js +65 -0
- package/dist/seo/react/hooks/use-entry-picker.hooks.js +7 -9
- package/dist/seo/react/hooks/use-seo-entry-panel.hooks.js +9 -2
- package/dist/seo/react/hooks/use-seo.hooks.js +22 -4
- package/dist/seo/react/hooks/use-share-image-warning.hooks.d.ts +19 -0
- package/dist/seo/react/hooks/use-share-image-warning.hooks.js +79 -0
- package/dist/seo/react/hooks/use-sitemap-modal.hooks.js +5 -3
- package/dist/seo/react/pages/SeoPage.js +1 -1
- package/dist/seo/rules.d.ts +21 -2
- package/dist/seo/rules.js +44 -4
- package/dist/widgets/react/hooks/WidgetsQuery.hooks.d.ts +37 -0
- package/dist/widgets/react/hooks/WidgetsQuery.hooks.js +70 -0
- package/dist/widgets/react/hooks/use-widget-instance-editor.hooks.js +5 -3
- package/dist/widgets/react/hooks/use-widget-region-editor.hooks.js +3 -1
- package/dist/widgets/react/hooks/use-widget-regions.hooks.js +7 -3
- package/dist/widgets/react/hooks/use-widgets-library.hooks.js +7 -3
- package/package.json +20 -14
|
@@ -7,7 +7,7 @@ import { FormSubmissionDate } from "./FormSubmissionDate.js";
|
|
|
7
7
|
import { formSubmissionRows } from '../../html-rules.js';
|
|
8
8
|
export function FormSubmissionDetail({ formId, submissionId, onBack, onDeleted, useFormSubmissionDetailHook = useWiredFormSubmissionDetail, t, answerColumns = [], }, _optional = {}) {
|
|
9
9
|
const formatDate = useFormSubmissionDate({});
|
|
10
|
-
const { submission, error, confirmOpen, deleting, requestDelete, cancelDelete, confirmDelete } = useFormSubmissionDetailHook({
|
|
10
|
+
const { submission, error, confirmOpen, deleting, requestDelete, cancelDelete, confirmDelete, fileUrl } = useFormSubmissionDetailHook({
|
|
11
11
|
formId,
|
|
12
12
|
submissionId,
|
|
13
13
|
onDeleted,
|
|
@@ -22,7 +22,10 @@ export function FormSubmissionDetail({ formId, submissionId, onBack, onDeleted,
|
|
|
22
22
|
// button sat right on top of the table and Delete sat right underneath it. Same
|
|
23
23
|
// flex-column-plus-gap idiom `.field-group` already uses elsewhere for vertical rhythm,
|
|
24
24
|
// rather than one-off margins on each button.
|
|
25
|
-
_jsxs("div", { className: "form-submission-detail", children: [_jsxs("button", { type: "button", className: "btn-secondary", onClick: onBack, "aria-label": t("Back to submissions"), ...agentHandle({ handle: "form-submission-back" }, { role: "link", label: "Back to this form's list of submissions" }), children: ["\u2190 ", t("Back")] }), error ? _jsx("div", { className: "notice error", children: error }) : null, _jsx("div", { className: "table-scroll", children: _jsx("table", { className: "list-table", children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { children: t("Submitted at") }), _jsx("td", { children: _jsx(FormSubmissionDate, { date: formatDate({ iso: submission.submittedAt }) }) })] }), _jsxs("tr", { children: [_jsx("th", { children: t("Source IP") }), _jsx("td", { children: submission.sourceIp })] }), formSubmissionRows({ data: submission.data, answerColumns }, { unknownFieldLabel: t("Unknown field") }).map(({ key, label, value
|
|
25
|
+
_jsxs("div", { className: "form-submission-detail", children: [_jsxs("button", { type: "button", className: "btn-secondary", onClick: onBack, "aria-label": t("Back to submissions"), ...agentHandle({ handle: "form-submission-back" }, { role: "link", label: "Back to this form's list of submissions" }), children: ["\u2190 ", t("Back")] }), error ? _jsx("div", { className: "notice error", children: error }) : null, _jsx("div", { className: "table-scroll", children: _jsx("table", { className: "list-table", children: _jsxs("tbody", { children: [_jsxs("tr", { children: [_jsx("th", { children: t("Submitted at") }), _jsx("td", { children: _jsx(FormSubmissionDate, { date: formatDate({ iso: submission.submittedAt }) }) })] }), _jsxs("tr", { children: [_jsx("th", { children: t("Source IP") }), _jsx("td", { children: submission.sourceIp })] }), formSubmissionRows({ data: submission.data, answerColumns }, { unknownFieldLabel: t("Unknown field") }).map(({ key, label, value, file }) => {
|
|
26
|
+
const href = file ? fileUrl(file) : null;
|
|
27
|
+
return (_jsxs("tr", { children: [_jsx("th", { className: "jini-submission-field-label", children: label }), _jsx("td", { children: href && file ? _jsx("a", { href: href, download: file.filename, ...agentHandle({ handle: `form-submission-file-${key}` }, { role: "link", label: `Download the uploaded file ${file.filename}` }), children: value }) : value })] }, key));
|
|
28
|
+
})] }) }) }), _jsx("button", { type: "button", className: "btn-danger", disabled: deleting, onClick: requestDelete, ...agentHandle({ handle: "form-submission-delete" }, {
|
|
26
29
|
role: "button",
|
|
27
30
|
label: "Move this submission to the trash. Opens a confirmation dialog first.",
|
|
28
31
|
}), children: t("Delete submission") }), _jsx(ConfirmDialog, { open: confirmOpen, agentHandle: "form-submission-delete-confirm", title: t("Move to trash?"), body: _jsx("p", { children: t("It will disappear from this list. You can restore it from the Trash.") }), confirmLabel: t("Move to trash"), destructive: true, pending: deleting, onConfirm: confirmDelete, onCancel: cancelDelete })] }));
|
|
@@ -2,6 +2,7 @@ import type { ChangeEvent, MutableRefObject } from 'react';
|
|
|
2
2
|
import { type FormDateLine } from '../../rules.js';
|
|
3
3
|
import type { FormFieldsEditorController } from './use-form-fields-editor.hooks.js';
|
|
4
4
|
import type { FieldAttributesDialogController } from './use-field-attributes-dialog.hooks.js';
|
|
5
|
+
import type { FieldSettingsForm } from '../../field-settings.js';
|
|
5
6
|
import type { FormSubmissionsController } from './use-form-submissions.hooks.js';
|
|
6
7
|
import type { FormEditorFieldsBodyProps } from '../components/FormEditorFieldsBody.js';
|
|
7
8
|
import type { AdminFormDefinition, AdminFormField } from '../../models.js';
|
|
@@ -18,14 +19,14 @@ export declare function useFormsListView({ forms, t, formatFormDates, toggleStat
|
|
|
18
19
|
dates: FormDateLine[];
|
|
19
20
|
menuLabel: string;
|
|
20
21
|
menuItems: import("../../../react/index.js").RowMenuItem[];
|
|
21
|
-
id: string;
|
|
22
|
-
name: string;
|
|
23
22
|
html?: string | undefined;
|
|
24
23
|
status: import("../../../core/index.js").FormDefinitionStatus;
|
|
25
|
-
|
|
24
|
+
id: string;
|
|
25
|
+
slug: string;
|
|
26
26
|
createdAt: string;
|
|
27
27
|
updatedAt: string;
|
|
28
|
-
|
|
28
|
+
name: string;
|
|
29
|
+
mode?: "builder" | "html" | undefined;
|
|
29
30
|
workspaceId?: string;
|
|
30
31
|
fields: AdminFormField[];
|
|
31
32
|
notify: import("../../models.js").AdminFormNotify;
|
|
@@ -47,6 +48,14 @@ export declare function useFormFieldsView({ fields, existingFieldIds, controller
|
|
|
47
48
|
base: string;
|
|
48
49
|
isExisting: boolean;
|
|
49
50
|
displayName: string;
|
|
51
|
+
typeGroups: {
|
|
52
|
+
group: import("../../field-settings.js").FieldTypeGroup;
|
|
53
|
+
types: Array<{
|
|
54
|
+
type: string;
|
|
55
|
+
label: string;
|
|
56
|
+
}>;
|
|
57
|
+
}[];
|
|
58
|
+
hasLength: boolean;
|
|
50
59
|
onId: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
51
60
|
onLabel: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
52
61
|
onType: (e: ChangeEvent<HTMLSelectElement>) => void;
|
|
@@ -65,6 +74,7 @@ export declare function useFieldAttributesView({ field, fieldIndex, controller,
|
|
|
65
74
|
}, _optional?: {}): {
|
|
66
75
|
displayName: string;
|
|
67
76
|
onClose: () => void;
|
|
77
|
+
hasStyling: boolean;
|
|
68
78
|
onClassName: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
69
79
|
rows: {
|
|
70
80
|
handle: string;
|
|
@@ -76,6 +86,66 @@ export declare function useFieldAttributesView({ field, fieldIndex, controller,
|
|
|
76
86
|
value: string;
|
|
77
87
|
}[];
|
|
78
88
|
};
|
|
89
|
+
/** Bind the settings section's controls; which ones render comes from the field's registry kind. */
|
|
90
|
+
export declare function useFieldSettingsView({ field, controller, t }: {
|
|
91
|
+
field: AdminFormField;
|
|
92
|
+
controller: FieldAttributesDialogController;
|
|
93
|
+
t: (key: string) => string;
|
|
94
|
+
}, _optional?: {}): {
|
|
95
|
+
settings: import("../../field-settings.js").FieldSettings;
|
|
96
|
+
form: FieldSettingsForm;
|
|
97
|
+
boundsType: import("../../field-settings.js").FieldBounds;
|
|
98
|
+
boundsClass: string;
|
|
99
|
+
/** `<input type>` for a typed default, or `null` when the default is a box or a choice. */
|
|
100
|
+
defaultInputType: string | null;
|
|
101
|
+
defaultClass: string | undefined;
|
|
102
|
+
onPlaceholder: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
103
|
+
onHelpText: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
104
|
+
onDefaultText: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
105
|
+
onMinLength: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
106
|
+
onMaxLength: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
107
|
+
onPattern: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
108
|
+
onMin: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
109
|
+
onMax: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
110
|
+
onStep: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
111
|
+
onMaxFileSize: (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) => void;
|
|
112
|
+
onDefaultChecked: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
113
|
+
onDefaultChoice: (e: ChangeEvent<HTMLSelectElement>) => void;
|
|
114
|
+
addOption: () => void;
|
|
115
|
+
defaultChoices: {
|
|
116
|
+
key: number;
|
|
117
|
+
value: string;
|
|
118
|
+
label: string;
|
|
119
|
+
}[];
|
|
120
|
+
options: {
|
|
121
|
+
handle: string;
|
|
122
|
+
isFirst: boolean;
|
|
123
|
+
isLast: boolean;
|
|
124
|
+
isDefault: boolean;
|
|
125
|
+
labelAria: string;
|
|
126
|
+
valueAria: string;
|
|
127
|
+
defaultAria: string;
|
|
128
|
+
upAria: string;
|
|
129
|
+
downAria: string;
|
|
130
|
+
removeAria: string;
|
|
131
|
+
onLabel: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
132
|
+
onValue: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
133
|
+
onDefault: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
134
|
+
onUp: () => void;
|
|
135
|
+
onDown: () => void;
|
|
136
|
+
onRemove: () => void;
|
|
137
|
+
_rowId: number;
|
|
138
|
+
value: string;
|
|
139
|
+
label: string;
|
|
140
|
+
}[];
|
|
141
|
+
uploads: {
|
|
142
|
+
checked: boolean;
|
|
143
|
+
handle: string;
|
|
144
|
+
onToggle: (e: ChangeEvent<HTMLInputElement>) => void;
|
|
145
|
+
value: string;
|
|
146
|
+
label: string;
|
|
147
|
+
}[];
|
|
148
|
+
};
|
|
79
149
|
export declare function useSubmissionsView({ controller }: {
|
|
80
150
|
controller: FormSubmissionsController;
|
|
81
151
|
}, _optional?: {}): {
|
|
@@ -84,8 +154,8 @@ export declare function useSubmissionsView({ controller }: {
|
|
|
84
154
|
rows: {
|
|
85
155
|
handle: string;
|
|
86
156
|
view: () => void;
|
|
87
|
-
id: string;
|
|
88
157
|
status?: "active" | "trash";
|
|
158
|
+
id: string;
|
|
89
159
|
formDefinitionId: string;
|
|
90
160
|
sourceIp: string;
|
|
91
161
|
submittedAt: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { buildAgentListHandles } from '@jini-ai/agentic';
|
|
2
2
|
import { fieldDisplayName, formRowMenuItems, FORM_TABS } from '../../rules.js';
|
|
3
|
+
import { fieldSettingsFor, fieldTypeGroups, labelChangePatch, retypeField, UPLOAD_CHOICES } from '../../field-settings.js';
|
|
3
4
|
import { useFormsOptions, FormsPortsContext } from './FormsPorts.hooks.js';
|
|
4
5
|
import { useContext } from 'react';
|
|
5
6
|
/** Publication actions keep their original DOM at the host slot. */
|
|
@@ -32,9 +33,11 @@ export function useFormFieldsView({ fields, existingFieldIds, controller }, _opt
|
|
|
32
33
|
controller.closeAttrsDialog();
|
|
33
34
|
},
|
|
34
35
|
rows: fields.map((field, index) => ({ field, index, base: handles[index], isExisting: existingFieldIds.includes(field.id), displayName: fieldDisplayName({ field, index }),
|
|
36
|
+
// Registry-driven: the type picker's groups and whether this type has a text length.
|
|
37
|
+
typeGroups: fieldTypeGroups({ current: field.type }), hasLength: fieldSettingsFor({ type: field.type }).length,
|
|
35
38
|
onId: (e) => controller.updateField(index, { id: e.target.value }),
|
|
36
|
-
onLabel: (e) => controller.updateField(index, { label: e.target.value }),
|
|
37
|
-
onType: (e) => controller.
|
|
39
|
+
onLabel: (e) => controller.updateField(index, labelChangePatch({ field, label: e.target.value, isExisting: existingFieldIds.includes(field.id) })),
|
|
40
|
+
onType: (e) => controller.replaceField(index, retypeField({ field, type: e.target.value })),
|
|
38
41
|
onRequired: (e) => controller.updateField(index, { required: e.target.checked }),
|
|
39
42
|
onMaxLength: (e) => controller.updateField(index, { maxLength: e.target.value ? Number(e.target.value) : null }),
|
|
40
43
|
ref: (el) => { controller.kebabRefs.current[index] = el; },
|
|
@@ -45,6 +48,7 @@ export function useFieldAttributesView({ field, fieldIndex, controller, onCancel
|
|
|
45
48
|
const handles = buildAgentListHandles({ prefix: 'form-field-attrs-row', ids: controller.rows.map(row => String(row._rowId)) });
|
|
46
49
|
return {
|
|
47
50
|
displayName: fieldDisplayName({ field, index: fieldIndex }), onClose: () => onCancel(),
|
|
51
|
+
hasStyling: Boolean(field.className) || Object.keys(field.attributes ?? {}).length > 0,
|
|
48
52
|
onClassName: (e) => controller.setClassName(e.target.value),
|
|
49
53
|
rows: controller.rows.map((row, index) => ({ ...row, handle: handles[index],
|
|
50
54
|
onName: (e) => controller.updateRow(row._rowId, { name: e.target.value }),
|
|
@@ -52,6 +56,46 @@ export function useFieldAttributesView({ field, fieldIndex, controller, onCancel
|
|
|
52
56
|
onRemove: () => controller.removeRow(row._rowId), })),
|
|
53
57
|
};
|
|
54
58
|
}
|
|
59
|
+
/** Bind the settings section's controls; which ones render comes from the field's registry kind. */
|
|
60
|
+
export function useFieldSettingsView({ field, controller, t }, _optional = {}) {
|
|
61
|
+
const settings = fieldSettingsFor({ type: field.type });
|
|
62
|
+
const form = controller.settings;
|
|
63
|
+
const value = (key) => (e) => controller.setSettings({ [key]: e.target.value });
|
|
64
|
+
const optionHandles = buildAgentListHandles({ prefix: 'form-field-option', ids: form.options.map(row => String(row._rowId)) });
|
|
65
|
+
const numbered = (key, n) => t(key).replace('{n}', String(n));
|
|
66
|
+
const kind = settings.defaultKind;
|
|
67
|
+
const boundsType = settings.bounds ?? 'number';
|
|
68
|
+
return {
|
|
69
|
+
settings, form, boundsType,
|
|
70
|
+
boundsClass: settings.bounds === 'number' ? 'field-settings-num' : 'field-settings-compact',
|
|
71
|
+
/** `<input type>` for a typed default, or `null` when the default is a box or a choice. */
|
|
72
|
+
defaultInputType: kind === 'temporal' ? boundsType : kind === 'number' ? 'number' : kind === 'text' || kind === 'textarea' ? 'text' : null,
|
|
73
|
+
defaultClass: kind === 'number' || kind === 'temporal' ? 'field-settings-compact' : undefined,
|
|
74
|
+
onPlaceholder: value('placeholder'), onHelpText: value('helpText'), onDefaultText: value('defaultText'),
|
|
75
|
+
onMinLength: value('minLength'), onMaxLength: value('maxLength'), onPattern: value('pattern'),
|
|
76
|
+
onMin: value('min'), onMax: value('max'), onStep: value('step'), onMaxFileSize: value('maxFileSizeMb'),
|
|
77
|
+
onDefaultChecked: (e) => controller.setSettings({ defaultChecked: e.target.checked }),
|
|
78
|
+
onDefaultChoice: (e) => controller.toggleDefaultChoice(e.target.value, true, true),
|
|
79
|
+
addOption: () => controller.addOption(),
|
|
80
|
+
defaultChoices: form.options.filter(row => row.value).map(row => ({ key: row._rowId, value: row.value, label: row.label || row.value })),
|
|
81
|
+
options: form.options.map((row, index) => ({ ...row, handle: optionHandles[index],
|
|
82
|
+
isFirst: index === 0, isLast: index === form.options.length - 1,
|
|
83
|
+
// Multi-choice defaults are ticked per option; a single default uses one select instead.
|
|
84
|
+
isDefault: form.defaultChoices.includes(row.value),
|
|
85
|
+
labelAria: numbered('Option {n} label', index + 1), valueAria: numbered('Option {n} value', index + 1),
|
|
86
|
+
defaultAria: numbered('Option {n} selected by default', index + 1),
|
|
87
|
+
upAria: numbered('Move option {n} up', index + 1), downAria: numbered('Move option {n} down', index + 1),
|
|
88
|
+
removeAria: numbered('Remove option {n}', index + 1),
|
|
89
|
+
onLabel: (e) => controller.updateOption(row._rowId, { label: e.target.value }),
|
|
90
|
+
onValue: (e) => controller.updateOption(row._rowId, { value: e.target.value }),
|
|
91
|
+
onDefault: (e) => controller.toggleDefaultChoice(row.value, e.target.checked, false),
|
|
92
|
+
onUp: () => controller.moveOption(row._rowId, -1), onDown: () => controller.moveOption(row._rowId, 1),
|
|
93
|
+
onRemove: () => controller.removeOption(row._rowId), })),
|
|
94
|
+
uploads: UPLOAD_CHOICES.map(choice => ({ ...choice, checked: form.accept.includes(choice.value),
|
|
95
|
+
handle: `form-field-settings-accept-${choice.label.toLowerCase()}`,
|
|
96
|
+
onToggle: (e) => controller.toggleAccept(choice.value, e.target.checked) })),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
55
99
|
export function useSubmissionsView({ controller }, _optional = {}) {
|
|
56
100
|
const handles = buildAgentListHandles({ prefix: 'form-submission-view', ids: (controller.submissions ?? []).map(submission => submission.id) });
|
|
57
101
|
return {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AdminFormField } from "../../models.js";
|
|
2
2
|
import { type AttrRow } from "../../rules.js";
|
|
3
|
+
import { type FieldSettingsForm, type OptionRow } from "../../field-settings.js";
|
|
3
4
|
/**
|
|
4
5
|
* @file `FieldAttributesDialog`'s own state and submit action (per-field CSS classes + HTML
|
|
5
6
|
* attributes), so the dialog in `FormEditor.tsx` is only markup.
|
|
@@ -20,6 +21,15 @@ export interface FieldAttributesDialogController {
|
|
|
20
21
|
updateRow: (rowId: number, patch: Partial<AttrRow>) => void;
|
|
21
22
|
removeRow: (rowId: number) => void;
|
|
22
23
|
addRow: () => void;
|
|
24
|
+
/** The field type's builder settings draft (`field-settings.ts`). */
|
|
25
|
+
settings: FieldSettingsForm;
|
|
26
|
+
setSettings: (patch: Partial<FieldSettingsForm>) => void;
|
|
27
|
+
toggleDefaultChoice: (value: string, on: boolean, single: boolean) => void;
|
|
28
|
+
toggleAccept: (value: string, on: boolean) => void;
|
|
29
|
+
updateOption: (rowId: number, patch: Partial<Pick<OptionRow, "value" | "label">>) => void;
|
|
30
|
+
addOption: () => void;
|
|
31
|
+
removeOption: (rowId: number) => void;
|
|
32
|
+
moveOption: (rowId: number, offset: -1 | 1) => void;
|
|
23
33
|
error: string | null;
|
|
24
34
|
submit: (e: React.FormEvent) => void;
|
|
25
35
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useController } from "../../../core/react/use-controller.js";
|
|
2
2
|
import { createFieldAttributesDialogController } from "../../controllers/field-attributes-dialog.controller.js";
|
|
3
3
|
import {} from "../../rules.js";
|
|
4
|
+
import { settingsFormFromField } from "../../field-settings.js";
|
|
4
5
|
export function useFieldAttributesDialog(props, _optional = {}) {
|
|
5
6
|
const { controller: stateController, snapshot: stateSnapshot } = useController({ create: () => createFieldAttributesDialogController({ field: props.field }), dependencies: [] });
|
|
6
|
-
const state = stateSnapshot ?? { className: props.field.className ?? "", rows: [], error: null };
|
|
7
|
+
const state = stateSnapshot ?? { className: props.field.className ?? "", rows: [], settings: settingsFormFromField({ field: props.field }), error: null };
|
|
7
8
|
const className = state.className;
|
|
8
9
|
const setClassName = (value) => stateController?.update({ key: "className", value });
|
|
9
10
|
const rows = state.rows;
|
|
@@ -11,12 +12,21 @@ export function useFieldAttributesDialog(props, _optional = {}) {
|
|
|
11
12
|
function updateRow(rowId, patch) { stateController?.updateRow({ rowId, patch }); }
|
|
12
13
|
function removeRow(rowId) { stateController?.removeRow({ rowId }); }
|
|
13
14
|
function addRow() { stateController?.addRow({}); }
|
|
15
|
+
const settingsActions = {
|
|
16
|
+
setSettings: (patch) => stateController?.setSettings({ patch }),
|
|
17
|
+
toggleDefaultChoice: (value, on, single) => stateController?.toggleDefaultChoice({ value, on, single }),
|
|
18
|
+
toggleAccept: (value, on) => stateController?.toggleAccept({ value, on }),
|
|
19
|
+
updateOption: (rowId, patch) => stateController?.updateOption({ rowId, patch }),
|
|
20
|
+
addOption: () => stateController?.addOption({}),
|
|
21
|
+
removeOption: (rowId) => stateController?.removeOption({ rowId }),
|
|
22
|
+
moveOption: (rowId, offset) => stateController?.moveOption({ rowId, offset }),
|
|
23
|
+
};
|
|
14
24
|
function submit(e) {
|
|
15
25
|
e.preventDefault();
|
|
16
26
|
const result = stateController?.submit({});
|
|
17
27
|
if (result?.ok)
|
|
18
28
|
props.onSave(result.patch);
|
|
19
29
|
}
|
|
20
|
-
return { className, setClassName, rows, updateRow, removeRow, addRow, error, submit };
|
|
30
|
+
return { className, setClassName, rows, updateRow, removeRow, addRow, settings: state.settings, ...settingsActions, error, submit };
|
|
21
31
|
}
|
|
22
32
|
//# sourceMappingURL=use-field-attributes-dialog.hooks.js.map
|
|
@@ -96,6 +96,17 @@ export interface FormEditorController {
|
|
|
96
96
|
onTabsKeyDown: (e: React.KeyboardEvent) => void;
|
|
97
97
|
handleSave: () => void;
|
|
98
98
|
handleStatusToggle: () => void;
|
|
99
|
+
/** Back with unsaved edits opens an app confirm modal instead of discarding them silently
|
|
100
|
+
* (Chrome check, 2026-10-11). `onBack` routes through the injected navigation port;
|
|
101
|
+
* only a dirty draft requires confirmation. */
|
|
102
|
+
leave: {
|
|
103
|
+
confirmOpen: boolean;
|
|
104
|
+
onBack: (event: {
|
|
105
|
+
preventDefault: () => void;
|
|
106
|
+
}) => void;
|
|
107
|
+
confirm: () => void;
|
|
108
|
+
cancel: () => void;
|
|
109
|
+
};
|
|
99
110
|
/** Bound translator — see this file's own header for why it arrives via the hook rather than
|
|
100
111
|
* `FormEditor.tsx` calling `useAdminLocale()`/`FORMS_DICT` directly. */
|
|
101
112
|
t: (key: string) => string;
|
|
@@ -3,7 +3,9 @@ import { useFormsPorts, useFormsOptions } from "./FormsPorts.hooks.js";
|
|
|
3
3
|
import { useController } from "../../../core/react/use-controller.js";
|
|
4
4
|
import { createFormEditorController, initialFormEditorState } from "../../controllers/form-editor.controller.js";
|
|
5
5
|
import { formHtmlEmbed, formAnswerColumns } from "../../html-rules.js";
|
|
6
|
-
import { useEffect, useRef } from "react";
|
|
6
|
+
import { useEffect, useRef, useState } from "react";
|
|
7
|
+
import { useDirtyGuard } from "@jini-ai/ui/panel-kit";
|
|
8
|
+
import { formEditorBaseline, formEditorDraft } from "../../form-editor-dirty.js";
|
|
7
9
|
import { useFetchMutation, useFetchQuery } from "@jini-ai/ui/fetch-query";
|
|
8
10
|
import { FORM_TABS, KEYS, existingFieldIdsOf, nextTabIndex, visibleFormEditorError } from "../../rules.js";
|
|
9
11
|
export function useFormEditor(props, { clipboard } = {}) {
|
|
@@ -145,6 +147,26 @@ export function useFormEditor(props, { clipboard } = {}) {
|
|
|
145
147
|
// already surfaced through statusMutation.error -> error below
|
|
146
148
|
}
|
|
147
149
|
}
|
|
150
|
+
// Same guard as the post and widget editors (reload/close prompt included); the in-app Back
|
|
151
|
+
// gets the admin's confirm modal rather than `confirmLeave`'s `window.confirm`.
|
|
152
|
+
const { isDirty } = useDirtyGuard({ current: formEditorDraft({ name, slug, fields, mode, html }), original: formEditorBaseline({ form, isNew }) });
|
|
153
|
+
const [leaveConfirmOpen, setLeaveConfirmOpen] = useState(false);
|
|
154
|
+
const leave = {
|
|
155
|
+
confirmOpen: leaveConfirmOpen,
|
|
156
|
+
onBack(event) {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
if (!isDirty) {
|
|
159
|
+
navigate("/forms");
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
setLeaveConfirmOpen(true);
|
|
163
|
+
},
|
|
164
|
+
confirm() {
|
|
165
|
+
setLeaveConfirmOpen(false);
|
|
166
|
+
navigate("/forms");
|
|
167
|
+
},
|
|
168
|
+
cancel: () => setLeaveConfirmOpen(false),
|
|
169
|
+
};
|
|
148
170
|
const saving = updateMutation.status === "pending" || createMutation.status === "pending" || statusMutation.status === "pending";
|
|
149
171
|
const error = visibleFormEditorError({
|
|
150
172
|
updateError: updateMutation.error,
|
|
@@ -175,7 +197,7 @@ export function useFormEditor(props, { clipboard } = {}) {
|
|
|
175
197
|
isNew,
|
|
176
198
|
form,
|
|
177
199
|
mode, onModeChange, html, setHtml, copyEmbed, copyFeedback,
|
|
178
|
-
answerColumns: formAnswerColumns({ fields: form?.fields ?? [] }),
|
|
200
|
+
answerColumns: formAnswerColumns({ fields: form?.fields ?? [] }, { yes: t("Yes"), no: t("No") }),
|
|
179
201
|
name,
|
|
180
202
|
setName,
|
|
181
203
|
slug,
|
|
@@ -192,6 +214,7 @@ export function useFormEditor(props, { clipboard } = {}) {
|
|
|
192
214
|
onTabsKeyDown,
|
|
193
215
|
handleSave,
|
|
194
216
|
handleStatusToggle,
|
|
217
|
+
leave,
|
|
195
218
|
t,
|
|
196
219
|
};
|
|
197
220
|
}
|
|
@@ -24,6 +24,8 @@ export interface FormFieldsEditorController {
|
|
|
24
24
|
* callback in the view. */
|
|
25
25
|
kebabRefs: React.MutableRefObject<Array<HTMLButtonElement | null>>;
|
|
26
26
|
updateField: (index: number, patch: Partial<AdminFormField>) => void;
|
|
27
|
+
/** Replaces the whole field, so keys absent from `field` are dropped (a type change). */
|
|
28
|
+
replaceField: (index: number, field: AdminFormField) => void;
|
|
27
29
|
addField: () => void;
|
|
28
30
|
removeField: (index: number) => void;
|
|
29
31
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useController } from "../../../core/react/use-controller.js";
|
|
2
2
|
import { createFormFieldsEditorController, initialFormFieldsEditorState } from "../../controllers/form-fields-editor.controller.js";
|
|
3
3
|
import { useRef } from "react";
|
|
4
|
-
import { addFormField, removeFormField, updateFormField } from "../../rules.js";
|
|
4
|
+
import { addFormField, removeFormField, replaceFormField, updateFormField } from "../../rules.js";
|
|
5
5
|
export function useFormFieldsEditor(props, _optional = {}) {
|
|
6
6
|
const { controller: stateController, snapshot: stateSnapshot } = useController({ create: () => createFormFieldsEditorController({}), dependencies: [] });
|
|
7
7
|
const state = stateSnapshot ?? initialFormFieldsEditorState({});
|
|
@@ -17,6 +17,9 @@ export function useFormFieldsEditor(props, _optional = {}) {
|
|
|
17
17
|
function updateField(index, patch) {
|
|
18
18
|
props.onChange(updateFormField({ fields: props.fields, index, patch }));
|
|
19
19
|
}
|
|
20
|
+
function replaceField(index, field) {
|
|
21
|
+
props.onChange(replaceFormField({ fields: props.fields, index, field }));
|
|
22
|
+
}
|
|
20
23
|
function addField() {
|
|
21
24
|
props.onChange(addFormField({ fields: props.fields }));
|
|
22
25
|
}
|
|
@@ -29,6 +32,7 @@ export function useFormFieldsEditor(props, _optional = {}) {
|
|
|
29
32
|
closeAttrsDialog,
|
|
30
33
|
kebabRefs,
|
|
31
34
|
updateField,
|
|
35
|
+
replaceField,
|
|
32
36
|
addField,
|
|
33
37
|
removeField,
|
|
34
38
|
};
|
|
@@ -48,6 +48,10 @@ export interface FormSubmissionDetailController {
|
|
|
48
48
|
* either way, so a failed delete doesn't leave the operator stuck behind it — the failure is
|
|
49
49
|
* still visible via `error` below. */
|
|
50
50
|
confirmDelete: () => Promise<void>;
|
|
51
|
+
/** Download URL for a file answer, or `null` when the host wired no media path. */
|
|
52
|
+
fileUrl: (file: {
|
|
53
|
+
mediaId: string;
|
|
54
|
+
}) => string | null;
|
|
51
55
|
}
|
|
52
56
|
export declare function useFormSubmissionDetail(props: {
|
|
53
57
|
port: AdminFormsPort;
|
|
@@ -71,6 +71,7 @@ export function useFormSubmissionDetail(props, _optional = {}) {
|
|
|
71
71
|
requestDelete,
|
|
72
72
|
cancelDelete,
|
|
73
73
|
confirmDelete,
|
|
74
|
+
fileUrl: ({ mediaId }) => port.submissionFileUrl?.({ mediaId }) ?? null,
|
|
74
75
|
};
|
|
75
76
|
}
|
|
76
77
|
export function useWiredFormSubmissionDetail(props, _optional = {}) {
|