@btst/stack 1.8.0 → 1.9.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.
Files changed (94) hide show
  1. package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +445 -16
  2. package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +445 -16
  3. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +28 -11
  4. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +26 -9
  5. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.cjs +224 -0
  6. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.mjs +222 -0
  7. package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.cjs +243 -0
  8. package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.mjs +241 -0
  9. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +56 -2
  10. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +56 -2
  11. package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.cjs +190 -0
  12. package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.mjs +187 -1
  13. package/dist/packages/better-stack/src/plugins/cms/db.cjs +38 -0
  14. package/dist/packages/better-stack/src/plugins/cms/db.mjs +38 -0
  15. package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.cjs +2 -2
  16. package/dist/packages/better-stack/src/plugins/form-builder/client/components/forms/form-renderer.mjs +1 -1
  17. package/dist/packages/ui/src/components/auto-form/fields/array.cjs +2 -2
  18. package/dist/packages/ui/src/components/auto-form/fields/array.mjs +1 -1
  19. package/dist/packages/ui/src/components/auto-form/fields/date.cjs +2 -2
  20. package/dist/packages/ui/src/components/auto-form/fields/date.mjs +1 -1
  21. package/dist/packages/ui/src/components/auto-form/fields/enum.cjs +2 -2
  22. package/dist/packages/ui/src/components/auto-form/fields/enum.mjs +1 -1
  23. package/dist/packages/ui/src/components/auto-form/fields/object.cjs +88 -8
  24. package/dist/packages/ui/src/components/auto-form/fields/object.mjs +82 -2
  25. package/dist/packages/ui/src/components/auto-form/fields/radio-group.cjs +2 -2
  26. package/dist/packages/ui/src/components/auto-form/fields/radio-group.mjs +1 -1
  27. package/dist/packages/ui/src/components/auto-form/index.cjs +5 -5
  28. package/dist/packages/ui/src/components/auto-form/index.mjs +1 -1
  29. package/dist/packages/ui/src/components/button.cjs +4 -2
  30. package/dist/packages/ui/src/components/button.mjs +4 -2
  31. package/dist/packages/ui/src/components/dialog.cjs +7 -1
  32. package/dist/packages/ui/src/components/dialog.mjs +7 -2
  33. package/dist/packages/ui/src/components/form-builder/edit-field-dialog.cjs +2 -2
  34. package/dist/packages/ui/src/components/form-builder/edit-field-dialog.mjs +1 -1
  35. package/dist/packages/ui/src/components/form-builder/form-preview.cjs +5 -5
  36. package/dist/packages/ui/src/components/form-builder/form-preview.mjs +1 -1
  37. package/dist/packages/ui/src/components/select.cjs +9 -2
  38. package/dist/packages/ui/src/components/select.mjs +9 -2
  39. package/dist/plugins/blog/api/index.d.cts +1 -1
  40. package/dist/plugins/blog/api/index.d.mts +1 -1
  41. package/dist/plugins/blog/api/index.d.ts +1 -1
  42. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  43. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  44. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  45. package/dist/plugins/blog/client/index.d.cts +1 -1
  46. package/dist/plugins/blog/client/index.d.mts +1 -1
  47. package/dist/plugins/blog/client/index.d.ts +1 -1
  48. package/dist/plugins/blog/query-keys.d.cts +2 -2
  49. package/dist/plugins/blog/query-keys.d.mts +2 -2
  50. package/dist/plugins/blog/query-keys.d.ts +2 -2
  51. package/dist/plugins/cms/api/index.d.cts +66 -2
  52. package/dist/plugins/cms/api/index.d.mts +66 -2
  53. package/dist/plugins/cms/api/index.d.ts +66 -2
  54. package/dist/plugins/cms/client/hooks/index.cjs +4 -0
  55. package/dist/plugins/cms/client/hooks/index.d.cts +82 -3
  56. package/dist/plugins/cms/client/hooks/index.d.mts +82 -3
  57. package/dist/plugins/cms/client/hooks/index.d.ts +82 -3
  58. package/dist/plugins/cms/client/hooks/index.mjs +1 -1
  59. package/dist/plugins/cms/client/index.d.cts +2 -2
  60. package/dist/plugins/cms/client/index.d.mts +2 -2
  61. package/dist/plugins/cms/client/index.d.ts +2 -2
  62. package/dist/plugins/cms/query-keys.d.cts +1 -1
  63. package/dist/plugins/cms/query-keys.d.mts +1 -1
  64. package/dist/plugins/cms/query-keys.d.ts +1 -1
  65. package/dist/plugins/form-builder/client/components/index.d.cts +1 -1
  66. package/dist/plugins/form-builder/client/components/index.d.mts +1 -1
  67. package/dist/plugins/form-builder/client/components/index.d.ts +1 -1
  68. package/dist/plugins/form-builder/client/index.d.cts +2 -2
  69. package/dist/plugins/form-builder/client/index.d.mts +2 -2
  70. package/dist/plugins/form-builder/client/index.d.ts +2 -2
  71. package/dist/shared/{stack.AX5nZ6A3.d.ts → stack.Co034Fpm.d.cts} +0 -21
  72. package/dist/shared/{stack.AX5nZ6A3.d.cts → stack.Co034Fpm.d.mts} +0 -21
  73. package/dist/shared/{stack.AX5nZ6A3.d.mts → stack.Co034Fpm.d.ts} +0 -21
  74. package/dist/shared/{stack.BIh2AXaW.d.cts → stack.DGjhPqmF.d.cts} +0 -9
  75. package/dist/shared/{stack.BIh2AXaW.d.mts → stack.DGjhPqmF.d.mts} +0 -9
  76. package/dist/shared/{stack.BIh2AXaW.d.ts → stack.DGjhPqmF.d.ts} +0 -9
  77. package/dist/shared/{stack.L-UFwz2G.d.mts → stack.oGOteE6g.d.cts} +27 -5
  78. package/dist/shared/{stack.L-UFwz2G.d.cts → stack.oGOteE6g.d.mts} +27 -5
  79. package/dist/shared/{stack.L-UFwz2G.d.ts → stack.oGOteE6g.d.ts} +27 -5
  80. package/package.json +1 -1
  81. package/src/plugins/cms/api/plugin.ts +667 -21
  82. package/src/plugins/cms/client/components/forms/content-form.tsx +62 -20
  83. package/src/plugins/cms/client/components/forms/relation-field.tsx +299 -0
  84. package/src/plugins/cms/client/components/inverse-relations-panel.tsx +329 -0
  85. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +127 -1
  86. package/src/plugins/cms/client/hooks/cms-hooks.tsx +344 -0
  87. package/src/plugins/cms/db.ts +38 -0
  88. package/src/plugins/cms/types.ts +99 -10
  89. package/src/plugins/form-builder/client/components/forms/form-renderer.tsx +1 -1
  90. package/dist/packages/ui/src/components/auto-form/{utils.cjs → helpers.cjs} +0 -0
  91. package/dist/packages/ui/src/components/auto-form/{utils.mjs → helpers.mjs} +0 -0
  92. package/dist/shared/{stack.DLhzx1-D.d.mts → stack.CcI4sYJP.d.cts} +1 -1
  93. package/dist/shared/{stack.DLhzx1-D.d.cts → stack.CcI4sYJP.d.mts} +1 -1
  94. package/dist/shared/{stack.DLhzx1-D.d.ts → stack.CcI4sYJP.d.ts} +1 -1
