@actuate-media/cms-admin 0.68.0 → 0.69.1
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 +18 -0
- package/dist/AdminRoot.d.ts.map +1 -1
- package/dist/AdminRoot.js +31 -11
- package/dist/AdminRoot.js.map +1 -1
- package/dist/__tests__/hooks/useIdleLogout.test.js +57 -2
- package/dist/__tests__/hooks/useIdleLogout.test.js.map +1 -1
- package/dist/__tests__/router/query-string.test.d.ts +2 -0
- package/dist/__tests__/router/query-string.test.d.ts.map +1 -0
- package/dist/__tests__/router/query-string.test.js +44 -0
- package/dist/__tests__/router/query-string.test.js.map +1 -0
- package/dist/__tests__/views/form-entries.render.test.js +14 -4
- package/dist/__tests__/views/form-entries.render.test.js.map +1 -1
- package/dist/__tests__/views/form-submissions.render.test.js +14 -4
- package/dist/__tests__/views/form-submissions.render.test.js.map +1 -1
- package/dist/__tests__/views/media-seo-audit.render.test.js +26 -0
- package/dist/__tests__/views/media-seo-audit.render.test.js.map +1 -1
- package/dist/__tests__/views/redirects-pane.render.test.d.ts +2 -0
- package/dist/__tests__/views/redirects-pane.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/redirects-pane.render.test.js +121 -0
- package/dist/__tests__/views/redirects-pane.render.test.js.map +1 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts +2 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.d.ts.map +1 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.js +91 -0
- package/dist/__tests__/views/users-selected-deeplink.render.test.js.map +1 -0
- package/dist/actuate-admin.css +1 -1
- package/dist/components/SEOPanel.js +3 -3
- package/dist/components/SEOPanel.js.map +1 -1
- package/dist/components/collections/EntryDetailPane.js +1 -1
- package/dist/components/collections/EntryDetailPane.js.map +1 -1
- package/dist/components/forms/EntriesTable.d.ts +3 -1
- package/dist/components/forms/EntriesTable.d.ts.map +1 -1
- package/dist/components/forms/EntriesTable.js +5 -2
- package/dist/components/forms/EntriesTable.js.map +1 -1
- package/dist/components/forms/FormEntryDetailPane.d.ts +10 -0
- package/dist/components/forms/FormEntryDetailPane.d.ts.map +1 -0
- package/dist/components/forms/FormEntryDetailPane.js +137 -0
- package/dist/components/forms/FormEntryDetailPane.js.map +1 -0
- package/dist/components/forms/FormSchemaPane.js +1 -1
- package/dist/components/forms/FormSchemaPane.js.map +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js +1 -1
- package/dist/components/script-tags/ScriptTagEditorPane.js.map +1 -1
- package/dist/components/seo/RedirectEditorPane.d.ts +12 -0
- package/dist/components/seo/RedirectEditorPane.d.ts.map +1 -0
- package/dist/components/seo/RedirectEditorPane.js +89 -0
- package/dist/components/seo/RedirectEditorPane.js.map +1 -0
- package/dist/components/seo/SeoEditorPane.js +1 -1
- package/dist/components/seo/SeoEditorPane.js.map +1 -1
- package/dist/components/ui/DataTable.js +2 -2
- package/dist/components/ui/DataTable.js.map +1 -1
- package/dist/components/ui/Skeleton.js +3 -3
- package/dist/components/ui/Skeleton.js.map +1 -1
- package/dist/components/ui/Toggle.js +1 -1
- package/dist/components/ui/Toggle.js.map +1 -1
- package/dist/fields/BlockBuilderField.js +1 -1
- package/dist/fields/BlockBuilderField.js.map +1 -1
- package/dist/fields/MediaField.js +1 -1
- package/dist/fields/MediaField.js.map +1 -1
- package/dist/fields/PropInput.js +1 -1
- package/dist/fields/PropInput.js.map +1 -1
- package/dist/hooks/useIdleLogout.d.ts +21 -6
- package/dist/hooks/useIdleLogout.d.ts.map +1 -1
- package/dist/hooks/useIdleLogout.js +41 -8
- package/dist/hooks/useIdleLogout.js.map +1 -1
- package/dist/layout/Header.js +1 -1
- package/dist/layout/Header.js.map +1 -1
- package/dist/router/index.d.ts +7 -0
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +23 -4
- package/dist/router/index.js.map +1 -1
- package/dist/views/DocumentEdit.js +1 -1
- package/dist/views/DocumentEdit.js.map +1 -1
- package/dist/views/FormEntries.d.ts.map +1 -1
- package/dist/views/FormEntries.js +18 -14
- package/dist/views/FormEntries.js.map +1 -1
- package/dist/views/FormSubmissions.d.ts.map +1 -1
- package/dist/views/FormSubmissions.js +18 -15
- package/dist/views/FormSubmissions.js.map +1 -1
- package/dist/views/Forms.js +5 -5
- package/dist/views/Forms.js.map +1 -1
- package/dist/views/MediaBrowser.d.ts +4 -1
- package/dist/views/MediaBrowser.d.ts.map +1 -1
- package/dist/views/MediaBrowser.js +28 -6
- package/dist/views/MediaBrowser.js.map +1 -1
- package/dist/views/MediaSeoAudit.d.ts +4 -1
- package/dist/views/MediaSeoAudit.d.ts.map +1 -1
- package/dist/views/MediaSeoAudit.js +12 -2
- package/dist/views/MediaSeoAudit.js.map +1 -1
- package/dist/views/Pages/PagesListView.js +14 -14
- package/dist/views/Pages/PagesListView.js.map +1 -1
- package/dist/views/Pages/PagesTreeTable.d.ts.map +1 -1
- package/dist/views/Pages/PagesTreeTable.js +3 -3
- package/dist/views/Pages/PagesTreeTable.js.map +1 -1
- package/dist/views/SEO.d.ts.map +1 -1
- package/dist/views/SEO.js +2 -1
- package/dist/views/SEO.js.map +1 -1
- package/dist/views/ScriptTags.d.ts.map +1 -1
- package/dist/views/ScriptTags.js +3 -3
- package/dist/views/ScriptTags.js.map +1 -1
- package/dist/views/Settings.d.ts +4 -1
- package/dist/views/Settings.d.ts.map +1 -1
- package/dist/views/Settings.js +4 -3
- package/dist/views/Settings.js.map +1 -1
- package/dist/views/Users.d.ts +4 -1
- package/dist/views/Users.d.ts.map +1 -1
- package/dist/views/Users.js +22 -2
- package/dist/views/Users.js.map +1 -1
- package/dist/views/seo/ContentTab.js +6 -6
- package/dist/views/seo/ContentTab.js.map +1 -1
- package/dist/views/seo/RedirectsTab.d.ts.map +1 -1
- package/dist/views/seo/RedirectsTab.js +23 -8
- package/dist/views/seo/RedirectsTab.js.map +1 -1
- package/dist/views/settings/SecurityTab.d.ts.map +1 -1
- package/dist/views/settings/SecurityTab.js +2 -1
- package/dist/views/settings/SecurityTab.js.map +1 -1
- package/package.json +2 -2
- package/src/AdminRoot.tsx +53 -12
- package/src/__tests__/hooks/useIdleLogout.test.tsx +103 -1
- package/src/__tests__/router/query-string.test.ts +54 -0
- package/src/__tests__/views/form-entries.render.test.tsx +16 -4
- package/src/__tests__/views/form-submissions.render.test.tsx +16 -4
- package/src/__tests__/views/media-seo-audit.render.test.tsx +34 -0
- package/src/__tests__/views/redirects-pane.render.test.tsx +156 -0
- package/src/__tests__/views/users-selected-deeplink.render.test.tsx +102 -0
- package/src/components/SEOPanel.tsx +9 -9
- package/src/components/collections/EntryDetailPane.tsx +1 -1
- package/src/components/forms/EntriesTable.tsx +12 -3
- package/src/components/forms/{EntryDetailDrawer.tsx → FormEntryDetailPane.tsx} +112 -102
- package/src/components/forms/FormSchemaPane.tsx +1 -1
- package/src/components/script-tags/ScriptTagEditorPane.tsx +1 -1
- package/src/components/seo/RedirectEditorPane.tsx +271 -0
- package/src/components/seo/SeoEditorPane.tsx +1 -1
- package/src/components/ui/DataTable.tsx +3 -3
- package/src/components/ui/Skeleton.tsx +8 -8
- package/src/components/ui/Toggle.tsx +1 -1
- package/src/fields/BlockBuilderField.tsx +1 -1
- package/src/fields/MediaField.tsx +1 -1
- package/src/fields/PropInput.tsx +1 -1
- package/src/hooks/useIdleLogout.ts +65 -13
- package/src/layout/Header.tsx +2 -2
- package/src/router/index.ts +25 -4
- package/src/styles/actuate-tokens.css +4 -0
- package/src/styles/theme.css +9 -3
- package/src/views/DocumentEdit.tsx +4 -4
- package/src/views/FormEntries.tsx +81 -66
- package/src/views/FormSubmissions.tsx +78 -64
- package/src/views/Forms.tsx +16 -16
- package/src/views/MediaBrowser.tsx +34 -6
- package/src/views/MediaSeoAudit.tsx +39 -2
- package/src/views/Pages/PagesListView.tsx +46 -46
- package/src/views/Pages/PagesTreeTable.tsx +11 -8
- package/src/views/SEO.tsx +2 -1
- package/src/views/ScriptTags.tsx +26 -36
- package/src/views/Settings.tsx +7 -2
- package/src/views/Users.tsx +23 -2
- package/src/views/seo/ContentTab.tsx +10 -10
- package/src/views/seo/RedirectsTab.tsx +145 -109
- package/src/views/settings/SecurityTab.tsx +2 -1
- package/dist/components/forms/EntryDetailDrawer.d.ts +0 -11
- package/dist/components/forms/EntryDetailDrawer.d.ts.map +0 -1
- package/dist/components/forms/EntryDetailDrawer.js +0 -145
- package/dist/components/forms/EntryDetailDrawer.js.map +0 -1
- package/dist/components/seo/RedirectEditorDrawer.d.ts +0 -9
- package/dist/components/seo/RedirectEditorDrawer.d.ts.map +0 -1
- package/dist/components/seo/RedirectEditorDrawer.js +0 -95
- package/dist/components/seo/RedirectEditorDrawer.js.map +0 -1
- package/src/components/seo/RedirectEditorDrawer.tsx +0 -231
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useState } from 'react';
|
|
4
|
-
import { toast } from 'sonner';
|
|
5
|
-
import { createRedirectRule, updateRedirectRule, } from '../../lib/seo-service.js';
|
|
6
|
-
import { Drawer } from './Drawer.js';
|
|
7
|
-
import { btnPrimary, btnSecondary } from './primitives.js';
|
|
8
|
-
import { Toggle } from '../ui/Toggle.js';
|
|
9
|
-
const STATUS_CODES = [
|
|
10
|
-
{ value: 301, label: '301 — Permanent' },
|
|
11
|
-
{ value: 302, label: '302 — Temporary' },
|
|
12
|
-
{ value: 307, label: '307 — Temporary (preserve method)' },
|
|
13
|
-
{ value: 308, label: '308 — Permanent (preserve method)' },
|
|
14
|
-
{ value: 410, label: '410 — Gone (no destination)' },
|
|
15
|
-
];
|
|
16
|
-
const inputCls = 'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring';
|
|
17
|
-
function normalizePath(p) {
|
|
18
|
-
const t = p.trim();
|
|
19
|
-
if (!t)
|
|
20
|
-
return '';
|
|
21
|
-
if (/^https?:\/\//i.test(t))
|
|
22
|
-
return t;
|
|
23
|
-
return t.startsWith('/') ? t : `/${t}`;
|
|
24
|
-
}
|
|
25
|
-
export function RedirectEditorDrawer({ open, onOpenChange, rule, onSaved, }) {
|
|
26
|
-
const [form, setForm] = useState({
|
|
27
|
-
type: 301,
|
|
28
|
-
fromPath: '',
|
|
29
|
-
toPath: '',
|
|
30
|
-
enabled: true,
|
|
31
|
-
preserveQuery: false,
|
|
32
|
-
notes: '',
|
|
33
|
-
});
|
|
34
|
-
const [saving, setSaving] = useState(false);
|
|
35
|
-
const [clientError, setClientError] = useState(null);
|
|
36
|
-
useEffect(() => {
|
|
37
|
-
if (!open)
|
|
38
|
-
return;
|
|
39
|
-
if (rule) {
|
|
40
|
-
setForm({
|
|
41
|
-
type: rule.type,
|
|
42
|
-
fromPath: rule.fromPath,
|
|
43
|
-
toPath: rule.toPath,
|
|
44
|
-
enabled: rule.enabled,
|
|
45
|
-
preserveQuery: rule.preserveQuery,
|
|
46
|
-
notes: rule.notes ?? '',
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
setForm({
|
|
51
|
-
type: 301,
|
|
52
|
-
fromPath: '',
|
|
53
|
-
toPath: '',
|
|
54
|
-
enabled: true,
|
|
55
|
-
preserveQuery: false,
|
|
56
|
-
notes: '',
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
setClientError(null);
|
|
60
|
-
}, [open, rule]);
|
|
61
|
-
const isGone = form.type === 410;
|
|
62
|
-
const update = (patch) => setForm((f) => ({ ...f, ...patch }));
|
|
63
|
-
async function handleSave() {
|
|
64
|
-
const fromPath = normalizePath(form.fromPath);
|
|
65
|
-
const toPath = isGone ? '' : normalizePath(form.toPath);
|
|
66
|
-
if (!fromPath) {
|
|
67
|
-
setClientError('Source path is required.');
|
|
68
|
-
return;
|
|
69
|
-
}
|
|
70
|
-
if (!isGone && !toPath) {
|
|
71
|
-
setClientError('Destination is required for redirect status codes.');
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
if (!isGone && fromPath === toPath) {
|
|
75
|
-
setClientError('Source and destination cannot be identical (would create a loop).');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
setClientError(null);
|
|
79
|
-
setSaving(true);
|
|
80
|
-
const payload = { ...form, fromPath, toPath };
|
|
81
|
-
const res = rule
|
|
82
|
-
? await updateRedirectRule(rule.id, payload)
|
|
83
|
-
: await createRedirectRule(payload);
|
|
84
|
-
setSaving(false);
|
|
85
|
-
if (res.error) {
|
|
86
|
-
toast.error(res.error);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
toast.success(rule ? 'Redirect updated.' : 'Redirect created.');
|
|
90
|
-
onOpenChange(false);
|
|
91
|
-
onSaved?.();
|
|
92
|
-
}
|
|
93
|
-
return (_jsx(Drawer, { open: open, onOpenChange: onOpenChange, title: rule ? 'Edit redirect' : 'New redirect', width: "max-w-lg", footer: _jsxs(_Fragment, { children: [_jsx("button", { className: btnSecondary, onClick: () => onOpenChange(false), disabled: saving, children: "Cancel" }), _jsx("button", { className: btnPrimary, onClick: handleSave, disabled: saving, children: saving ? 'Saving…' : rule ? 'Save changes' : 'Create redirect' })] }), children: _jsxs("div", { className: "space-y-4", children: [clientError && (_jsx("p", { className: "text-destructive bg-destructive/10 rounded-md px-3 py-2 text-sm", role: "alert", children: clientError })), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-foreground text-sm font-medium", htmlFor: "redir-type", children: "Status code" }), _jsx("select", { id: "redir-type", className: inputCls, value: form.type, onChange: (e) => update({ type: Number(e.target.value) }), children: STATUS_CODES.map((s) => (_jsx("option", { value: s.value, children: s.label }, s.value))) })] }), _jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-foreground text-sm font-medium", htmlFor: "redir-from", children: "Source path" }), _jsx("input", { id: "redir-from", className: inputCls, value: form.fromPath, onChange: (e) => update({ fromPath: e.target.value }), placeholder: "/old-path" })] }), !isGone && (_jsxs("div", { className: "space-y-1.5", children: [_jsx("label", { className: "text-foreground text-sm font-medium", htmlFor: "redir-to", children: "Destination" }), _jsx("input", { id: "redir-to", className: inputCls, value: form.toPath, onChange: (e) => update({ toPath: e.target.value }), placeholder: "/new-path or https://example.com/page" })] })), _jsxs("div", { className: "space-y-1.5", children: [_jsxs("label", { className: "text-foreground text-sm font-medium", htmlFor: "redir-notes", children: ["Notes ", _jsx("span", { className: "text-muted-foreground font-normal", children: "(optional)" })] }), _jsx("input", { id: "redir-notes", className: inputCls, value: form.notes ?? '', onChange: (e) => update({ notes: e.target.value }), placeholder: "Why this redirect exists" })] }), _jsxs("div", { className: "border-border space-y-3 rounded-lg border p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsx("span", { className: "text-foreground text-sm font-medium", children: "Enabled" }), _jsx(Toggle, { checked: !!form.enabled, onCheckedChange: (v) => update({ enabled: v }), variant: "enabled", "aria-label": "Enabled" })] }), !isGone && (_jsxs("div", { className: "flex items-center justify-between gap-3", children: [_jsxs("span", { className: "min-w-0", children: [_jsx("span", { className: "text-foreground block text-sm font-medium", children: "Preserve query string" }), _jsx("span", { className: "text-muted-foreground block text-sm", children: "Append the original query params to the destination." })] }), _jsx(Toggle, { checked: !!form.preserveQuery, onCheckedChange: (v) => update({ preserveQuery: v }), "aria-label": "Preserve query string" })] }))] })] }) }));
|
|
94
|
-
}
|
|
95
|
-
//# sourceMappingURL=RedirectEditorDrawer.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"RedirectEditorDrawer.js","sourceRoot":"","sources":["../../../src/components/seo/RedirectEditorDrawer.tsx"],"names":[],"mappings":"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"}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
'use client'
|
|
2
|
-
|
|
3
|
-
import { useEffect, useState } from 'react'
|
|
4
|
-
import { toast } from 'sonner'
|
|
5
|
-
import {
|
|
6
|
-
createRedirectRule,
|
|
7
|
-
updateRedirectRule,
|
|
8
|
-
type RedirectRule,
|
|
9
|
-
type RedirectRulePayload,
|
|
10
|
-
type RedirectStatusCode,
|
|
11
|
-
} from '../../lib/seo-service.js'
|
|
12
|
-
import { Drawer } from './Drawer.js'
|
|
13
|
-
import { btnPrimary, btnSecondary } from './primitives.js'
|
|
14
|
-
import { Toggle } from '../ui/Toggle.js'
|
|
15
|
-
|
|
16
|
-
const STATUS_CODES: { value: RedirectStatusCode; label: string }[] = [
|
|
17
|
-
{ value: 301, label: '301 — Permanent' },
|
|
18
|
-
{ value: 302, label: '302 — Temporary' },
|
|
19
|
-
{ value: 307, label: '307 — Temporary (preserve method)' },
|
|
20
|
-
{ value: 308, label: '308 — Permanent (preserve method)' },
|
|
21
|
-
{ value: 410, label: '410 — Gone (no destination)' },
|
|
22
|
-
]
|
|
23
|
-
|
|
24
|
-
const inputCls =
|
|
25
|
-
'w-full rounded-md border border-border bg-input-background px-3 py-2 text-base text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'
|
|
26
|
-
|
|
27
|
-
function normalizePath(p: string): string {
|
|
28
|
-
const t = p.trim()
|
|
29
|
-
if (!t) return ''
|
|
30
|
-
if (/^https?:\/\//i.test(t)) return t
|
|
31
|
-
return t.startsWith('/') ? t : `/${t}`
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function RedirectEditorDrawer({
|
|
35
|
-
open,
|
|
36
|
-
onOpenChange,
|
|
37
|
-
rule,
|
|
38
|
-
onSaved,
|
|
39
|
-
}: {
|
|
40
|
-
open: boolean
|
|
41
|
-
onOpenChange: (open: boolean) => void
|
|
42
|
-
/** null = create mode */
|
|
43
|
-
rule: RedirectRule | null
|
|
44
|
-
onSaved?: () => void
|
|
45
|
-
}) {
|
|
46
|
-
const [form, setForm] = useState<RedirectRulePayload>({
|
|
47
|
-
type: 301,
|
|
48
|
-
fromPath: '',
|
|
49
|
-
toPath: '',
|
|
50
|
-
enabled: true,
|
|
51
|
-
preserveQuery: false,
|
|
52
|
-
notes: '',
|
|
53
|
-
})
|
|
54
|
-
const [saving, setSaving] = useState(false)
|
|
55
|
-
const [clientError, setClientError] = useState<string | null>(null)
|
|
56
|
-
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
if (!open) return
|
|
59
|
-
if (rule) {
|
|
60
|
-
setForm({
|
|
61
|
-
type: rule.type,
|
|
62
|
-
fromPath: rule.fromPath,
|
|
63
|
-
toPath: rule.toPath,
|
|
64
|
-
enabled: rule.enabled,
|
|
65
|
-
preserveQuery: rule.preserveQuery,
|
|
66
|
-
notes: rule.notes ?? '',
|
|
67
|
-
})
|
|
68
|
-
} else {
|
|
69
|
-
setForm({
|
|
70
|
-
type: 301,
|
|
71
|
-
fromPath: '',
|
|
72
|
-
toPath: '',
|
|
73
|
-
enabled: true,
|
|
74
|
-
preserveQuery: false,
|
|
75
|
-
notes: '',
|
|
76
|
-
})
|
|
77
|
-
}
|
|
78
|
-
setClientError(null)
|
|
79
|
-
}, [open, rule])
|
|
80
|
-
|
|
81
|
-
const isGone = form.type === 410
|
|
82
|
-
const update = (patch: Partial<RedirectRulePayload>) => setForm((f) => ({ ...f, ...patch }))
|
|
83
|
-
|
|
84
|
-
async function handleSave() {
|
|
85
|
-
const fromPath = normalizePath(form.fromPath)
|
|
86
|
-
const toPath = isGone ? '' : normalizePath(form.toPath)
|
|
87
|
-
if (!fromPath) {
|
|
88
|
-
setClientError('Source path is required.')
|
|
89
|
-
return
|
|
90
|
-
}
|
|
91
|
-
if (!isGone && !toPath) {
|
|
92
|
-
setClientError('Destination is required for redirect status codes.')
|
|
93
|
-
return
|
|
94
|
-
}
|
|
95
|
-
if (!isGone && fromPath === toPath) {
|
|
96
|
-
setClientError('Source and destination cannot be identical (would create a loop).')
|
|
97
|
-
return
|
|
98
|
-
}
|
|
99
|
-
setClientError(null)
|
|
100
|
-
setSaving(true)
|
|
101
|
-
const payload: RedirectRulePayload = { ...form, fromPath, toPath }
|
|
102
|
-
const res = rule
|
|
103
|
-
? await updateRedirectRule(rule.id, payload)
|
|
104
|
-
: await createRedirectRule(payload)
|
|
105
|
-
setSaving(false)
|
|
106
|
-
if (res.error) {
|
|
107
|
-
toast.error(res.error)
|
|
108
|
-
return
|
|
109
|
-
}
|
|
110
|
-
toast.success(rule ? 'Redirect updated.' : 'Redirect created.')
|
|
111
|
-
onOpenChange(false)
|
|
112
|
-
onSaved?.()
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return (
|
|
116
|
-
<Drawer
|
|
117
|
-
open={open}
|
|
118
|
-
onOpenChange={onOpenChange}
|
|
119
|
-
title={rule ? 'Edit redirect' : 'New redirect'}
|
|
120
|
-
width="max-w-lg"
|
|
121
|
-
footer={
|
|
122
|
-
<>
|
|
123
|
-
<button className={btnSecondary} onClick={() => onOpenChange(false)} disabled={saving}>
|
|
124
|
-
Cancel
|
|
125
|
-
</button>
|
|
126
|
-
<button className={btnPrimary} onClick={handleSave} disabled={saving}>
|
|
127
|
-
{saving ? 'Saving…' : rule ? 'Save changes' : 'Create redirect'}
|
|
128
|
-
</button>
|
|
129
|
-
</>
|
|
130
|
-
}
|
|
131
|
-
>
|
|
132
|
-
<div className="space-y-4">
|
|
133
|
-
{clientError && (
|
|
134
|
-
<p
|
|
135
|
-
className="text-destructive bg-destructive/10 rounded-md px-3 py-2 text-sm"
|
|
136
|
-
role="alert"
|
|
137
|
-
>
|
|
138
|
-
{clientError}
|
|
139
|
-
</p>
|
|
140
|
-
)}
|
|
141
|
-
<div className="space-y-1.5">
|
|
142
|
-
<label className="text-foreground text-sm font-medium" htmlFor="redir-type">
|
|
143
|
-
Status code
|
|
144
|
-
</label>
|
|
145
|
-
<select
|
|
146
|
-
id="redir-type"
|
|
147
|
-
className={inputCls}
|
|
148
|
-
value={form.type}
|
|
149
|
-
onChange={(e) => update({ type: Number(e.target.value) as RedirectStatusCode })}
|
|
150
|
-
>
|
|
151
|
-
{STATUS_CODES.map((s) => (
|
|
152
|
-
<option key={s.value} value={s.value}>
|
|
153
|
-
{s.label}
|
|
154
|
-
</option>
|
|
155
|
-
))}
|
|
156
|
-
</select>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<div className="space-y-1.5">
|
|
160
|
-
<label className="text-foreground text-sm font-medium" htmlFor="redir-from">
|
|
161
|
-
Source path
|
|
162
|
-
</label>
|
|
163
|
-
<input
|
|
164
|
-
id="redir-from"
|
|
165
|
-
className={inputCls}
|
|
166
|
-
value={form.fromPath}
|
|
167
|
-
onChange={(e) => update({ fromPath: e.target.value })}
|
|
168
|
-
placeholder="/old-path"
|
|
169
|
-
/>
|
|
170
|
-
</div>
|
|
171
|
-
|
|
172
|
-
{!isGone && (
|
|
173
|
-
<div className="space-y-1.5">
|
|
174
|
-
<label className="text-foreground text-sm font-medium" htmlFor="redir-to">
|
|
175
|
-
Destination
|
|
176
|
-
</label>
|
|
177
|
-
<input
|
|
178
|
-
id="redir-to"
|
|
179
|
-
className={inputCls}
|
|
180
|
-
value={form.toPath}
|
|
181
|
-
onChange={(e) => update({ toPath: e.target.value })}
|
|
182
|
-
placeholder="/new-path or https://example.com/page"
|
|
183
|
-
/>
|
|
184
|
-
</div>
|
|
185
|
-
)}
|
|
186
|
-
|
|
187
|
-
<div className="space-y-1.5">
|
|
188
|
-
<label className="text-foreground text-sm font-medium" htmlFor="redir-notes">
|
|
189
|
-
Notes <span className="text-muted-foreground font-normal">(optional)</span>
|
|
190
|
-
</label>
|
|
191
|
-
<input
|
|
192
|
-
id="redir-notes"
|
|
193
|
-
className={inputCls}
|
|
194
|
-
value={form.notes ?? ''}
|
|
195
|
-
onChange={(e) => update({ notes: e.target.value })}
|
|
196
|
-
placeholder="Why this redirect exists"
|
|
197
|
-
/>
|
|
198
|
-
</div>
|
|
199
|
-
|
|
200
|
-
<div className="border-border space-y-3 rounded-lg border p-4">
|
|
201
|
-
<div className="flex items-center justify-between gap-3">
|
|
202
|
-
<span className="text-foreground text-sm font-medium">Enabled</span>
|
|
203
|
-
<Toggle
|
|
204
|
-
checked={!!form.enabled}
|
|
205
|
-
onCheckedChange={(v) => update({ enabled: v })}
|
|
206
|
-
variant="enabled"
|
|
207
|
-
aria-label="Enabled"
|
|
208
|
-
/>
|
|
209
|
-
</div>
|
|
210
|
-
{!isGone && (
|
|
211
|
-
<div className="flex items-center justify-between gap-3">
|
|
212
|
-
<span className="min-w-0">
|
|
213
|
-
<span className="text-foreground block text-sm font-medium">
|
|
214
|
-
Preserve query string
|
|
215
|
-
</span>
|
|
216
|
-
<span className="text-muted-foreground block text-sm">
|
|
217
|
-
Append the original query params to the destination.
|
|
218
|
-
</span>
|
|
219
|
-
</span>
|
|
220
|
-
<Toggle
|
|
221
|
-
checked={!!form.preserveQuery}
|
|
222
|
-
onCheckedChange={(v) => update({ preserveQuery: v })}
|
|
223
|
-
aria-label="Preserve query string"
|
|
224
|
-
/>
|
|
225
|
-
</div>
|
|
226
|
-
)}
|
|
227
|
-
</div>
|
|
228
|
-
</div>
|
|
229
|
-
</Drawer>
|
|
230
|
-
)
|
|
231
|
-
}
|