@kyro-cms/admin 0.3.2 → 0.3.5
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/EditorClient-XEUOVAAC.js +466 -0
- package/dist/EditorClient-XEUOVAAC.js.map +1 -0
- package/dist/EditorClient-YLCGVDXY.cjs +468 -0
- package/dist/EditorClient-YLCGVDXY.cjs.map +1 -0
- package/dist/chunk-7KPIUCGT.js +384 -0
- package/dist/chunk-7KPIUCGT.js.map +1 -0
- package/dist/chunk-GOACG6R7.cjs +473 -0
- package/dist/chunk-GOACG6R7.cjs.map +1 -0
- package/dist/index.cjs +14861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +1661 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +563 -0
- package/dist/index.js +14784 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
- package/src/components/ActionBar.tsx +7 -43
- package/src/components/Admin.tsx +138 -277
- package/src/components/ApiKeysManager.tsx +428 -419
- package/src/components/AuditLogsPage.tsx +35 -39
- package/src/components/AuthBridge.tsx +51 -0
- package/src/components/AutoForm.tsx +495 -1230
- package/src/components/BrandingHub.tsx +18 -19
- package/src/components/BulkActionsBar.tsx +1 -1
- package/src/components/CreateView.tsx +22 -36
- package/src/components/Dashboard.tsx +60 -84
- package/src/components/DetailView.tsx +113 -91
- package/src/components/DeveloperCenter.tsx +200 -198
- package/src/components/FieldRenderer.tsx +206 -0
- package/src/components/GraphQLPlayground.tsx +340 -480
- package/src/components/ListView.tsx +828 -254
- package/src/components/LoginPage.tsx +3 -4
- package/src/components/MarketplaceManager.tsx +254 -0
- package/src/components/MediaGallery.tsx +856 -1192
- package/src/components/PluginsManager.tsx +277 -0
- package/src/components/RestPlayground.tsx +398 -560
- package/src/components/SessionsManager.tsx +211 -0
- package/src/components/Sidebar.astro +179 -151
- package/src/components/ThemeProvider.tsx +7 -161
- package/src/components/UserManagement.tsx +162 -146
- package/src/components/UserMenu.tsx +110 -0
- package/src/components/WebhookManager.tsx +305 -367
- package/src/components/blocks/AccordionBlock.tsx +4 -4
- package/src/components/blocks/ArrayBlock.tsx +3 -3
- package/src/components/blocks/BlockEditModal.tsx +8 -8
- package/src/components/blocks/BlockWrapper.tsx +61 -0
- package/src/components/blocks/ButtonBlock.tsx +4 -4
- package/src/components/blocks/ChildBlocksTree.tsx +23 -25
- package/src/components/blocks/CodeBlock.tsx +15 -15
- package/src/components/blocks/ColumnsBlock.tsx +6 -44
- package/src/components/blocks/DividerBlock.tsx +3 -3
- package/src/components/blocks/FileBlock.tsx +4 -4
- package/src/components/blocks/HeadingBlock.tsx +6 -38
- package/src/components/blocks/HeroBlock.tsx +4 -4
- package/src/components/blocks/ImageBlock.tsx +4 -4
- package/src/components/blocks/LinkBlock.tsx +4 -4
- package/src/components/blocks/ListBlock.tsx +3 -3
- package/src/components/blocks/ParagraphBlock.tsx +12 -42
- package/src/components/blocks/RelationshipBlock.tsx +4 -4
- package/src/components/blocks/RichTextBlock.tsx +4 -4
- package/src/components/blocks/VStackBlock.tsx +5 -37
- package/src/components/blocks/VideoBlock.tsx +4 -4
- package/src/components/blocks/types.ts +11 -0
- package/src/components/fields/AccordionField.tsx +1 -1
- package/src/components/fields/ArrayField.tsx +2 -2
- package/src/components/fields/ArrayLayout.tsx +93 -0
- package/src/components/fields/BlocksField.tsx +122 -111
- package/src/components/fields/ButtonField.tsx +1 -1
- package/src/components/fields/CheckboxField.tsx +14 -15
- package/src/components/fields/ChildrenField.tsx +2 -2
- package/src/components/fields/CodeField.tsx +3 -3
- package/src/components/fields/ColumnsField.tsx +2 -2
- package/src/components/fields/DateField.tsx +13 -26
- package/src/components/fields/EditorClient.tsx +26 -28
- package/src/components/fields/FieldLayout.tsx +52 -0
- package/src/components/fields/GroupLayout.tsx +35 -0
- package/src/components/fields/JSONField.tsx +7 -7
- package/src/components/fields/LinkField.tsx +1 -1
- package/src/components/fields/MarkdownField.tsx +1 -1
- package/src/components/fields/NumberField.tsx +13 -26
- package/src/components/fields/PortableTextField.tsx +4 -4
- package/src/components/fields/PortableTextRenderer.tsx +1 -1
- package/src/components/fields/RelationshipBlockField.tsx +31 -23
- package/src/components/fields/RelationshipField.tsx +14 -14
- package/src/components/fields/SelectField.tsx +17 -26
- package/src/components/fields/TabsLayout.tsx +69 -0
- package/src/components/fields/TextField.tsx +85 -38
- package/src/components/fields/UploadField.tsx +71 -41
- package/src/components/fields/VideoField.tsx +1 -1
- package/src/components/fields/extensions/blockComponents.tsx +2 -2
- package/src/components/fields/extensions/blocksStore.ts +207 -193
- package/src/components/fields/types.ts +22 -0
- package/src/components/layout/Layout.tsx +1 -1
- package/src/components/ui/ActionMenu.tsx +63 -0
- package/src/components/ui/Badge.tsx +59 -5
- package/src/components/ui/BlockDrawer.tsx +4 -5
- package/src/components/ui/CommandPalette.tsx +58 -36
- package/src/components/ui/CommandPaletteWrapper.tsx +18 -17
- package/src/components/ui/Dropdown.tsx +18 -16
- package/src/components/ui/EmptyState.tsx +25 -0
- package/src/components/ui/GlobalModal.tsx +49 -0
- package/src/components/ui/IconButton.tsx +44 -0
- package/src/components/ui/Modal.tsx +19 -20
- package/src/components/ui/PageHeader.tsx +158 -0
- package/src/components/ui/Pagination.tsx +61 -0
- package/src/components/ui/PromptModal.tsx +1 -1
- package/src/components/ui/SearchInput.tsx +57 -0
- package/src/components/ui/SeoPreview.tsx +31 -0
- package/src/components/ui/SessionModal.tsx +0 -0
- package/src/components/ui/SlidePanel.tsx +2 -0
- package/src/components/ui/Toast.tsx +65 -122
- package/src/components/ui/Toaster.tsx +18 -0
- package/src/components/ui/icons.tsx +112 -0
- package/src/components/users/UserDetail.tsx +290 -0
- package/src/components/users/UserForm.tsx +242 -0
- package/src/components/users/UsersList.tsx +338 -0
- package/src/env.d.ts +13 -13
- package/src/fields/index.ts +2 -1
- package/src/global.d.ts +7 -0
- package/src/hooks/data.ts +2 -9
- package/src/hooks/useAsyncData.ts +36 -0
- package/src/hooks/useAutoFormState.ts +527 -0
- package/src/hooks/useSelection.ts +49 -0
- package/src/hooks/useSession.ts +0 -0
- package/src/index.ts +11 -1
- package/src/integration.ts +86 -11
- package/src/kyro-cms.d.ts +209 -0
- package/src/layouts/AdminLayout.astro +128 -11
- package/src/layouts/AuthLayout.astro +21 -5
- package/src/lib/api.ts +175 -55
- package/src/lib/autoform-store.ts +435 -0
- package/src/lib/config.ts +82 -34
- package/src/lib/createRegistry.ts +29 -0
- package/src/lib/default-kyro-config.ts +4 -0
- package/src/lib/globals.ts +50 -0
- package/src/lib/media-utils.ts +18 -0
- package/src/lib/object-utils.ts +77 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/stores/index.ts +370 -0
- package/src/lib/types.ts +43 -0
- package/src/lib/useResourceManager.ts +105 -0
- package/src/pages/403.astro +67 -0
- package/src/pages/[collection]/[id].astro +14 -180
- package/src/pages/[collection]/index.astro +11 -6
- package/src/pages/api-explorer.astro +173 -0
- package/src/pages/audit/index.astro +2 -0
- package/src/pages/auth/login.astro +122 -0
- package/src/pages/auth/register.astro +167 -0
- package/src/pages/graphql-explorer.astro +59 -0
- package/src/pages/{admin/graphql.astro → graphql.astro} +51 -17
- package/src/pages/index.astro +577 -0
- package/src/pages/index_ALT.astro +3 -0
- package/src/pages/keys.astro +11 -0
- package/src/pages/marketplace.astro +11 -0
- package/src/pages/media.astro +3 -0
- package/src/pages/plugins.astro +8 -0
- package/src/pages/preview/[collection]/[id].astro +188 -123
- package/src/pages/rest-playground.astro +62 -0
- package/src/pages/roles/index.astro +183 -76
- package/src/pages/sessions.astro +8 -0
- package/src/pages/settings/[slug].astro +92 -114
- package/src/pages/settings/index.astro +5 -3
- package/src/pages/users/[id].astro +25 -154
- package/src/pages/users/index.astro +19 -130
- package/src/pages/users/new.astro +9 -86
- package/src/pages/webhooks.astro +11 -0
- package/src/routes.ts +80 -0
- package/src/styles/main.css +119 -79
- package/src/theme/tokens.ts +1 -0
- package/src/vite-env.d.ts +14 -0
- package/src/collections/auth/index.ts +0 -155
- package/src/collections/portfolio/index.ts +0 -343
- package/src/components/ApiExplorer.tsx +0 -325
- package/src/components/EnhancedListView.tsx +0 -889
- package/src/components/GraphQLExplorer.tsx +0 -675
- package/src/components/Icons.tsx +0 -23
- package/src/components/StatusBadge.tsx +0 -76
- package/src/lib/MediaService.ts +0 -541
- package/src/lib/auth/sqlite-adapter.ts +0 -319
- package/src/lib/dataStore.ts +0 -226
- package/src/lib/db/adapter.ts +0 -54
- package/src/lib/db/drizzle-mysql-adapter.ts +0 -194
- package/src/lib/db/drizzle-mysql-auth-adapter.ts +0 -327
- package/src/lib/db/drizzle-postgres-adapter.ts +0 -202
- package/src/lib/db/drizzle-postgres-auth-adapter.ts +0 -304
- package/src/lib/db/drizzle-sqlite-adapter.ts +0 -227
- package/src/lib/db/drizzle-sqlite-auth-adapter.ts +0 -548
- package/src/lib/db/index.ts +0 -449
- package/src/lib/db/mongodb-adapter.ts +0 -207
- package/src/lib/db/mongodb-auth-adapter.ts +0 -305
- package/src/lib/db/schema/mysql-auth.ts +0 -113
- package/src/lib/db/schema/mysql-content.ts +0 -20
- package/src/lib/db/schema/postgres-auth.ts +0 -116
- package/src/lib/db/schema/postgres-content.ts +0 -35
- package/src/lib/db/schema/postgres-media.ts +0 -52
- package/src/lib/db/schema/postgres-settings.ts +0 -11
- package/src/lib/db/schema/sqlite-auth.ts +0 -112
- package/src/lib/db/schema/sqlite-content.ts +0 -20
- package/src/lib/db/version-adapter.ts +0 -248
- package/src/lib/graphql/index.ts +0 -1
- package/src/lib/graphql/schema.ts +0 -443
- package/src/lib/rate-limit.ts +0 -267
- package/src/lib/storage.ts +0 -374
- package/src/lib/store.ts +0 -85
- package/src/middleware.ts +0 -177
- package/src/pages/admin/api-explorer.astro +0 -98
- package/src/pages/admin/graphql-explorer.astro +0 -40
- package/src/pages/admin/index.astro +0 -286
- package/src/pages/admin/keys.astro +0 -8
- package/src/pages/admin/rest-playground.astro +0 -44
- package/src/pages/admin/webhooks.astro +0 -8
- package/src/pages/api/[collection]/[id]/publish.ts +0 -52
- package/src/pages/api/[collection]/[id]/unpublish.ts +0 -42
- package/src/pages/api/[collection]/[id]/versions.ts +0 -66
- package/src/pages/api/[collection]/[id].ts +0 -213
- package/src/pages/api/[collection]/index.ts +0 -209
- package/src/pages/api/auth/[id].ts +0 -121
- package/src/pages/api/auth/audit-logs.ts +0 -57
- package/src/pages/api/auth/login.ts +0 -211
- package/src/pages/api/auth/logout.ts +0 -66
- package/src/pages/api/auth/me.ts +0 -36
- package/src/pages/api/auth/refresh.ts +0 -119
- package/src/pages/api/auth/register.ts +0 -188
- package/src/pages/api/auth/users.ts +0 -97
- package/src/pages/api/collections.ts +0 -59
- package/src/pages/api/globals/[slug].ts +0 -42
- package/src/pages/api/graphql.ts +0 -90
- package/src/pages/api/health.ts +0 -426
- package/src/pages/api/keys/[id].ts +0 -26
- package/src/pages/api/keys/index.ts +0 -75
- package/src/pages/api/media/[id].ts +0 -309
- package/src/pages/api/media/folders.ts +0 -609
- package/src/pages/api/media/index.ts +0 -146
- package/src/pages/api/media/resize.ts +0 -267
- package/src/pages/api/search.ts +0 -82
- package/src/pages/api/slug-availability.ts +0 -70
- package/src/pages/api/storage-config.ts +0 -20
- package/src/pages/api/storage-status.ts +0 -206
- package/src/pages/api/upload.ts +0 -334
- package/src/pages/api/webhooks/index.ts +0 -71
- package/src/pages/login.astro +0 -82
- package/src/pages/register.astro +0 -102
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Field } from "@kyro-cms/core/client";
|
|
3
|
+
|
|
4
|
+
interface FieldLayoutProps {
|
|
5
|
+
field: Field;
|
|
6
|
+
error?: string;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
hideLabel?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function FieldLayout({
|
|
12
|
+
field,
|
|
13
|
+
error,
|
|
14
|
+
children,
|
|
15
|
+
hideLabel = false,
|
|
16
|
+
}: FieldLayoutProps) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="space-y-2.5 w-full group">
|
|
19
|
+
{field.label && !hideLabel && (
|
|
20
|
+
<div className="flex justify-between items-end mb-1">
|
|
21
|
+
<label className="block text-xs font-bold tracking-widest text-[var(--kyro-text-secondary)] opacity-50 group-focus-within:opacity-100 group-focus-within:text-[var(--kyro-primary)] transition-all duration-300">
|
|
22
|
+
{field.label}
|
|
23
|
+
{field.required && (
|
|
24
|
+
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
25
|
+
)}
|
|
26
|
+
</label>
|
|
27
|
+
</div>
|
|
28
|
+
)}
|
|
29
|
+
|
|
30
|
+
<div className="relative transform transition-transform duration-200 focus-within:scale-[1.002]">
|
|
31
|
+
{children}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{(field.admin?.description || error) && (
|
|
35
|
+
<div className="flex flex-col gap-1.5 px-1">
|
|
36
|
+
{field.admin?.description && !error && (
|
|
37
|
+
<p className="text-[11px] leading-relaxed text-[var(--kyro-text-muted)] font-medium opacity-60 italic">
|
|
38
|
+
{field.admin.description}
|
|
39
|
+
</p>
|
|
40
|
+
)}
|
|
41
|
+
{error && (
|
|
42
|
+
<p className="text-[11px] leading-relaxed text-[var(--kyro-error)] font-bold flex items-center gap-2 animate-in fade-in slide-in-from-top-1 duration-300">
|
|
43
|
+
<span className="w-1.5 h-1.5 rounded-full bg-[var(--kyro-error)] shadow-[0_0_8px_var(--kyro-error)]" />
|
|
44
|
+
{error}
|
|
45
|
+
</p>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Field } from "@kyro-cms/core/client";
|
|
3
|
+
|
|
4
|
+
interface GroupLayoutProps {
|
|
5
|
+
field: Field;
|
|
6
|
+
value: Record<string, unknown> | null;
|
|
7
|
+
onChange: (value: Record<string, unknown>) => void;
|
|
8
|
+
renderField: (
|
|
9
|
+
field: Field,
|
|
10
|
+
parentData: Record<string, unknown>,
|
|
11
|
+
onChange: (value: unknown) => void,
|
|
12
|
+
) => React.ReactNode;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function GroupLayout({
|
|
16
|
+
field,
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
renderField,
|
|
20
|
+
}: GroupLayoutProps) {
|
|
21
|
+
const groupData = value || {};
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="kyro-form-group border border-[var(--kyro-border)] rounded-[var(--kyro-radius-lg)] p-6 bg-[var(--kyro-surface-accent)]/30">
|
|
25
|
+
<h3 className="text-sm font-bold tracking-widest text-[var(--kyro-text-primary)] mb-6 border-b border-[var(--kyro-border)] pb-2 inline-block">
|
|
26
|
+
{field.label || field.name}
|
|
27
|
+
</h3>
|
|
28
|
+
<div className="space-y-6">
|
|
29
|
+
{(field as Field & { fields?: Field[] }).fields.map((f: Field) =>
|
|
30
|
+
renderField(f, groupData, onChange),
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -3,15 +3,15 @@ import type { JSONField as JSONFieldType } from "@kyro-cms/core/client";
|
|
|
3
3
|
|
|
4
4
|
interface JSONFieldProps {
|
|
5
5
|
field: JSONFieldType;
|
|
6
|
-
value?:
|
|
7
|
-
onChange?: (value:
|
|
6
|
+
value?: unknown;
|
|
7
|
+
onChange?: (value: unknown) => void;
|
|
8
8
|
error?: string;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
interface TreeNode {
|
|
13
13
|
key: string;
|
|
14
|
-
value:
|
|
14
|
+
value: unknown;
|
|
15
15
|
type: "object" | "array" | "string" | "number" | "boolean" | "null";
|
|
16
16
|
path: string[];
|
|
17
17
|
collapsed?: boolean;
|
|
@@ -232,7 +232,7 @@ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
|
|
|
232
232
|
if (node.type === "array") {
|
|
233
233
|
return (
|
|
234
234
|
<div className="pl-4 border-l border-[var(--kyro-border)]">
|
|
235
|
-
{node.value.map((item:
|
|
235
|
+
{node.value.map((item: unknown, index: number) => (
|
|
236
236
|
<TreeNodeView
|
|
237
237
|
key={index}
|
|
238
238
|
name={String(index)}
|
|
@@ -315,7 +315,7 @@ const TreeView: React.FC<{ data: TreeNode }> = ({ data }) => {
|
|
|
315
315
|
);
|
|
316
316
|
};
|
|
317
317
|
|
|
318
|
-
const TreeNodeView: React.FC<{ name: string; value:
|
|
318
|
+
const TreeNodeView: React.FC<{ name: string; value: unknown; path: string[] }> = ({
|
|
319
319
|
name,
|
|
320
320
|
value,
|
|
321
321
|
path,
|
|
@@ -325,13 +325,13 @@ const TreeNodeView: React.FC<{ name: string; value: any; path: string[] }> = ({
|
|
|
325
325
|
return <TreeView data={treeNode} />;
|
|
326
326
|
};
|
|
327
327
|
|
|
328
|
-
function getType(value:
|
|
328
|
+
function getType(value: unknown): TreeNode["type"] {
|
|
329
329
|
if (value === null) return "null";
|
|
330
330
|
if (Array.isArray(value)) return "array";
|
|
331
331
|
return typeof value as TreeNode["type"];
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
function buildTree(value:
|
|
334
|
+
function buildTree(value: unknown, path: string[]): TreeNode {
|
|
335
335
|
return {
|
|
336
336
|
key: path[path.length - 1] || "root",
|
|
337
337
|
value,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NumberField as NumberFieldType } from "@kyro-cms/core/client";
|
|
2
|
+
import FieldLayout from "./FieldLayout";
|
|
2
3
|
|
|
3
4
|
interface NumberFieldComponentProps {
|
|
4
5
|
field: NumberFieldType;
|
|
@@ -15,42 +16,28 @@ export default function NumberField({
|
|
|
15
16
|
error,
|
|
16
17
|
disabled,
|
|
17
18
|
}: NumberFieldComponentProps) {
|
|
19
|
+
const isReadOnly = field.admin?.readOnly;
|
|
20
|
+
|
|
18
21
|
return (
|
|
19
|
-
<
|
|
20
|
-
{field
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{field.required && (
|
|
24
|
-
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
25
|
-
)}
|
|
26
|
-
</label>
|
|
27
|
-
)}
|
|
22
|
+
<FieldLayout
|
|
23
|
+
field={field}
|
|
24
|
+
error={error}
|
|
25
|
+
>
|
|
28
26
|
<input
|
|
29
27
|
type="number"
|
|
28
|
+
id={field.name}
|
|
30
29
|
value={value ?? ""}
|
|
31
30
|
onChange={(e) => onChange?.(parseFloat(e.target.value) || 0)}
|
|
32
31
|
placeholder={field.admin?.placeholder}
|
|
33
|
-
disabled={disabled ||
|
|
32
|
+
disabled={disabled || isReadOnly}
|
|
34
33
|
min={field.min}
|
|
35
34
|
max={field.max}
|
|
36
|
-
step={field.step || field.integer ? 1 : "any"}
|
|
35
|
+
step={field.step || (field.integer ? 1 : "any")}
|
|
37
36
|
required={field.required}
|
|
38
|
-
className={`
|
|
39
|
-
|
|
40
|
-
? "border-[var(--kyro-error)] focus:border-[var(--kyro-error)] focus:ring-[var(--kyro-error)]"
|
|
41
|
-
: "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
|
|
42
|
-
} ${
|
|
43
|
-
disabled || field.admin?.readOnly
|
|
44
|
-
? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-secondary)] opacity-50"
|
|
45
|
-
: "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"
|
|
37
|
+
className={`kyro-form-input ${
|
|
38
|
+
disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
|
|
46
39
|
}`}
|
|
47
40
|
/>
|
|
48
|
-
|
|
49
|
-
<p className="text-xs text-[var(--kyro-text-muted)]">
|
|
50
|
-
{field.admin.description}
|
|
51
|
-
</p>
|
|
52
|
-
)}
|
|
53
|
-
{error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
|
|
54
|
-
</div>
|
|
41
|
+
</FieldLayout>
|
|
55
42
|
);
|
|
56
43
|
}
|
|
@@ -18,15 +18,15 @@ interface PortableTextFieldProps {
|
|
|
18
18
|
placeholder?: string;
|
|
19
19
|
};
|
|
20
20
|
};
|
|
21
|
-
value?:
|
|
22
|
-
onChange?: (value:
|
|
21
|
+
value?: unknown;
|
|
22
|
+
onChange?: (value: unknown) => void;
|
|
23
23
|
error?: string;
|
|
24
24
|
disabled?: boolean;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const EditorLazy = lazy(() => import("./EditorClient"));
|
|
28
28
|
|
|
29
|
-
function toPortableTextArray(value:
|
|
29
|
+
function toPortableTextArray(value: unknown): Record<string, unknown>[] {
|
|
30
30
|
if (Array.isArray(value)) {
|
|
31
31
|
if (
|
|
32
32
|
value.length > 0 &&
|
|
@@ -86,7 +86,7 @@ const PortableTextField: React.FC<PortableTextFieldProps> = ({
|
|
|
86
86
|
}, [ptValue, isMounted]);
|
|
87
87
|
|
|
88
88
|
const handleChange = useCallback(
|
|
89
|
-
(blocks:
|
|
89
|
+
(blocks: Record<string, unknown>[]) => {
|
|
90
90
|
if (!blocks || !Array.isArray(blocks)) {
|
|
91
91
|
onChange?.([]);
|
|
92
92
|
return;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { Search, Loader2, X } from "
|
|
2
|
+
import { Search, Loader2, X } from "../ui/icons";
|
|
3
3
|
import { apiGet, buildSearchQuery } from "../../lib/api";
|
|
4
4
|
|
|
5
5
|
interface RelationshipBlockFieldProps {
|
|
@@ -8,7 +8,7 @@ interface RelationshipBlockFieldProps {
|
|
|
8
8
|
selectedIds?: string[];
|
|
9
9
|
selectedId?: string;
|
|
10
10
|
labelField?: string;
|
|
11
|
-
onChange: (field: string, value:
|
|
11
|
+
onChange: (field: string, value: unknown) => void;
|
|
12
12
|
compact?: boolean;
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -23,7 +23,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
|
|
|
23
23
|
}) => {
|
|
24
24
|
const [isOpen, setIsOpen] = useState(false);
|
|
25
25
|
const [search, setSearch] = useState("");
|
|
26
|
-
const [options, setOptions] = useState<
|
|
26
|
+
const [options, setOptions] = useState<Record<string, unknown>[]>([]);
|
|
27
27
|
const [loading, setLoading] = useState(false);
|
|
28
28
|
const [collections, setCollections] = useState<string[]>([]);
|
|
29
29
|
const [loadingCollections, setLoadingCollections] = useState(true);
|
|
@@ -32,7 +32,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
|
|
|
32
32
|
apiGet("/api/collections")
|
|
33
33
|
.then((data) => {
|
|
34
34
|
setCollections(
|
|
35
|
-
(data.collections || []).map((c:
|
|
35
|
+
(data.collections || []).map((c: Record<string, unknown>) => c.slug || c.name || c),
|
|
36
36
|
);
|
|
37
37
|
setLoadingCollections(false);
|
|
38
38
|
})
|
|
@@ -55,7 +55,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
|
|
|
55
55
|
if (isOpen) fetchOptions(search);
|
|
56
56
|
}, [isOpen, search, relationTo, labelField]);
|
|
57
57
|
|
|
58
|
-
const getLabel = (opt:
|
|
58
|
+
const getLabel = (opt: Record<string, unknown>) => {
|
|
59
59
|
return (
|
|
60
60
|
opt?.[labelField] ||
|
|
61
61
|
opt?.title ||
|
|
@@ -70,7 +70,7 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
|
|
|
70
70
|
|
|
71
71
|
const activeIds = hasMany ? selectedIds : selectedId ? [selectedId] : [];
|
|
72
72
|
|
|
73
|
-
const handleSelect = (opt:
|
|
73
|
+
const handleSelect = (opt: Record<string, unknown>) => {
|
|
74
74
|
if (hasMany) {
|
|
75
75
|
if (activeIds.includes(opt.id)) {
|
|
76
76
|
onChange(
|
|
@@ -97,26 +97,34 @@ export const RelationshipBlockField: React.FC<RelationshipBlockFieldProps> = ({
|
|
|
97
97
|
? "w-full px-2.5 py-1.5 border border-[var(--kyro-border)] rounded bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent"
|
|
98
98
|
: "w-full px-3 py-2.5 border border-[var(--kyro-border)] rounded-lg bg-[var(--kyro-bg-secondary)] text-sm text-[var(--kyro-text-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--kyro-sidebar-active)] focus:border-transparent";
|
|
99
99
|
|
|
100
|
+
// Check if relationTo is a known single collection (not an array)
|
|
101
|
+
const isKnownCollection = relationTo && typeof relationTo === "string" && relationTo.length > 0;
|
|
102
|
+
|
|
100
103
|
return (
|
|
101
104
|
<div className={compact ? "space-y-2" : "space-y-4"}>
|
|
102
105
|
<div className={compact ? "flex items-center gap-2" : "space-y-3"}>
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
{/* Hide collection selector if relationTo is known (single collection) */}
|
|
107
|
+
{!isKnownCollection && (
|
|
108
|
+
<>
|
|
109
|
+
{loadingCollections ? (
|
|
110
|
+
<div className={selectClass + " text-[var(--kyro-text-muted)]"}>
|
|
111
|
+
Loading...
|
|
112
|
+
</div>
|
|
113
|
+
) : (
|
|
114
|
+
<select
|
|
115
|
+
value={relationTo}
|
|
116
|
+
onChange={(e) => onChange("relationTo", e.target.value)}
|
|
117
|
+
className={selectClass}
|
|
118
|
+
>
|
|
119
|
+
<option value="">Select collection...</option>
|
|
120
|
+
{collections.map((col) => (
|
|
121
|
+
<option key={col} value={col}>
|
|
122
|
+
{col}
|
|
123
|
+
</option>
|
|
124
|
+
))}
|
|
125
|
+
</select>
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
120
128
|
)}
|
|
121
129
|
|
|
122
130
|
{!compact && (
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEffect, useState, useRef } from "react";
|
|
2
|
-
import { Search, X, ChevronDown, Loader2 } from "
|
|
2
|
+
import { Search, X, ChevronDown, Loader2 } from "../ui/icons";
|
|
3
3
|
import { apiGet, buildSearchQuery } from "../../lib/api";
|
|
4
4
|
|
|
5
5
|
interface RelationshipFieldProps {
|
|
@@ -30,7 +30,7 @@ export function RelationshipField({
|
|
|
30
30
|
}: RelationshipFieldProps) {
|
|
31
31
|
const [isOpen, setIsOpen] = useState(false);
|
|
32
32
|
const [search, setSearch] = useState("");
|
|
33
|
-
const [options, setOptions] = useState<
|
|
33
|
+
const [options, setOptions] = useState<Record<string, unknown>[]>([]);
|
|
34
34
|
const [loading, setLoading] = useState(false);
|
|
35
35
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
36
36
|
|
|
@@ -49,9 +49,9 @@ export function RelationshipField({
|
|
|
49
49
|
.then((data) => {
|
|
50
50
|
setOptions((prev) => {
|
|
51
51
|
const existingIds = new Set(prev.map((o) => o.id));
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
const newDocs = (data.docs || []).filter(
|
|
53
|
+
(d: Record<string, unknown>) => !existingIds.has(d.id as string),
|
|
54
|
+
);
|
|
55
55
|
return [...prev, ...newDocs];
|
|
56
56
|
});
|
|
57
57
|
setLoading(false);
|
|
@@ -61,8 +61,8 @@ export function RelationshipField({
|
|
|
61
61
|
});
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
|
|
65
|
-
|
|
64
|
+
const fetchSelectedItems = () => {
|
|
65
|
+
const items: (string | Record<string, unknown>)[] = isMultiple
|
|
66
66
|
? Array.isArray(value)
|
|
67
67
|
? value
|
|
68
68
|
: []
|
|
@@ -106,7 +106,7 @@ export function RelationshipField({
|
|
|
106
106
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
107
107
|
}, []);
|
|
108
108
|
|
|
109
|
-
const getLabel = (opt:
|
|
109
|
+
const getLabel = (opt: Record<string, unknown>) => {
|
|
110
110
|
return (
|
|
111
111
|
opt?.title ||
|
|
112
112
|
opt?.name ||
|
|
@@ -119,11 +119,11 @@ export function RelationshipField({
|
|
|
119
119
|
);
|
|
120
120
|
};
|
|
121
121
|
|
|
122
|
-
const getValueId = (val:
|
|
122
|
+
const getValueId = (val: string | Record<string, unknown>) => {
|
|
123
123
|
return val?.id || val;
|
|
124
124
|
};
|
|
125
125
|
|
|
126
|
-
const isSelected = (opt:
|
|
126
|
+
const isSelected = (opt: Record<string, unknown>) => {
|
|
127
127
|
const optId = opt.id;
|
|
128
128
|
if (!value) return false;
|
|
129
129
|
if (isMultiple && Array.isArray(value)) {
|
|
@@ -132,7 +132,7 @@ export function RelationshipField({
|
|
|
132
132
|
return getValueId(value) === optId;
|
|
133
133
|
};
|
|
134
134
|
|
|
135
|
-
const handleSelect = (opt:
|
|
135
|
+
const handleSelect = (opt: Record<string, unknown>) => {
|
|
136
136
|
const optId = opt.id;
|
|
137
137
|
if (isMultiple) {
|
|
138
138
|
const current = Array.isArray(value) ? value : [];
|
|
@@ -159,7 +159,7 @@ export function RelationshipField({
|
|
|
159
159
|
const renderSelectedItems = () => {
|
|
160
160
|
if (!value) return null;
|
|
161
161
|
|
|
162
|
-
let items:
|
|
162
|
+
let items: (string | Record<string, unknown>)[];
|
|
163
163
|
if (isMultiple) {
|
|
164
164
|
items = Array.isArray(value) ? value : [];
|
|
165
165
|
} else {
|
|
@@ -183,8 +183,8 @@ export function RelationshipField({
|
|
|
183
183
|
type="button"
|
|
184
184
|
onClick={() => {
|
|
185
185
|
if (isMultiple) {
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
onChange?.(
|
|
187
|
+
(items as (string | Record<string, unknown>)[]).filter((_: unknown, i: number) => i !== idx),
|
|
188
188
|
);
|
|
189
189
|
} else {
|
|
190
190
|
onChange?.(null);
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { SelectField as SelectFieldType } from "@kyro-cms/core/client";
|
|
2
|
+
import FieldLayout from "./FieldLayout";
|
|
2
3
|
|
|
3
4
|
interface SelectFieldComponentProps {
|
|
4
5
|
field: SelectFieldType;
|
|
5
6
|
value?: string | string[];
|
|
6
|
-
onChange?: (value: string | string[]) => void;
|
|
7
|
+
onChange?: (value: string | string[] | undefined) => void;
|
|
7
8
|
error?: string;
|
|
8
9
|
disabled?: boolean;
|
|
9
10
|
}
|
|
@@ -15,17 +16,15 @@ export default function SelectField({
|
|
|
15
16
|
error,
|
|
16
17
|
disabled,
|
|
17
18
|
}: SelectFieldComponentProps) {
|
|
19
|
+
const isReadOnly = field.admin?.readOnly;
|
|
20
|
+
|
|
18
21
|
return (
|
|
19
|
-
<
|
|
20
|
-
{field
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{field.required && (
|
|
24
|
-
<span className="text-[var(--kyro-error)] ml-1">*</span>
|
|
25
|
-
)}
|
|
26
|
-
</label>
|
|
27
|
-
)}
|
|
22
|
+
<FieldLayout
|
|
23
|
+
field={field}
|
|
24
|
+
error={error}
|
|
25
|
+
>
|
|
28
26
|
<select
|
|
27
|
+
id={field.name}
|
|
29
28
|
value={
|
|
30
29
|
field.hasMany
|
|
31
30
|
? Array.isArray(value)
|
|
@@ -38,31 +37,23 @@ export default function SelectField({
|
|
|
38
37
|
const selected = e.target.value ? e.target.value.split(",") : [];
|
|
39
38
|
onChange?.(selected);
|
|
40
39
|
} else {
|
|
41
|
-
onChange?.(e.target.value);
|
|
40
|
+
onChange?.(e.target.value || undefined);
|
|
42
41
|
}
|
|
43
42
|
}}
|
|
44
43
|
multiple={field.hasMany}
|
|
45
|
-
disabled={disabled ||
|
|
44
|
+
disabled={disabled || isReadOnly}
|
|
46
45
|
required={field.required}
|
|
47
|
-
className={`
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
: "border-[var(--kyro-border)] focus:border-[var(--kyro-primary)] focus:ring-[var(--kyro-primary)]"
|
|
51
|
-
} ${disabled || field.admin?.readOnly ? "bg-[var(--kyro-bg-secondary)] text-[var(--kyro-text-muted)]" : "bg-[var(--kyro-surface)] text-[var(--kyro-text-primary)]"}`}
|
|
46
|
+
className={`kyro-form-input ${
|
|
47
|
+
disabled || isReadOnly ? "opacity-50 cursor-not-allowed" : ""
|
|
48
|
+
}`}
|
|
52
49
|
>
|
|
53
|
-
{!field.required && <option value="">Select...</option>}
|
|
54
|
-
{field.options
|
|
50
|
+
{!field.required && !field.hasMany && <option value="">Select...</option>}
|
|
51
|
+
{field.options?.map((option) => (
|
|
55
52
|
<option key={option.value} value={option.value}>
|
|
56
53
|
{option.label}
|
|
57
54
|
</option>
|
|
58
55
|
))}
|
|
59
56
|
</select>
|
|
60
|
-
|
|
61
|
-
<p className="text-xs text-[var(--kyro-text-secondary)]">
|
|
62
|
-
{field.admin.description}
|
|
63
|
-
</p>
|
|
64
|
-
)}
|
|
65
|
-
{error && <p className="text-xs text-[var(--kyro-error)]">{error}</p>}
|
|
66
|
-
</div>
|
|
57
|
+
</FieldLayout>
|
|
67
58
|
);
|
|
68
59
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import type { Field } from "@kyro-cms/core/client";
|
|
3
|
+
import { SeoPreview } from "../ui/SeoPreview";
|
|
4
|
+
|
|
5
|
+
interface TabsLayoutProps {
|
|
6
|
+
field: Field;
|
|
7
|
+
formData: Record<string, unknown>;
|
|
8
|
+
onTabDataChange: (tabData: Record<string, unknown>) => void;
|
|
9
|
+
renderField: (
|
|
10
|
+
field: Field,
|
|
11
|
+
parentData: Record<string, unknown>,
|
|
12
|
+
onChange: (value: unknown) => void,
|
|
13
|
+
) => React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function TabsLayout({
|
|
17
|
+
field,
|
|
18
|
+
formData,
|
|
19
|
+
onTabDataChange,
|
|
20
|
+
renderField,
|
|
21
|
+
}: TabsLayoutProps) {
|
|
22
|
+
const [activeTab, setActiveTab] = useState(0);
|
|
23
|
+
|
|
24
|
+
const fieldTabs = (field as Field & { tabs?: { label: string; fields: Field[] }[] }).tabs || [];
|
|
25
|
+
const currentTab = fieldTabs[activeTab] || fieldTabs[0];
|
|
26
|
+
|
|
27
|
+
// Get nested tab data
|
|
28
|
+
const tabData = formData[field.name as string] || {};
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="space-y-8">
|
|
32
|
+
<div className="flex items-center gap-2 border-b border-[var(--kyro-border)] mb-6 overflow-x-auto hide-scrollbar">
|
|
33
|
+
{fieldTabs.map((tab: { label: string }, index: number) => (
|
|
34
|
+
<button
|
|
35
|
+
key={index}
|
|
36
|
+
type="button"
|
|
37
|
+
className={`px-6 py-3 text-xs tracking-widest font-bold transition-all border-b-2 -mb-[1px] whitespace-nowrap ${activeTab === index
|
|
38
|
+
? "border-[var(--kyro-primary)] text-[var(--kyro-primary)]"
|
|
39
|
+
: "border-transparent text-[var(--kyro-text-secondary)] hover:text-[var(--kyro-text-primary)] opacity-60 hover:opacity-100"
|
|
40
|
+
}`}
|
|
41
|
+
onClick={() => setActiveTab(index)}
|
|
42
|
+
>
|
|
43
|
+
{tab.label}
|
|
44
|
+
</button>
|
|
45
|
+
))}
|
|
46
|
+
</div>
|
|
47
|
+
<div className="space-y-6">
|
|
48
|
+
{currentTab?.fields.map((f: Field) =>
|
|
49
|
+
renderField(f, tabData, onTabDataChange),
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
{currentTab?.label === "SEO" && (
|
|
54
|
+
<div className="mt-12 pt-8 border-t border-[var(--kyro-border)]">
|
|
55
|
+
<h4 className="text-[10px] font-bold text-[var(--kyro-text-secondary)] tracking-[0.2em] mb-6 opacity-50">
|
|
56
|
+
Live Google Preview
|
|
57
|
+
</h4>
|
|
58
|
+
<SeoPreview
|
|
59
|
+
title={formData.metaTitle || formData.title || "Untitled"}
|
|
60
|
+
description={
|
|
61
|
+
formData.metaDescription || "Please enter a description..."
|
|
62
|
+
}
|
|
63
|
+
slug={formData.slug || "your-slug"}
|
|
64
|
+
/>
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|