@@ -1,10 +1,9 @@
1
1
  "use client";
2
2
  import { jsxs, jsx } from 'react/jsx-runtime';
3
- import { useState, useEffect, useMemo } from 'react';
3
+ import { useState, useRef, useEffect, useMemo } from 'react';
4
4
  import { z } from 'zod';
5
- import { toast } from 'sonner';
6
5
  import SteppedAutoForm from '../../../../../../../ui/src/components/auto-form/stepped-auto-form.mjs';
7
- import { buildFieldConfigFromJsonSchema as buildFieldConfigFromJsonSchema$1 } from '../../../../../../../ui/src/components/auto-form/utils.mjs';
6
+ import { buildFieldConfigFromJsonSchema as buildFieldConfigFromJsonSchema$1 } from '../../../../../../../ui/src/components/auto-form/helpers.mjs';
8
7
  import { formSchemaToZod } from '../../../../../../../ui/src/lib/schema-converter.mjs';
9
8
  import { Input } from '../../../../../../../ui/src/components/input.mjs';
10
9
  import { Label } from '../../../../../../../ui/src/components/label.mjs';
@@ -13,6 +12,7 @@ import { usePluginOverrides } from '@btst/stack/context';
13
12
  import { slugify } from '../../../utils.mjs';
14
13
  import { CMS_LOCALIZATION } from '../../localization/index.mjs';
