@btst/stack 1.8.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.
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.cjs +445 -16
- package/dist/packages/better-stack/src/plugins/cms/api/plugin.mjs +445 -16
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.cjs +24 -7
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/content-form.mjs +25 -8
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.cjs +224 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/forms/relation-field.mjs +222 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.cjs +243 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.mjs +241 -0
- package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.cjs +56 -2
- package/dist/packages/better-stack/src/plugins/cms/client/components/pages/content-editor-page.internal.mjs +56 -2
- package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.cjs +190 -0
- package/dist/packages/better-stack/src/plugins/cms/client/hooks/cms-hooks.mjs +187 -1
- package/dist/packages/better-stack/src/plugins/cms/db.cjs +38 -0
- package/dist/packages/better-stack/src/plugins/cms/db.mjs +38 -0
- package/dist/packages/ui/src/components/auto-form/fields/object.cjs +81 -1
- package/dist/packages/ui/src/components/auto-form/fields/object.mjs +81 -1
- package/dist/packages/ui/src/components/dialog.cjs +6 -0
- package/dist/packages/ui/src/components/dialog.mjs +6 -1
- package/dist/plugins/cms/api/index.d.cts +67 -3
- package/dist/plugins/cms/api/index.d.mts +67 -3
- package/dist/plugins/cms/api/index.d.ts +67 -3
- package/dist/plugins/cms/client/hooks/index.cjs +4 -0
- package/dist/plugins/cms/client/hooks/index.d.cts +82 -3
- package/dist/plugins/cms/client/hooks/index.d.mts +82 -3
- package/dist/plugins/cms/client/hooks/index.d.ts +82 -3
- package/dist/plugins/cms/client/hooks/index.mjs +1 -1
- package/dist/plugins/cms/query-keys.d.cts +1 -1
- package/dist/plugins/cms/query-keys.d.mts +1 -1
- package/dist/plugins/cms/query-keys.d.ts +1 -1
- package/dist/plugins/form-builder/api/index.d.cts +1 -1
- package/dist/plugins/form-builder/api/index.d.mts +1 -1
- package/dist/plugins/form-builder/api/index.d.ts +1 -1
- package/dist/shared/{stack.L-UFwz2G.d.cts → stack.oGOteE6g.d.cts} +27 -5
- package/dist/shared/{stack.L-UFwz2G.d.mts → stack.oGOteE6g.d.mts} +27 -5
- package/dist/shared/{stack.L-UFwz2G.d.ts → stack.oGOteE6g.d.ts} +27 -5
- package/package.json +1 -1
- package/src/plugins/cms/api/plugin.ts +667 -21
- package/src/plugins/cms/client/components/forms/content-form.tsx +60 -18
- package/src/plugins/cms/client/components/forms/relation-field.tsx +299 -0
- package/src/plugins/cms/client/components/inverse-relations-panel.tsx +329 -0
- package/src/plugins/cms/client/components/pages/content-editor-page.internal.tsx +127 -1
- package/src/plugins/cms/client/hooks/cms-hooks.tsx +344 -0
- package/src/plugins/cms/db.ts +38 -0
- package/src/plugins/cms/types.ts +99 -10
package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.cjs
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
const React = require('react');
|
|
6
|
+
const reactQuery = require('@tanstack/react-query');
|
|
7
|
+
const lucideReact = require('lucide-react');
|
|
8
|
+
const button = require('../../../../../../ui/src/components/button.cjs');
|
|
9
|
+
const card = require('../../../../../../ui/src/components/card.cjs');
|
|
10
|
+
const client = require('@btst/stack/plugins/client');
|
|
11
|
+
const context = require('@btst/stack/context');
|
|
12
|
+
const alertDialog = require('../../../../../../ui/src/components/alert-dialog.cjs');
|
|
13
|
+
const cmsHooks = require('../hooks/cms-hooks.cjs');
|
|
14
|
+
|
|
15
|
+
function InverseRelationsPanel({
|
|
16
|
+
contentTypeSlug,
|
|
17
|
+
itemId
|
|
18
|
+
}) {
|
|
19
|
+
const { apiBaseURL, apiBasePath, headers, navigate, Link } = context.usePluginOverrides("cms");
|
|
20
|
+
const basePath = context.useBasePath();
|
|
21
|
+
const client$1 = client.createApiClient({
|
|
22
|
+
baseURL: apiBaseURL,
|
|
23
|
+
basePath: apiBasePath
|
|
24
|
+
});
|
|
25
|
+
const { data: inverseRelationsData, isLoading } = reactQuery.useQuery({
|
|
26
|
+
queryKey: ["cmsInverseRelations", contentTypeSlug, itemId],
|
|
27
|
+
queryFn: async () => {
|
|
28
|
+
const response = await client$1("/content-types/:slug/inverse-relations", {
|
|
29
|
+
method: "GET",
|
|
30
|
+
params: { slug: contentTypeSlug },
|
|
31
|
+
query: { itemId },
|
|
32
|
+
headers
|
|
33
|
+
});
|
|
34
|
+
return response.data?.inverseRelations ?? [];
|
|
35
|
+
},
|
|
36
|
+
staleTime: 1e3 * 60 * 5
|
|
37
|
+
});
|
|
38
|
+
if (isLoading) {
|
|
39
|
+
return /* @__PURE__ */ jsxRuntime.jsx(card.Card, { className: "animate-pulse", children: /* @__PURE__ */ jsxRuntime.jsx(card.CardHeader, { children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: "h-5 w-32 bg-muted rounded" }) }) });
|
|
40
|
+
}
|
|
41
|
+
const inverseRelations = inverseRelationsData ?? [];
|
|
42
|
+
if (inverseRelations.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "space-y-4", children: [
|
|
46
|
+
/* @__PURE__ */ jsxRuntime.jsx("h3", { className: "text-lg font-semibold", children: "Related Items" }),
|
|
47
|
+
inverseRelations.map((relation) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
48
|
+
InverseRelationSection,
|
|
49
|
+
{
|
|
50
|
+
relation,
|
|
51
|
+
contentTypeSlug,
|
|
52
|
+
itemId,
|
|
53
|
+
basePath,
|
|
54
|
+
navigate,
|
|
55
|
+
Link,
|
|
56
|
+
client: client$1,
|
|
57
|
+
headers
|
|
58
|
+
},
|
|
59
|
+
`${relation.sourceType}-${relation.fieldName}`
|
|
60
|
+
))
|
|
61
|
+
] });
|
|
62
|
+
}
|
|
63
|
+
function InverseRelationSection({
|
|
64
|
+
relation,
|
|
65
|
+
contentTypeSlug,
|
|
66
|
+
itemId,
|
|
67
|
+
basePath,
|
|
68
|
+
navigate,
|
|
69
|
+
Link,
|
|
70
|
+
client,
|
|
71
|
+
headers
|
|
72
|
+
}) {
|
|
73
|
+
const [isExpanded, setIsExpanded] = React.useState(true);
|
|
74
|
+
const [deleteItemId, setDeleteItemId] = React.useState(null);
|
|
75
|
+
const [deleteError, setDeleteError] = React.useState(null);
|
|
76
|
+
const deleteContent = cmsHooks.useDeleteContent(relation.sourceType);
|
|
77
|
+
const { data: itemsData, refetch } = reactQuery.useQuery({
|
|
78
|
+
queryKey: [
|
|
79
|
+
"cmsInverseRelationItems",
|
|
80
|
+
contentTypeSlug,
|
|
81
|
+
relation.sourceType,
|
|
82
|
+
itemId,
|
|
83
|
+
relation.fieldName
|
|
84
|
+
],
|
|
85
|
+
queryFn: async () => {
|
|
86
|
+
const response = await client(
|
|
87
|
+
"/content-types/:slug/inverse-relations/:sourceType",
|
|
88
|
+
{
|
|
89
|
+
method: "GET",
|
|
90
|
+
params: { slug: contentTypeSlug, sourceType: relation.sourceType },
|
|
91
|
+
query: { itemId, fieldName: relation.fieldName },
|
|
92
|
+
headers
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
return response.data ?? { items: [], total: 0 };
|
|
96
|
+
},
|
|
97
|
+
staleTime: 1e3 * 60 * 5,
|
|
98
|
+
enabled: isExpanded
|
|
99
|
+
});
|
|
100
|
+
const items = itemsData?.items ?? [];
|
|
101
|
+
const total = itemsData?.total ?? relation.count;
|
|
102
|
+
const handleDelete = async () => {
|
|
103
|
+
if (deleteItemId) {
|
|
104
|
+
setDeleteError(null);
|
|
105
|
+
try {
|
|
106
|
+
await deleteContent.mutateAsync(deleteItemId);
|
|
107
|
+
setDeleteItemId(null);
|
|
108
|
+
refetch();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const message = error instanceof Error ? error.message : "Failed to delete item. Please try again.";
|
|
111
|
+
setDeleteError(message);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
const handleAddNew = () => {
|
|
116
|
+
const createUrl = `${basePath}/cms/${relation.sourceType}/new?prefill_${relation.fieldName}=${itemId}`;
|
|
117
|
+
navigate(createUrl);
|
|
118
|
+
};
|
|
119
|
+
const LinkComponent = Link ?? "a";
|
|
120
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(card.Card, { children: [
|
|
121
|
+
/* @__PURE__ */ jsxRuntime.jsx(card.CardHeader, { className: "py-3", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
122
|
+
"button",
|
|
123
|
+
{
|
|
124
|
+
type: "button",
|
|
125
|
+
onClick: () => setIsExpanded(!isExpanded),
|
|
126
|
+
className: "flex items-center justify-between w-full text-left",
|
|
127
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(card.CardTitle, { className: "text-base flex items-center gap-2", children: [
|
|
128
|
+
isExpanded ? /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ChevronRight, { className: "h-4 w-4" }),
|
|
129
|
+
relation.sourceTypeName,
|
|
130
|
+
" (",
|
|
131
|
+
total,
|
|
132
|
+
")"
|
|
133
|
+
] })
|
|
134
|
+
}
|
|
135
|
+
) }),
|
|
136
|
+
isExpanded && /* @__PURE__ */ jsxRuntime.jsxs(card.CardContent, { className: "pt-0", children: [
|
|
137
|
+
items.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-sm text-muted-foreground py-2", children: [
|
|
138
|
+
"No ",
|
|
139
|
+
relation.sourceTypeName.toLowerCase(),
|
|
140
|
+
" items yet."
|
|
141
|
+
] }) : /* @__PURE__ */ jsxRuntime.jsx("ul", { className: "space-y-2", children: items.map((item) => {
|
|
142
|
+
const displayValue = getDisplayValue(item);
|
|
143
|
+
const editUrl = `${basePath}/cms/${relation.sourceType}/${item.id}`;
|
|
144
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
145
|
+
"li",
|
|
146
|
+
{
|
|
147
|
+
className: "flex items-center justify-between py-2 px-3 rounded-md bg-muted/50 hover:bg-muted transition-colors",
|
|
148
|
+
children: [
|
|
149
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
150
|
+
LinkComponent,
|
|
151
|
+
{
|
|
152
|
+
href: editUrl,
|
|
153
|
+
className: "flex-1 text-sm hover:underline flex items-center gap-2",
|
|
154
|
+
children: [
|
|
155
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: displayValue }),
|
|
156
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.ExternalLink, { className: "h-3 w-3 opacity-50" })
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
161
|
+
button.Button,
|
|
162
|
+
{
|
|
163
|
+
variant: "ghost",
|
|
164
|
+
size: "icon",
|
|
165
|
+
className: "h-7 w-7 text-muted-foreground hover:text-destructive",
|
|
166
|
+
onClick: () => setDeleteItemId(item.id),
|
|
167
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Trash2, { className: "h-3.5 w-3.5" })
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
]
|
|
171
|
+
},
|
|
172
|
+
item.id
|
|
173
|
+
);
|
|
174
|
+
}) }),
|
|
175
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-3 pt-3 border-t", children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
176
|
+
button.Button,
|
|
177
|
+
{
|
|
178
|
+
variant: "outline",
|
|
179
|
+
size: "sm",
|
|
180
|
+
onClick: handleAddNew,
|
|
181
|
+
className: "w-full",
|
|
182
|
+
children: [
|
|
183
|
+
/* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "h-4 w-4 mr-2" }),
|
|
184
|
+
"Add ",
|
|
185
|
+
relation.sourceTypeName
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
) })
|
|
189
|
+
] }),
|
|
190
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
191
|
+
alertDialog.AlertDialog,
|
|
192
|
+
{
|
|
193
|
+
open: !!deleteItemId,
|
|
194
|
+
onOpenChange: (open) => {
|
|
195
|
+
if (!open) {
|
|
196
|
+
setDeleteItemId(null);
|
|
197
|
+
setDeleteError(null);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
children: /* @__PURE__ */ jsxRuntime.jsxs(alertDialog.AlertDialogContent, { children: [
|
|
201
|
+
/* @__PURE__ */ jsxRuntime.jsxs(alertDialog.AlertDialogHeader, { children: [
|
|
202
|
+
/* @__PURE__ */ jsxRuntime.jsxs(alertDialog.AlertDialogTitle, { children: [
|
|
203
|
+
"Delete ",
|
|
204
|
+
relation.sourceTypeName,
|
|
205
|
+
"?"
|
|
206
|
+
] }),
|
|
207
|
+
/* @__PURE__ */ jsxRuntime.jsxs(alertDialog.AlertDialogDescription, { children: [
|
|
208
|
+
"This action cannot be undone. This will permanently delete this",
|
|
209
|
+
" ",
|
|
210
|
+
relation.sourceTypeName.toLowerCase(),
|
|
211
|
+
"."
|
|
212
|
+
] })
|
|
213
|
+
] }),
|
|
214
|
+
deleteError && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-sm text-destructive", children: deleteError }),
|
|
215
|
+
/* @__PURE__ */ jsxRuntime.jsxs(alertDialog.AlertDialogFooter, { children: [
|
|
216
|
+
/* @__PURE__ */ jsxRuntime.jsx(alertDialog.AlertDialogCancel, { children: "Cancel" }),
|
|
217
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
218
|
+
alertDialog.AlertDialogAction,
|
|
219
|
+
{
|
|
220
|
+
onClick: handleDelete,
|
|
221
|
+
className: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
222
|
+
children: "Delete"
|
|
223
|
+
}
|
|
224
|
+
)
|
|
225
|
+
] })
|
|
226
|
+
] })
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
] });
|
|
230
|
+
}
|
|
231
|
+
function getDisplayValue(item) {
|
|
232
|
+
const data = item.parsedData;
|
|
233
|
+
const displayFields = ["name", "title", "label", "content", "author", "slug"];
|
|
234
|
+
for (const field of displayFields) {
|
|
235
|
+
if (typeof data[field] === "string" && data[field]) {
|
|
236
|
+
const value = data[field];
|
|
237
|
+
return value.length > 50 ? `${value.slice(0, 50)}...` : value;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return item.slug;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
exports.InverseRelationsPanel = InverseRelationsPanel;
|
package/dist/packages/better-stack/src/plugins/cms/client/components/inverse-relations-panel.mjs
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useQuery } from '@tanstack/react-query';
|
|
5
|
+
import { ChevronDown, ChevronRight, ExternalLink, Trash2, Plus } from 'lucide-react';
|
|
6
|
+
import { Button } from '../../../../../../ui/src/components/button.mjs';
|
|
7
|
+
import { Card, CardHeader, CardTitle, CardContent } from '../../../../../../ui/src/components/card.mjs';
|
|
8
|
+
import { createApiClient } from '@btst/stack/plugins/client';
|
|
9
|
+
import { usePluginOverrides, useBasePath } from '@btst/stack/context';
|
|
10
|
+
import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from '../../../../../../ui/src/components/alert-dialog.mjs';
|
|
11
|
+
import { useDeleteContent } from '../hooks/cms-hooks.mjs';
|
|
12
|
+
|
|
13
|
+
function InverseRelationsPanel({
|
|
14
|
+
contentTypeSlug,
|
|
15
|
+
itemId
|
|
16
|
+
}) {
|
|
17
|
+
const { apiBaseURL, apiBasePath, headers, navigate, Link } = usePluginOverrides("cms");
|
|
18
|
+
const basePath = useBasePath();
|
|
19
|
+
const client = createApiClient({
|
|
20
|
+
baseURL: apiBaseURL,
|
|
21
|
+
basePath: apiBasePath
|
|
22
|
+
});
|
|
23
|
+
const { data: inverseRelationsData, isLoading } = useQuery({
|
|
24
|
+
queryKey: ["cmsInverseRelations", contentTypeSlug, itemId],
|
|
25
|
+
queryFn: async () => {
|
|
26
|
+
const response = await client("/content-types/:slug/inverse-relations", {
|
|
27
|
+
method: "GET",
|
|
28
|
+
params: { slug: contentTypeSlug },
|
|
29
|
+
query: { itemId },
|
|
30
|
+
headers
|
|
31
|
+
});
|
|
32
|
+
return response.data?.inverseRelations ?? [];
|
|
33
|
+
},
|
|
34
|
+
staleTime: 1e3 * 60 * 5
|
|
35
|
+
});
|
|
36
|
+
if (isLoading) {
|
|
37
|
+
return /* @__PURE__ */ jsx(Card, { className: "animate-pulse", children: /* @__PURE__ */ jsx(CardHeader, { children: /* @__PURE__ */ jsx("div", { className: "h-5 w-32 bg-muted rounded" }) }) });
|
|
38
|
+
}
|
|
39
|
+
const inverseRelations = inverseRelationsData ?? [];
|
|
40
|
+
if (inverseRelations.length === 0) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold", children: "Related Items" }),
|
|
45
|
+
inverseRelations.map((relation) => /* @__PURE__ */ jsx(
|
|
46
|
+
InverseRelationSection,
|
|
47
|
+
{
|
|
48
|
+
relation,
|
|
49
|
+
contentTypeSlug,
|
|
50
|
+
itemId,
|
|
51
|
+
basePath,
|
|
52
|
+
navigate,
|
|
53
|
+
Link,
|
|
54
|
+
client,
|
|
55
|
+
headers
|
|
56
|
+
},
|
|
57
|
+
`${relation.sourceType}-${relation.fieldName}`
|
|
58
|
+
))
|
|
59
|
+
] });
|
|
60
|
+
}
|
|
61
|
+
function InverseRelationSection({
|
|
62
|
+
relation,
|
|
63
|
+
contentTypeSlug,
|
|
64
|
+
itemId,
|
|
65
|
+
basePath,
|
|
66
|
+
navigate,
|
|
67
|
+
Link,
|
|
68
|
+
client,
|
|
69
|
+
headers
|
|
70
|
+
}) {
|
|
71
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
72
|
+
const [deleteItemId, setDeleteItemId] = useState(null);
|
|
73
|
+
const [deleteError, setDeleteError] = useState(null);
|
|
74
|
+
const deleteContent = useDeleteContent(relation.sourceType);
|
|
75
|
+
const { data: itemsData, refetch } = useQuery({
|
|
76
|
+
queryKey: [
|
|
77
|
+
"cmsInverseRelationItems",
|
|
78
|
+
contentTypeSlug,
|
|
79
|
+
relation.sourceType,
|
|
80
|
+
itemId,
|
|
81
|
+
relation.fieldName
|
|
82
|
+
],
|
|
83
|
+
queryFn: async () => {
|
|
84
|
+
const response = await client(
|
|
85
|
+
"/content-types/:slug/inverse-relations/:sourceType",
|
|
86
|
+
{
|
|
87
|
+
method: "GET",
|
|
88
|
+
params: { slug: contentTypeSlug, sourceType: relation.sourceType },
|
|
89
|
+
query: { itemId, fieldName: relation.fieldName },
|
|
90
|
+
headers
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
return response.data ?? { items: [], total: 0 };
|
|
94
|
+
},
|
|
95
|
+
staleTime: 1e3 * 60 * 5,
|
|
96
|
+
enabled: isExpanded
|
|
97
|
+
});
|
|
98
|
+
const items = itemsData?.items ?? [];
|
|
99
|
+
const total = itemsData?.total ?? relation.count;
|
|
100
|
+
const handleDelete = async () => {
|
|
101
|
+
if (deleteItemId) {
|
|
102
|
+
setDeleteError(null);
|
|
103
|
+
try {
|
|
104
|
+
await deleteContent.mutateAsync(deleteItemId);
|
|
105
|
+
setDeleteItemId(null);
|
|
106
|
+
refetch();
|
|
107
|
+
} catch (error) {
|
|
108
|
+
const message = error instanceof Error ? error.message : "Failed to delete item. Please try again.";
|
|
109
|
+
setDeleteError(message);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
const handleAddNew = () => {
|
|
114
|
+
const createUrl = `${basePath}/cms/${relation.sourceType}/new?prefill_${relation.fieldName}=${itemId}`;
|
|
115
|
+
navigate(createUrl);
|
|
116
|
+
};
|
|
117
|
+
const LinkComponent = Link ?? "a";
|
|
118
|
+
return /* @__PURE__ */ jsxs(Card, { children: [
|
|
119
|
+
/* @__PURE__ */ jsx(CardHeader, { className: "py-3", children: /* @__PURE__ */ jsx(
|
|
120
|
+
"button",
|
|
121
|
+
{
|
|
122
|
+
type: "button",
|
|
123
|
+
onClick: () => setIsExpanded(!isExpanded),
|
|
124
|
+
className: "flex items-center justify-between w-full text-left",
|
|
125
|
+
children: /* @__PURE__ */ jsxs(CardTitle, { className: "text-base flex items-center gap-2", children: [
|
|
126
|
+
isExpanded ? /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" }) : /* @__PURE__ */ jsx(ChevronRight, { className: "h-4 w-4" }),
|
|
127
|
+
relation.sourceTypeName,
|
|
128
|
+
" (",
|
|
129
|
+
total,
|
|
130
|
+
")"
|
|
131
|
+
] })
|
|
132
|
+
}
|
|
133
|
+
) }),
|
|
134
|
+
isExpanded && /* @__PURE__ */ jsxs(CardContent, { className: "pt-0", children: [
|
|
135
|
+
items.length === 0 ? /* @__PURE__ */ jsxs("p", { className: "text-sm text-muted-foreground py-2", children: [
|
|
136
|
+
"No ",
|
|
137
|
+
relation.sourceTypeName.toLowerCase(),
|
|
138
|
+
" items yet."
|
|
139
|
+
] }) : /* @__PURE__ */ jsx("ul", { className: "space-y-2", children: items.map((item) => {
|
|
140
|
+
const displayValue = getDisplayValue(item);
|
|
141
|
+
const editUrl = `${basePath}/cms/${relation.sourceType}/${item.id}`;
|
|
142
|
+
return /* @__PURE__ */ jsxs(
|
|
143
|
+
"li",
|
|
144
|
+
{
|
|
145
|
+
className: "flex items-center justify-between py-2 px-3 rounded-md bg-muted/50 hover:bg-muted transition-colors",
|
|
146
|
+
children: [
|
|
147
|
+
/* @__PURE__ */ jsxs(
|
|
148
|
+
LinkComponent,
|
|
149
|
+
{
|
|
150
|
+
href: editUrl,
|
|
151
|
+
className: "flex-1 text-sm hover:underline flex items-center gap-2",
|
|
152
|
+
children: [
|
|
153
|
+
/* @__PURE__ */ jsx("span", { className: "truncate", children: displayValue }),
|
|
154
|
+
/* @__PURE__ */ jsx(ExternalLink, { className: "h-3 w-3 opacity-50" })
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
),
|
|
158
|
+
/* @__PURE__ */ jsx(
|
|
159
|
+
Button,
|
|
160
|
+
{
|
|
161
|
+
variant: "ghost",
|
|
162
|
+
size: "icon",
|
|
163
|
+
className: "h-7 w-7 text-muted-foreground hover:text-destructive",
|
|
164
|
+
onClick: () => setDeleteItemId(item.id),
|
|
165
|
+
children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
|
|
166
|
+
}
|
|
167
|
+
)
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
item.id
|
|
171
|
+
);
|
|
172
|
+
}) }),
|
|
173
|
+
/* @__PURE__ */ jsx("div", { className: "mt-3 pt-3 border-t", children: /* @__PURE__ */ jsxs(
|
|
174
|
+
Button,
|
|
175
|
+
{
|
|
176
|
+
variant: "outline",
|
|
177
|
+
size: "sm",
|
|
178
|
+
onClick: handleAddNew,
|
|
179
|
+
className: "w-full",
|
|
180
|
+
children: [
|
|
181
|
+
/* @__PURE__ */ jsx(Plus, { className: "h-4 w-4 mr-2" }),
|
|
182
|
+
"Add ",
|
|
183
|
+
relation.sourceTypeName
|
|
184
|
+
]
|
|
185
|
+
}
|
|
186
|
+
) })
|
|
187
|
+
] }),
|
|
188
|
+
/* @__PURE__ */ jsx(
|
|
189
|
+
AlertDialog,
|
|
190
|
+
{
|
|
191
|
+
open: !!deleteItemId,
|
|
192
|
+
onOpenChange: (open) => {
|
|
193
|
+
if (!open) {
|
|
194
|
+
setDeleteItemId(null);
|
|
195
|
+
setDeleteError(null);
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
children: /* @__PURE__ */ jsxs(AlertDialogContent, { children: [
|
|
199
|
+
/* @__PURE__ */ jsxs(AlertDialogHeader, { children: [
|
|
200
|
+
/* @__PURE__ */ jsxs(AlertDialogTitle, { children: [
|
|
201
|
+
"Delete ",
|
|
202
|
+
relation.sourceTypeName,
|
|
203
|
+
"?"
|
|
204
|
+
] }),
|
|
205
|
+
/* @__PURE__ */ jsxs(AlertDialogDescription, { children: [
|
|
206
|
+
"This action cannot be undone. This will permanently delete this",
|
|
207
|
+
" ",
|
|
208
|
+
relation.sourceTypeName.toLowerCase(),
|
|
209
|
+
"."
|
|
210
|
+
] })
|
|
211
|
+
] }),
|
|
212
|
+
deleteError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive", children: deleteError }),
|
|
213
|
+
/* @__PURE__ */ jsxs(AlertDialogFooter, { children: [
|
|
214
|
+
/* @__PURE__ */ jsx(AlertDialogCancel, { children: "Cancel" }),
|
|
215
|
+
/* @__PURE__ */ jsx(
|
|
216
|
+
AlertDialogAction,
|
|
217
|
+
{
|
|
218
|
+
onClick: handleDelete,
|
|
219
|
+
className: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
|
220
|
+
children: "Delete"
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
] })
|
|
224
|
+
] })
|
|
225
|
+
}
|
|
226
|
+
)
|
|
227
|
+
] });
|
|
228
|
+
}
|
|
229
|
+
function getDisplayValue(item) {
|
|
230
|
+
const data = item.parsedData;
|
|
231
|
+
const displayFields = ["name", "title", "label", "content", "author", "slug"];
|
|
232
|
+
for (const field of displayFields) {
|
|
233
|
+
if (typeof data[field] === "string" && data[field]) {
|
|
234
|
+
const value = data[field];
|
|
235
|
+
return value.length > 50 ? `${value.slice(0, 50)}...` : value;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return item.slug;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export { InverseRelationsPanel };
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
const React = require('react');
|
|
5
6
|
const lucideReact = require('lucide-react');
|
|
6
7
|
const button = require('../../../../../../../ui/src/components/button.cjs');
|
|
7
8
|
const context = require('@btst/stack/context');
|
|
8
9
|
const contentForm = require('../forms/content-form.cjs');
|
|
10
|
+
const inverseRelationsPanel = require('../inverse-relations-panel.cjs');
|
|
9
11
|
const emptyState = require('../shared/empty-state.cjs');
|
|
10
12
|
const pageWrapper = require('../shared/page-wrapper.cjs');
|
|
11
13
|
const editorSkeleton = require('../loading/editor-skeleton.cjs');
|
|
@@ -13,11 +15,59 @@ const index = require('../../localization/index.cjs');
|
|
|
13
15
|
const useRouteLifecycle = require('../../../../../../../ui/src/hooks/use-route-lifecycle.cjs');
|
|
14
16
|
const cmsHooks = require('../../hooks/cms-hooks.cjs');
|
|
15
17
|
|
|
18
|
+
function usePrefillParams() {
|
|
19
|
+
const [prefillData, setPrefillData] = React.useState({});
|
|
20
|
+
React.useEffect(() => {
|
|
21
|
+
if (typeof window === "undefined") {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const parseAndSetPrefillData = () => {
|
|
25
|
+
const params = new URLSearchParams(window.location.search);
|
|
26
|
+
const data = {};
|
|
27
|
+
for (const [key, value] of params.entries()) {
|
|
28
|
+
if (key.startsWith("prefill_")) {
|
|
29
|
+
const fieldName = key.slice("prefill_".length);
|
|
30
|
+
if (fieldName) {
|
|
31
|
+
data[fieldName] = value;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
setPrefillData(data);
|
|
36
|
+
};
|
|
37
|
+
parseAndSetPrefillData();
|
|
38
|
+
window.addEventListener("popstate", parseAndSetPrefillData);
|
|
39
|
+
return () => {
|
|
40
|
+
window.removeEventListener("popstate", parseAndSetPrefillData);
|
|
41
|
+
};
|
|
42
|
+
}, []);
|
|
43
|
+
return prefillData;
|
|
44
|
+
}
|
|
45
|
+
function convertPrefillToFormData(prefillParams, jsonSchema) {
|
|
46
|
+
const properties = jsonSchema.properties;
|
|
47
|
+
if (!properties) {
|
|
48
|
+
return prefillParams;
|
|
49
|
+
}
|
|
50
|
+
const result = {};
|
|
51
|
+
for (const [fieldName, value] of Object.entries(prefillParams)) {
|
|
52
|
+
const fieldSchema = properties[fieldName];
|
|
53
|
+
if (fieldSchema?.fieldType === "relation" && fieldSchema.relation) {
|
|
54
|
+
if (fieldSchema.relation.type === "belongsTo") {
|
|
55
|
+
result[fieldName] = { id: value };
|
|
56
|
+
} else {
|
|
57
|
+
result[fieldName] = [{ id: value }];
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
result[fieldName] = value;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
16
65
|
function ContentEditorPage({ typeSlug, id }) {
|
|
17
66
|
const overrides = context.usePluginOverrides("cms");
|
|
18
67
|
const { navigate } = overrides;
|
|
19
68
|
const localization = { ...index.CMS_LOCALIZATION, ...overrides.localization };
|
|
20
69
|
const basePath = context.useBasePath();
|
|
70
|
+
const prefillParams = usePrefillParams();
|
|
21
71
|
useRouteLifecycle.useRouteLifecycle({
|
|
22
72
|
routeName: "contentEditor",
|
|
23
73
|
context: {
|
|
@@ -86,14 +136,18 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
86
136
|
contentForm.ContentForm,
|
|
87
137
|
{
|
|
88
138
|
contentType,
|
|
89
|
-
initialData: item?.parsedData
|
|
139
|
+
initialData: isEditing ? item?.parsedData : Object.keys(prefillParams).length > 0 ? convertPrefillToFormData(
|
|
140
|
+
prefillParams,
|
|
141
|
+
JSON.parse(contentType.jsonSchema)
|
|
142
|
+
) : void 0,
|
|
90
143
|
initialSlug: item?.slug,
|
|
91
144
|
isEditing,
|
|
92
145
|
onSubmit: handleSubmit,
|
|
93
146
|
onCancel: () => navigate(`${basePath}/cms/${typeSlug}`)
|
|
94
147
|
},
|
|
95
148
|
isEditing ? `edit-${id}` : "create"
|
|
96
|
-
)
|
|
149
|
+
),
|
|
150
|
+
isEditing && id && /* @__PURE__ */ jsxRuntime.jsx(inverseRelationsPanel.InverseRelationsPanel, { contentTypeSlug: typeSlug, itemId: id })
|
|
97
151
|
] }) });
|
|
98
152
|
}
|
|
99
153
|
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
3
4
|
import { ArrowLeft } from 'lucide-react';
|
|
4
5
|
import { Button } from '../../../../../../../ui/src/components/button.mjs';
|
|
5
6
|
import { usePluginOverrides, useBasePath } from '@btst/stack/context';
|
|
6
7
|
import { ContentForm } from '../forms/content-form.mjs';
|
|
8
|
+
import { InverseRelationsPanel } from '../inverse-relations-panel.mjs';
|
|
7
9
|
import { EmptyState } from '../shared/empty-state.mjs';
|
|
8
10
|
import { PageWrapper } from '../shared/page-wrapper.mjs';
|
|
9
11
|
import { EditorSkeleton } from '../loading/editor-skeleton.mjs';
|
|
@@ -11,11 +13,59 @@ import { CMS_LOCALIZATION } from '../../localization/index.mjs';
|
|
|
11
13
|
import { useRouteLifecycle } from '../../../../../../../ui/src/hooks/use-route-lifecycle.mjs';
|
|
12
14
|
import { useSuspenseContentTypes, useContentItem, useCreateContent, useUpdateContent } from '../../hooks/cms-hooks.mjs';
|
|
13
15
|
|
|
16
|
+
function usePrefillParams() {
|
|
17
|
+
const [prefillData, setPrefillData] = useState({});
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (typeof window === "undefined") {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const parseAndSetPrefillData = () => {
|
|
23
|
+
const params = new URLSearchParams(window.location.search);
|
|
24
|
+
const data = {};
|
|
25
|
+
for (const [key, value] of params.entries()) {
|
|
26
|
+
if (key.startsWith("prefill_")) {
|
|
27
|
+
const fieldName = key.slice("prefill_".length);
|
|
28
|
+
if (fieldName) {
|
|
29
|
+
data[fieldName] = value;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
setPrefillData(data);
|
|
34
|
+
};
|
|
35
|
+
parseAndSetPrefillData();
|
|
36
|
+
window.addEventListener("popstate", parseAndSetPrefillData);
|
|
37
|
+
return () => {
|
|
38
|
+
window.removeEventListener("popstate", parseAndSetPrefillData);
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
return prefillData;
|
|
42
|
+
}
|
|
43
|
+
function convertPrefillToFormData(prefillParams, jsonSchema) {
|
|
44
|
+
const properties = jsonSchema.properties;
|
|
45
|
+
if (!properties) {
|
|
46
|
+
return prefillParams;
|
|
47
|
+
}
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const [fieldName, value] of Object.entries(prefillParams)) {
|
|
50
|
+
const fieldSchema = properties[fieldName];
|
|
51
|
+
if (fieldSchema?.fieldType === "relation" && fieldSchema.relation) {
|
|
52
|
+
if (fieldSchema.relation.type === "belongsTo") {
|
|
53
|
+
result[fieldName] = { id: value };
|
|
54
|
+
} else {
|
|
55
|
+
result[fieldName] = [{ id: value }];
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
result[fieldName] = value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
14
63
|
function ContentEditorPage({ typeSlug, id }) {
|
|
15
64
|
const overrides = usePluginOverrides("cms");
|
|
16
65
|
const { navigate } = overrides;
|
|
17
66
|
const localization = { ...CMS_LOCALIZATION, ...overrides.localization };
|
|
18
67
|
const basePath = useBasePath();
|
|
68
|
+
const prefillParams = usePrefillParams();
|
|
19
69
|
useRouteLifecycle({
|
|
20
70
|
routeName: "contentEditor",
|
|
21
71
|
context: {
|
|
@@ -84,14 +134,18 @@ function ContentEditorPage({ typeSlug, id }) {
|
|
|
84
134
|
ContentForm,
|
|
85
135
|
{
|
|
86
136
|
contentType,
|
|
87
|
-
initialData: item?.parsedData
|
|
137
|
+
initialData: isEditing ? item?.parsedData : Object.keys(prefillParams).length > 0 ? convertPrefillToFormData(
|
|
138
|
+
prefillParams,
|
|
139
|
+
JSON.parse(contentType.jsonSchema)
|
|
140
|
+
) : void 0,
|
|
88
141
|
initialSlug: item?.slug,
|
|
89
142
|
isEditing,
|
|
90
143
|
onSubmit: handleSubmit,
|
|
91
144
|
onCancel: () => navigate(`${basePath}/cms/${typeSlug}`)
|
|
92
145
|
},
|
|
93
146
|
isEditing ? `edit-${id}` : "create"
|
|
94
|
-
)
|
|
147
|
+
),
|
|
148
|
+
isEditing && id && /* @__PURE__ */ jsx(InverseRelationsPanel, { contentTypeSlug: typeSlug, itemId: id })
|
|
95
149
|
] }) });
|
|
96
150
|
}
|
|
97
151
|
|