@btst/stack 1.7.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/dist/api/index.d.cts +2 -2
  2. package/dist/api/index.d.mts +2 -2
  3. package/dist/api/index.d.ts +2 -2
  4. package/dist/client/index.cjs +6 -2
  5. package/dist/client/index.d.cts +2 -1
  6. package/dist/client/index.d.mts +2 -1
  7. package/dist/client/index.d.ts +2 -1
  8. package/dist/client/index.mjs +6 -2
  9. package/dist/index.d.cts +1 -1
  10. package/dist/index.d.mts +1 -1
  11. package/dist/index.d.ts +1 -1
  12. package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +445 -16
  13. package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +445 -16
  14. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -7
  15. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +25 -8
  16. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.cjs +224 -0
  17. package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.mjs +222 -0
  18. package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.cjs +243 -0
  19. package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.mjs +241 -0
  20. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +56 -2
  21. package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +56 -2
  22. package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.cjs +190 -0
  23. package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.mjs +187 -1
  24. package/dist/packages/better-stack/src/plugins/cms/db.cjs +38 -0
  25. package/dist/packages/better-stack/src/plugins/cms/db.mjs +38 -0
  26. package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.cjs +43 -0
  27. package/dist/packages/better-stack/src/plugins/route-docs/client/components/loading/docs-skeleton.mjs +41 -0
  28. package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.cjs +794 -0
  29. package/dist/packages/better-stack/src/plugins/route-docs/client/components/pages/docs-page.mjs +788 -0
  30. package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.cjs +111 -0
  31. package/dist/packages/better-stack/src/plugins/route-docs/client/plugin.mjs +106 -0
  32. package/dist/packages/better-stack/src/plugins/route-docs/generator.cjs +244 -0
  33. package/dist/packages/better-stack/src/plugins/route-docs/generator.mjs +227 -0
  34. package/dist/packages/ui/src/components/auto-form/fields/object.cjs +81 -1
  35. package/dist/packages/ui/src/components/auto-form/fields/object.mjs +81 -1
  36. package/dist/packages/ui/src/components/dialog.cjs +6 -0
  37. package/dist/packages/ui/src/components/dialog.mjs +6 -1
  38. package/dist/packages/ui/src/components/sheet.cjs +25 -0
  39. package/dist/packages/ui/src/components/sheet.mjs +24 -1
  40. package/dist/plugins/api/index.d.cts +2 -2
  41. package/dist/plugins/api/index.d.mts +2 -2
  42. package/dist/plugins/api/index.d.ts +2 -2
  43. package/dist/plugins/blog/api/index.d.cts +1 -1
  44. package/dist/plugins/blog/api/index.d.mts +1 -1
  45. package/dist/plugins/blog/api/index.d.ts +1 -1
  46. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  47. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  48. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  49. package/dist/plugins/blog/client/index.d.cts +1 -1
  50. package/dist/plugins/blog/client/index.d.mts +1 -1
  51. package/dist/plugins/blog/client/index.d.ts +1 -1
  52. package/dist/plugins/blog/query-keys.d.cts +2 -2
  53. package/dist/plugins/blog/query-keys.d.mts +2 -2
  54. package/dist/plugins/blog/query-keys.d.ts +2 -2
  55. package/dist/plugins/client/index.d.cts +2 -2
  56. package/dist/plugins/client/index.d.mts +2 -2
  57. package/dist/plugins/client/index.d.ts +2 -2
  58. package/dist/plugins/cms/api/index.d.cts +67 -3
  59. package/dist/plugins/cms/api/index.d.mts +67 -3
  60. package/dist/plugins/cms/api/index.d.ts +67 -3
  61. package/dist/plugins/cms/client/hooks/index.cjs +4 -0
  62. package/dist/plugins/cms/client/hooks/index.d.cts +82 -3
  63. package/dist/plugins/cms/client/hooks/index.d.mts +82 -3
  64. package/dist/plugins/cms/client/hooks/index.d.ts +82 -3
  65. package/dist/plugins/cms/client/hooks/index.mjs +1 -1
  66. package/dist/plugins/cms/query-keys.d.cts +1 -1
  67. package/dist/plugins/cms/query-keys.d.mts +1 -1
  68. package/dist/plugins/cms/query-keys.d.ts +1 -1
  69. package/dist/plugins/form-builder/api/index.d.cts +1 -1
  70. package/dist/plugins/form-builder/api/index.d.mts +1 -1
  71. package/dist/plugins/form-builder/api/index.d.ts +1 -1
  72. package/dist/plugins/open-api/api/index.d.cts +1 -1
  73. package/dist/plugins/open-api/api/index.d.mts +1 -1
  74. package/dist/plugins/open-api/api/index.d.ts +1 -1
  75. package/dist/plugins/route-docs/client/index.cjs +10 -0
  76. package/dist/plugins/route-docs/client/index.d.cts +126 -0
  77. package/dist/plugins/route-docs/client/index.d.mts +126 -0
  78. package/dist/plugins/route-docs/client/index.d.ts +126 -0
  79. package/dist/plugins/route-docs/client/index.mjs +1 -0
  80. package/dist/plugins/route-docs/client.css +3 -0
  81. package/dist/plugins/route-docs/style.css +19 -0
  82. package/dist/shared/{stack.L-UFwz2G.d.mts → stack.oGOteE6g.d.cts} +27 -5
  83. package/dist/shared/{stack.L-UFwz2G.d.ts → stack.oGOteE6g.d.mts} +27 -5
  84. package/dist/shared/{stack.L-UFwz2G.d.cts → stack.oGOteE6g.d.ts} +27 -5
  85. package/dist/shared/{stack.CSce37mX.d.cts → stack.u9iYV6vt.d.cts} +14 -2
  86. package/dist/shared/{stack.CSce37mX.d.mts → stack.u9iYV6vt.d.mts} +14 -2
  87. package/dist/shared/{stack.CSce37mX.d.ts → stack.u9iYV6vt.d.ts} +14 -2
  88. package/package.json +15 -1
  89. package/src/client/index.ts +11 -4
  90. package/src/plugins/cms/api/plugin.ts +667 -21
  91. package/src/plugins/cms/client/components/forms/content-form.tsx +60 -18
  92. package/src/plugins/cms/client/components/forms/relation-field.tsx +299 -0
  93. package/src/plugins/cms/client/components/inverse-relations-panel.tsx +329 -0
  94. package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +127 -1
  95. package/src/plugins/cms/client/hooks/cms-hooks.tsx +344 -0
  96. package/src/plugins/cms/db.ts +38 -0
  97. package/src/plugins/cms/types.ts +99 -10
  98. package/src/plugins/route-docs/client/components/loading/docs-skeleton.tsx +82 -0
  99. package/src/plugins/route-docs/client/components/loading/index.tsx +1 -0
  100. package/src/plugins/route-docs/client/components/pages/docs-page.tsx +1240 -0
  101. package/src/plugins/route-docs/client/index.ts +7 -0
  102. package/src/plugins/route-docs/client/plugin.tsx +187 -0
  103. package/src/plugins/route-docs/client.css +3 -0
  104. package/src/plugins/route-docs/generator.ts +385 -0
  105. package/src/plugins/route-docs/index.ts +12 -0
  106. package/src/plugins/route-docs/style.css +19 -0
  107. package/src/types.ts +19 -1
  108. package/dist/shared/{stack.CcI4sYJP.d.mts → stack.DLhzx1-D.d.cts} +1 -1
  109. package/dist/shared/{stack.CcI4sYJP.d.ts → stack.DLhzx1-D.d.mts} +1 -1
  110. package/dist/shared/{stack.CcI4sYJP.d.cts → stack.DLhzx1-D.d.ts} +1 -1
@@ -1,8 +1,7 @@
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
6
  import { buildFieldConfigFromJsonSchema as buildFieldConfigFromJsonSchema$1 } from '../../../../../../../ui/src/components/auto-form/utils.mjs';
8
7
  import { formSchemaToZod } from '../../../../../../../ui/src/lib/schema-converter.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 };