15
14
  import { CMSFileUpload } from './file-upload.mjs';
15
+ import { RelationField } from './relation-field.mjs';
16
16
 
17
17
  function buildFieldConfigFromJsonSchema(jsonSchema, uploadImage, fieldComponents) {
18
18
  const baseConfig = buildFieldConfigFromJsonSchema$1(jsonSchema, fieldComponents);
@@ -36,6 +36,13 @@ function buildFieldConfigFromJsonSchema(jsonSchema, uploadImage, fieldComponents
36
36
  };
37
37
  }
38
38
  }
39
+ if (prop.fieldType === "relation" && prop.relation && !fieldComponents?.["relation"]) {
40
+ const relationConfig = prop.relation;
41
+ baseConfig[key] = {
42
+ ...baseConfig[key],
43
+ fieldType: (props) => /* @__PURE__ */ jsx(RelationField, { ...props, relation: relationConfig })
44
+ };
45
+ }
39
46
  }
40
47
  return baseConfig;
41
48
  }
@@ -73,9 +80,17 @@ function ContentForm({
73
80
  const [slugManuallyEdited, setSlugManuallyEdited] = useState(isEditing);
74
81
  const [isSubmitting, setIsSubmitting] = useState(false);
75
82
  const [formData, setFormData] = useState(initialData);
83
+ const [slugError, setSlugError] = useState(null);
84
+ const [submitError, setSubmitError] = useState(null);
85
+ const hasSyncedPrefillRef = useRef(false);
76
86
  useEffect(() => {
77
- if (isEditing && Object.keys(initialData).length > 0) {
87
+ const hasData = Object.keys(initialData).length > 0;
88
+ const shouldSync = hasData && (isEditing || !hasSyncedPrefillRef.current);
89
+ if (shouldSync) {
78
90
  setFormData(initialData);
91
+ if (!isEditing) {
92
+ hasSyncedPrefillRef.current = true;
93
+ }
79
94
  }
80
95
  }, [initialData, isEditing]);
81
96
  useEffect(() => {
@@ -115,19 +130,18 @@ function ContentForm({
115
130
  }
116
131
  };
117
132
  const handleSubmit = async (data) => {
133
+ setSlugError(null);
134
+ setSubmitError(null);
118
135
  if (!slug.trim()) {
119
- toast.error("Slug is required");
136
+ setSlugError("Slug is required");
120
137
  return;
121
138
  }
122
139
  setIsSubmitting(true);
123
140
  try {
124
141
  await onSubmit({ slug, data });
125
- toast.success(
126
- isEditing ? localization.CMS_TOAST_UPDATE_SUCCESS : localization.CMS_TOAST_CREATE_SUCCESS
127
- );
128
142
  } catch (error) {
129
143
  const message = error instanceof Error ? error.message : localization.CMS_TOAST_ERROR;
130
- toast.error(message);
144
+ setSubmitError(message);
131
145
  } finally {
132
146
  setIsSubmitting(false);
133
147
  }
@@ -145,6 +159,7 @@ function ContentForm({
145
159
  value: slug,
146
160
  onChange: (e) => {
147
161
  setSlug(e.target.value);
162
+ setSlugError(null);
148
163
  if (!isEditing) {
149
164
  setSlugManuallyEdited(true);
150
165
  }
@@ -153,8 +168,10 @@ function ContentForm({
153
168
  placeholder: slugSourceField ? `Auto-generated from ${slugSourceField}` : "Enter slug..."
154
169
  }
155
170
  ),
171
+ slugError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: slugError }),
156
172
  /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: localization.CMS_LABEL_SLUG_DESCRIPTION })
157
173
  ] }),
174
+ submitError && /* @__PURE__ */ jsx("div", { className: "rounded-md border border-destructive/50 bg-destructive/10 p-3", children: /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: submitError }) }),
158
175
  /* @__PURE__ */ jsx(
159
176
  SteppedAutoForm,
160
177
  {
@@ -0,0 +1,224 @@
1
+ "use client";
2
+ 'use strict';
3
+
4
+ const jsxRuntime = require('react/jsx-runtime');
5
+ const React = require('react');
6
+ const multiSelect = require('../../../../../../../ui/src/components/multi-select.cjs');
7
+ const button = require('../../../../../../../ui/src/components/button.cjs');
8
+ const lucideReact = require('lucide-react');
9
+ const dialog = require('../../../../../../../ui/src/components/dialog.cjs');
10
+ const input = require('../../../../../../../ui/src/components/input.cjs');
11
+ const label = require('../../../../../../../ui/src/components/label.cjs');
12
+ const textarea = require('../../../../../../../ui/src/components/textarea.cjs');
13
+ const cmsHooks = require('../../hooks/cms-hooks.cjs');
14
+
15
+ function RelationField({
16
+ field,
17
+ fieldConfigItem,
18
+ label: label$1,
19
+ isRequired,
20
+ relation
21
+ }) {
22
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = React.useState(false);
23
+ const [newItemName, setNewItemName] = React.useState("");
24
+ const [newItemDescription, setNewItemDescription] = React.useState("");
25
+ const [createError, setCreateError] = React.useState(null);
26
+ const isSingleSelect = relation.type === "belongsTo";
27
+ const { items: availableItems, isLoading } = cmsHooks.useContent(relation.targetType, {
28
+ limit: 100
29
+ // Load a good chunk for the dropdown
30
+ });
31
+ const createMutation = cmsHooks.useCreateContent(relation.targetType);
32
+ const normalizedValue = React.useMemo(() => {
33
+ if (!field.value) return [];
34
+ if (isSingleSelect) {
35
+ const singleValue = field.value;
36
+ if (singleValue && singleValue.id) {
37
+ return [{ id: singleValue.id }];
38
+ }
39
+ return [];
40
+ }
41
+ return field.value || [];
42
+ }, [field.value, isSingleSelect]);
43
+ const selectedOptions = normalizedValue.map((v) => {
44
+ const item = availableItems.find((item2) => item2.id === v.id);
45
+ if (item) {
46
+ const displayValue = item.parsedData?.[relation.displayField] || item.slug;
47
+ return {
48
+ value: item.id,
49
+ label: String(displayValue)
50
+ };
51
+ }
52
+ return { value: v.id, label: `ID: ${v.id.slice(0, 8)}...` };
53
+ }).filter(Boolean);
54
+ const options = availableItems.map((item) => {
55
+ const displayValue = item.parsedData?.[relation.displayField] || item.slug;
56
+ return {
57
+ value: item.id,
58
+ label: String(displayValue)
59
+ };
60
+ });
61
+ const handleChange = React.useCallback(
62
+ (newOptions) => {
63
+ if (isSingleSelect) {
64
+ if (newOptions.length > 0) {
65
+ field.onChange({ id: newOptions[0].value });
66
+ } else {
67
+ field.onChange(void 0);
68
+ }
69
+ } else {
70
+ const newValue = newOptions.map((opt) => ({ id: opt.value }));
71
+ field.onChange(newValue);
72
+ }
73
+ },
74
+ [field, isSingleSelect]
75
+ );
76
+ const handleCreateItem = async () => {
77
+ if (!newItemName.trim()) return;
78
+ setCreateError(null);
79
+ try {
80
+ const result = await createMutation.mutateAsync({
81
+ slug: newItemName.toLowerCase().replace(/\s+/g, "-"),
82
+ data: {
83
+ [relation.displayField]: newItemName,
84
+ description: newItemDescription || void 0
85
+ }
86
+ });
87
+ if (isSingleSelect) {
88
+ field.onChange({ id: result.id });
89
+ } else {
90
+ const newValue = [...normalizedValue, { id: result.id }];
91
+ field.onChange(newValue);
92
+ }
93
+ setNewItemName("");
94
+ setNewItemDescription("");
95
+ setIsCreateDialogOpen(false);
96
+ } catch (error) {
97
+ const message = error instanceof Error ? error.message : "Failed to create item. Please try again.";
98
+ setCreateError(message);
99
+ }
100
+ };
101
+ const handleRemove = React.useCallback(
102
+ (idToRemove) => {
103
+ if (isSingleSelect) {
104
+ field.onChange(void 0);
105
+ } else {
106
+ const newValue = normalizedValue.filter((v) => v.id !== idToRemove);
107
+ field.onChange(newValue);
108
+ }
109
+ },
110
+ [normalizedValue, field, isSingleSelect]
111
+ );
112
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
113
+ /* @__PURE__ */ jsxRuntime.jsxs(label.Label, { children: [
114
+ label$1,
115
+ isRequired && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-destructive ml-1", children: "*" })
116
+ ] }),
117
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex gap-2", children: [
118
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsxRuntime.jsx(
119
+ multiSelect,
120
+ {
121
+ value: selectedOptions,
122
+ onChange: handleChange,
123
+ options,
124
+ placeholder: isLoading ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
125
+ disabled: isLoading,
126
+ hidePlaceholderWhenSelected: true,
127
+ emptyIndicator: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-center text-sm text-muted-foreground py-4", children: [
128
+ "No ",
129
+ relation.targetType,
130
+ " items found"
131
+ ] }),
132
+ maxSelected: isSingleSelect ? 1 : void 0,
133
+ className: "min-h-10"
134
+ }
135
+ ) }),
136
+ relation.creatable && /* @__PURE__ */ jsxRuntime.jsxs(
137
+ dialog.Dialog,
138
+ {
139
+ open: isCreateDialogOpen,
140
+ onOpenChange: setIsCreateDialogOpen,
141
+ children: [
142
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.DialogTrigger, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsx(button.Button, { type: "button", variant: "outline", size: "icon", children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4" }) }) }),
143
+ /* @__PURE__ */ jsxRuntime.jsxs(dialog.DialogContent, { children: [
144
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.DialogHeader, { children: /* @__PURE__ */ jsxRuntime.jsxs(dialog.DialogTitle, { children: [
145
+ "Create New ",
146
+ relation.targetType
147
+ ] }) }),
148
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4 py-4", children: [
149
+ createError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: createError }),
150
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
151
+ /* @__PURE__ */ jsxRuntime.jsx(label.Label, { htmlFor: "newItemName", children: relation.displayField.charAt(0).toUpperCase() + relation.displayField.slice(1) }),
152
+ /* @__PURE__ */ jsxRuntime.jsx(
153
+ input.Input,
154
+ {
155
+ id: "newItemName",
156
+ value: newItemName,
157
+ onChange: (e) => setNewItemName(e.target.value),
158
+ placeholder: `Enter ${relation.displayField}...`
159
+ }
160
+ )
161
+ ] }),
162
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-2", children: [
163
+ /* @__PURE__ */ jsxRuntime.jsx(label.Label, { htmlFor: "newItemDescription", children: "Description (optional)" }),
164
+ /* @__PURE__ */ jsxRuntime.jsx(
165
+ textarea.Textarea,
166
+ {
167
+ id: "newItemDescription",
168
+ value: newItemDescription,
169
+ onChange: (e) => setNewItemDescription(e.target.value),
170
+ placeholder: "Enter description...",
171
+ rows: 3
172
+ }
173
+ )
174
+ ] }),
175
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex justify-end gap-2", children: [
176
+ /* @__PURE__ */ jsxRuntime.jsx(
177
+ button.Button,
178
+ {
179
+ type: "button",
180
+ variant: "outline",
181
+ onClick: () => setIsCreateDialogOpen(false),
182
+ children: "Cancel"
183
+ }
184
+ ),
185
+ /* @__PURE__ */ jsxRuntime.jsx(
186
+ button.Button,
187
+ {
188
+ type: "button",
189
+ onClick: handleCreateItem,
190
+ disabled: !newItemName.trim() || createMutation.isPending,
191
+ children: createMutation.isPending ? "Creating..." : "Create"
192
+ }
193
+ )
194
+ ] })
195
+ ] })
196
+ ] })
197
+ ]
198
+ }
199
+ )
200
+ ] }),
201
+ selectedOptions.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-1 mt-2", children: selectedOptions.map((opt) => /* @__PURE__ */ jsxRuntime.jsxs(
202
+ "div",
203
+ {
204
+ className: "inline-flex items-center gap-1 px-2 py-1 text-xs rounded-md bg-secondary text-secondary-foreground",
205
+ children: [
206
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: opt.label }),
207
+ /* @__PURE__ */ jsxRuntime.jsx(
208
+ "button",
209
+ {
210
+ type: "button",
211
+ onClick: () => handleRemove(opt.value),
212
+ className: "hover:text-destructive",
213
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3 w-3" })
214
+ }
215
+ )
216
+ ]
217
+ },
218
+ opt.value
219
+ )) }),
220
+ fieldConfigItem?.description && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-muted-foreground", children: fieldConfigItem.description })
221
+ ] });
222
+ }
223
+
224
+ exports.RelationField = RelationField;
@@ -0,0 +1,222 @@
1
+ "use client";
2
+ import { jsxs, jsx } from 'react/jsx-runtime';
3
+ import { useState, useMemo, useCallback } from 'react';
4
+ import MultipleSelector from '../../../../../../../ui/src/components/multi-select.mjs';
5
+ import { Button } from '../../../../../../../ui/src/components/button.mjs';
6
+ import { Plus, X } from 'lucide-react';
7
+ import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle } from '../../../../../../../ui/src/components/dialog.mjs';
8
+ import { Input } from '../../../../../../../ui/src/components/input.mjs';
9
+ import { Label } from '../../../../../../../ui/src/components/label.mjs';
10
+ import { Textarea } from '../../../../../../../ui/src/components/textarea.mjs';
11
+ import { useContent, useCreateContent } from '../../hooks/cms-hooks.mjs';
12
+
13
+ function RelationField({
14
+ field,
15
+ fieldConfigItem,
16
+ label,
17
+ isRequired,
18
+ relation
19
+ }) {
20
+ const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
21
+ const [newItemName, setNewItemName] = useState("");
22
+ const [newItemDescription, setNewItemDescription] = useState("");
23
+ const [createError, setCreateError] = useState(null);
24
+ const isSingleSelect = relation.type === "belongsTo";
25
+ const { items: availableItems, isLoading } = useContent(relation.targetType, {
26
+ limit: 100
27
+ // Load a good chunk for the dropdown
28
+ });
29
+ const createMutation = useCreateContent(relation.targetType);
30
+ const normalizedValue = useMemo(() => {
31
+ if (!field.value) return [];
32
+ if (isSingleSelect) {
33
+ const singleValue = field.value;
34
+ if (singleValue && singleValue.id) {
35
+ return [{ id: singleValue.id }];
36
+ }
37
+ return [];
38
+ }
39
+ return field.value || [];
40
+ }, [field.value, isSingleSelect]);
41
+ const selectedOptions = normalizedValue.map((v) => {
42
+ const item = availableItems.find((item2) => item2.id === v.id);
43
+ if (item) {
44
+ const displayValue = item.parsedData?.[relation.displayField] || item.slug;
45
+ return {
46
+ value: item.id,
47
+ label: String(displayValue)
48
+ };
49
+ }
50
+ return { value: v.id, label: `ID: ${v.id.slice(0, 8)}...` };
51
+ }).filter(Boolean);
52
+ const options = availableItems.map((item) => {
53
+ const displayValue = item.parsedData?.[relation.displayField] || item.slug;
54
+ return {
55
+ value: item.id,
56
+ label: String(displayValue)
57
+ };
58
+ });
59
+ const handleChange = useCallback(
60
+ (newOptions) => {
61
+ if (isSingleSelect) {
62
+ if (newOptions.length > 0) {
63
+ field.onChange({ id: newOptions[0].value });
64
+ } else {
65
+ field.onChange(void 0);
66
+ }
67
+ } else {
68
+ const newValue = newOptions.map((opt) => ({ id: opt.value }));
69
+ field.onChange(newValue);
70
+ }
71
+ },
72
+ [field, isSingleSelect]
73
+ );
74
+ const handleCreateItem = async () => {
75
+ if (!newItemName.trim()) return;
76
+ setCreateError(null);
77
+ try {
78
+ const result = await createMutation.mutateAsync({
79
+ slug: newItemName.toLowerCase().replace(/\s+/g, "-"),
80
+ data: {
81
+ [relation.displayField]: newItemName,
82
+ description: newItemDescription || void 0
83
+ }
84
+ });
85
+ if (isSingleSelect) {
86
+ field.onChange({ id: result.id });
87
+ } else {
88
+ const newValue = [...normalizedValue, { id: result.id }];
89
+ field.onChange(newValue);
90
+ }
91
+ setNewItemName("");
92
+ setNewItemDescription("");
93
+ setIsCreateDialogOpen(false);
94
+ } catch (error) {
95
+ const message = error instanceof Error ? error.message : "Failed to create item. Please try again.";
96
+ setCreateError(message);
97
+ }
98
+ };
99
+ const handleRemove = useCallback(
100
+ (idToRemove) => {
101
+ if (isSingleSelect) {
102
+ field.onChange(void 0);
103
+ } else {
104
+ const newValue = normalizedValue.filter((v) => v.id !== idToRemove);
105
+ field.onChange(newValue);
106
+ }
107
+ },
108
+ [normalizedValue, field, isSingleSelect]
109
+ );
110
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
111
+ /* @__PURE__ */ jsxs(Label, { children: [
112
+ label,
113
+ isRequired && /* @__PURE__ */ jsx("span", { className: "text-destructive ml-1", children: "*" })
114
+ ] }),
115
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
116
+ /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
117
+ MultipleSelector,
118
+ {
119
+ value: selectedOptions,
120
+ onChange: handleChange,
121
+ options,
122
+ placeholder: isLoading ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
123
+ disabled: isLoading,
124
+ hidePlaceholderWhenSelected: true,
125
+ emptyIndicator: /* @__PURE__ */ jsxs("p", { className: "text-center text-sm text-muted-foreground py-4", children: [
126
+ "No ",
127
+ relation.targetType,
128
+ " items found"
129
+ ] }),
130
+ maxSelected: isSingleSelect ? 1 : void 0,
131
+ className: "min-h-10"
132
+ }
133
+ ) }),
134
+ relation.creatable && /* @__PURE__ */ jsxs(
135
+ Dialog,
136
+ {
137
+ open: isCreateDialogOpen,
138
+ onOpenChange: setIsCreateDialogOpen,
139
+ children: [
140
+ /* @__PURE__ */ jsx(DialogTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { type: "button", variant: "outline", size: "icon", children: /* @__PURE__ */ jsx(Plus, { className: "h-4 w-4" }) }) }),
141
+ /* @__PURE__ */ jsxs(DialogContent, { children: [
142
+ /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsxs(DialogTitle, { children: [
143
+ "Create New ",
144
+ relation.targetType
145
+ ] }) }),
146
+ /* @__PURE__ */ jsxs("div", { className: "space-y-4 py-4", children: [
147
+ createError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: createError }),
148
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
149
+ /* @__PURE__ */ jsx(Label, { htmlFor: "newItemName", children: relation.displayField.charAt(0).toUpperCase() + relation.displayField.slice(1) }),
150
+ /* @__PURE__ */ jsx(
151
+ Input,
152
+ {
153
+ id: "newItemName",
154
+ value: newItemName,
155
+ onChange: (e) => setNewItemName(e.target.value),
156
+ placeholder: `Enter ${relation.displayField}...`
157
+ }
158
+ )
159
+ ] }),
160
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
161
+ /* @__PURE__ */ jsx(Label, { htmlFor: "newItemDescription", children: "Description (optional)" }),
162
+ /* @__PURE__ */ jsx(
163
+ Textarea,
164
+ {
165
+ id: "newItemDescription",
166
+ value: newItemDescription,
167
+ onChange: (e) => setNewItemDescription(e.target.value),
168
+ placeholder: "Enter description...",
169
+ rows: 3
170
+ }
171
+ )
172
+ ] }),
173
+ /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-2", children: [
174
+ /* @__PURE__ */ jsx(
175
+ Button,
176
+ {
177
+ type: "button",
178
+ variant: "outline",
179
+ onClick: () => setIsCreateDialogOpen(false),
180
+ children: "Cancel"
181
+ }
182
+ ),
183
+ /* @__PURE__ */ jsx(
184
+ Button,
185
+ {
186
+ type: "button",
187
+ onClick: handleCreateItem,
188
+ disabled: !newItemName.trim() || createMutation.isPending,
189
+ children: createMutation.isPending ? "Creating..." : "Create"
190
+ }
191
+ )
192
+ ] })
193
+ ] })
194
+ ] })
195
+ ]
196
+ }
197
+ )
198
+ ] }),
199
+ selectedOptions.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1 mt-2", children: selectedOptions.map((opt) => /* @__PURE__ */ jsxs(
200
+ "div",
201
+ {
202
+ className: "inline-flex items-center gap-1 px-2 py-1 text-xs rounded-md bg-secondary text-secondary-foreground",
203
+ children: [
204
+ /* @__PURE__ */ jsx("span", { children: opt.label }),
205
+ /* @__PURE__ */ jsx(
206
+ "button",
207
+ {
208
+ type: "button",
209
+ onClick: () => handleRemove(opt.value),
210
+ className: "hover:text-destructive",
211
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
212
+ }
213
+ )
214
+ ]
215
+ },
216
+ opt.value
217
+ )) }),
218
+ fieldConfigItem?.description && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: fieldConfigItem.description })
219
+ ] });
220
+ }
221
+
222
+ export { RelationField };