@btst/stack 2.12.0 → 2.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/stack/src/plugins/cms/api/plugin.cjs +14 -11
- package/dist/packages/stack/src/plugins/cms/api/plugin.mjs +14 -11
- package/dist/packages/stack/src/plugins/cms/client/components/forms/relation-field.cjs +87 -15
- package/dist/packages/stack/src/plugins/cms/client/components/forms/relation-field.mjs +87 -15
- package/dist/packages/stack/src/plugins/cms/schemas.cjs +11 -6
- package/dist/packages/stack/src/plugins/cms/schemas.mjs +10 -6
- package/dist/packages/ui/src/components/auto-form/fields/array.cjs +181 -4
- package/dist/packages/ui/src/components/auto-form/fields/array.mjs +184 -7
- package/dist/packages/ui/src/components/auto-form/index.cjs +16 -1
- package/dist/packages/ui/src/components/auto-form/index.mjs +17 -2
- package/dist/plugins/cms/api/index.cjs +2 -0
- package/dist/plugins/cms/api/index.d.cts +3 -2
- package/dist/plugins/cms/api/index.d.mts +3 -2
- package/dist/plugins/cms/api/index.d.ts +3 -2
- package/dist/plugins/cms/api/index.mjs +1 -0
- package/dist/plugins/cms/client/hooks/index.d.cts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.mts +1 -1
- package/dist/plugins/cms/client/hooks/index.d.ts +1 -1
- package/dist/plugins/cms/query-keys.d.cts +398 -7
- package/dist/plugins/cms/query-keys.d.mts +398 -7
- package/dist/plugins/cms/query-keys.d.ts +398 -7
- package/dist/plugins/kanban/api/index.d.cts +1 -1
- package/dist/plugins/kanban/api/index.d.mts +1 -1
- package/dist/plugins/kanban/api/index.d.ts +1 -1
- package/dist/plugins/kanban/query-keys.d.cts +1 -1
- package/dist/plugins/kanban/query-keys.d.mts +1 -1
- package/dist/plugins/kanban/query-keys.d.ts +1 -1
- package/dist/plugins/ui-builder/index.d.cts +1 -1
- package/dist/plugins/ui-builder/index.d.mts +1 -1
- package/dist/plugins/ui-builder/index.d.ts +1 -1
- package/dist/shared/{stack.B8QD11QU.d.cts → stack.DFtpFS00.d.cts} +10 -0
- package/dist/shared/{stack.B8QD11QU.d.mts → stack.DFtpFS00.d.mts} +10 -0
- package/dist/shared/{stack.B8QD11QU.d.ts → stack.DFtpFS00.d.ts} +10 -0
- package/package.json +1 -1
- package/src/plugins/cms/__tests__/schema-roundtrip.test.ts +203 -0
- package/src/plugins/cms/api/index.ts +1 -0
- package/src/plugins/cms/api/plugin.ts +33 -16
- package/src/plugins/cms/client/components/forms/relation-field.tsx +121 -36
- package/src/plugins/cms/schemas.ts +26 -6
- package/src/plugins/cms/types.ts +10 -0
- package/dist/shared/stack.DbnCLdNF.d.mts +0 -399
- package/dist/shared/stack.fXlShnhv.d.ts +0 -399
- package/dist/shared/stack.r6HohbyX.d.cts +0 -399
- package/dist/shared/{stack.B6S3cgwN.d.cts → stack.CuI_-z-_.d.cts} +16 -16
- package/dist/shared/{stack.Bzfx-_lq.d.mts → stack.D2RVkQD8.d.mts} +16 -16
- package/dist/shared/{stack.j5SFLC1d.d.ts → stack.DXrOtuek.d.ts} +16 -16
|
@@ -297,6 +297,13 @@ const cmsBackendPlugin = (config) => {
|
|
|
297
297
|
}
|
|
298
298
|
}),
|
|
299
299
|
routes: (adapter) => {
|
|
300
|
+
const listContentQuerySchema = schemas.createListContentQuerySchema(
|
|
301
|
+
config.maxPageSize
|
|
302
|
+
);
|
|
303
|
+
const paginationQuerySchema = z.z.object({
|
|
304
|
+
limit: z.z.coerce.number().min(1).max(config.maxPageSize ?? schemas.DEFAULT_MAX_PAGE_SIZE).optional().default(20),
|
|
305
|
+
offset: z.z.coerce.number().min(0).optional().default(0)
|
|
306
|
+
});
|
|
300
307
|
const getContentType = async (slug) => {
|
|
301
308
|
await ensureSynced(adapter);
|
|
302
309
|
return adapter.findOne({
|
|
@@ -319,7 +326,7 @@ const cmsBackendPlugin = (config) => {
|
|
|
319
326
|
});
|
|
320
327
|
const typesWithCounts = await Promise.all(
|
|
321
328
|
contentTypes.map(async (ct) => {
|
|
322
|
-
const
|
|
329
|
+
const itemCount = await adapter.count({
|
|
323
330
|
model: "contentItem",
|
|
324
331
|
where: [
|
|
325
332
|
{
|
|
@@ -331,7 +338,7 @@ const cmsBackendPlugin = (config) => {
|
|
|
331
338
|
});
|
|
332
339
|
return {
|
|
333
340
|
...getters.serializeContentType(ct),
|
|
334
|
-
itemCount
|
|
341
|
+
itemCount
|
|
335
342
|
};
|
|
336
343
|
})
|
|
337
344
|
);
|
|
@@ -358,7 +365,7 @@ const cmsBackendPlugin = (config) => {
|
|
|
358
365
|
{
|
|
359
366
|
method: "GET",
|
|
360
367
|
params: z.z.object({ typeSlug: z.z.string() }),
|
|
361
|
-
query:
|
|
368
|
+
query: listContentQuerySchema
|
|
362
369
|
},
|
|
363
370
|
async (ctx) => {
|
|
364
371
|
const { typeSlug } = ctx.params;
|
|
@@ -663,10 +670,8 @@ const cmsBackendPlugin = (config) => {
|
|
|
663
670
|
params: z.z.object({ typeSlug: z.z.string() }),
|
|
664
671
|
query: z.z.object({
|
|
665
672
|
field: z.z.string(),
|
|
666
|
-
targetId: z.z.string()
|
|
667
|
-
|
|
668
|
-
offset: z.z.coerce.number().min(0).optional().default(0)
|
|
669
|
-
})
|
|
673
|
+
targetId: z.z.string()
|
|
674
|
+
}).merge(paginationQuerySchema)
|
|
670
675
|
},
|
|
671
676
|
async (ctx) => {
|
|
672
677
|
const { typeSlug } = ctx.params;
|
|
@@ -810,10 +815,8 @@ const cmsBackendPlugin = (config) => {
|
|
|
810
815
|
}),
|
|
811
816
|
query: z.z.object({
|
|
812
817
|
itemId: z.z.string(),
|
|
813
|
-
fieldName: z.z.string()
|
|
814
|
-
|
|
815
|
-
offset: z.z.coerce.number().min(0).optional().default(0)
|
|
816
|
-
})
|
|
818
|
+
fieldName: z.z.string()
|
|
819
|
+
}).merge(paginationQuerySchema)
|
|
817
820
|
},
|
|
818
821
|
async (ctx) => {
|
|
819
822
|
const { slug, sourceType } = ctx.params;
|
|
@@ -2,7 +2,7 @@ import { defineBackendPlugin, createEndpoint } from '@btst/stack/plugins/api';
|
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { formSchemaToZod, zodToFormSchema } from '../../../../../ui/src/lib/schema-converter.mjs';
|
|
4
4
|
import { cmsSchema } from '../db.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { createListContentQuerySchema, DEFAULT_MAX_PAGE_SIZE } from '../schemas.mjs';
|
|
6
6
|
import { slugify } from '../utils.mjs';
|
|
7
7
|
import { serializeContentType, getAllContentItems, serializeContentItemWithType, serializeContentItem, getContentItemById, getContentItemBySlug, getAllContentTypes } from './getters.mjs';
|
|
8
8
|
import { createCMSContentItem } from './mutations.mjs';
|
|
@@ -295,6 +295,13 @@ const cmsBackendPlugin = (config) => {
|
|
|
295
295
|
}
|
|
296
296
|
}),
|
|
297
297
|
routes: (adapter) => {
|
|
298
|
+
const listContentQuerySchema = createListContentQuerySchema(
|
|
299
|
+
config.maxPageSize
|
|
300
|
+
);
|
|
301
|
+
const paginationQuerySchema = z.object({
|
|
302
|
+
limit: z.coerce.number().min(1).max(config.maxPageSize ?? DEFAULT_MAX_PAGE_SIZE).optional().default(20),
|
|
303
|
+
offset: z.coerce.number().min(0).optional().default(0)
|
|
304
|
+
});
|
|
298
305
|
const getContentType = async (slug) => {
|
|
299
306
|
await ensureSynced(adapter);
|
|
300
307
|
return adapter.findOne({
|
|
@@ -317,7 +324,7 @@ const cmsBackendPlugin = (config) => {
|
|
|
317
324
|
});
|
|
318
325
|
const typesWithCounts = await Promise.all(
|
|
319
326
|
contentTypes.map(async (ct) => {
|
|
320
|
-
const
|
|
327
|
+
const itemCount = await adapter.count({
|
|
321
328
|
model: "contentItem",
|
|
322
329
|
where: [
|
|
323
330
|
{
|
|
@@ -329,7 +336,7 @@ const cmsBackendPlugin = (config) => {
|
|
|
329
336
|
});
|
|
330
337
|
return {
|
|
331
338
|
...serializeContentType(ct),
|
|
332
|
-
itemCount
|
|
339
|
+
itemCount
|
|
333
340
|
};
|
|
334
341
|
})
|
|
335
342
|
);
|
|
@@ -661,10 +668,8 @@ const cmsBackendPlugin = (config) => {
|
|
|
661
668
|
params: z.object({ typeSlug: z.string() }),
|
|
662
669
|
query: z.object({
|
|
663
670
|
field: z.string(),
|
|
664
|
-
targetId: z.string()
|
|
665
|
-
|
|
666
|
-
offset: z.coerce.number().min(0).optional().default(0)
|
|
667
|
-
})
|
|
671
|
+
targetId: z.string()
|
|
672
|
+
}).merge(paginationQuerySchema)
|
|
668
673
|
},
|
|
669
674
|
async (ctx) => {
|
|
670
675
|
const { typeSlug } = ctx.params;
|
|
@@ -808,10 +813,8 @@ const cmsBackendPlugin = (config) => {
|
|
|
808
813
|
}),
|
|
809
814
|
query: z.object({
|
|
810
815
|
itemId: z.string(),
|
|
811
|
-
fieldName: z.string()
|
|
812
|
-
|
|
813
|
-
offset: z.coerce.number().min(0).optional().default(0)
|
|
814
|
-
})
|
|
816
|
+
fieldName: z.string()
|
|
817
|
+
}).merge(paginationQuerySchema)
|
|
815
818
|
},
|
|
816
819
|
async (ctx) => {
|
|
817
820
|
const { slug, sourceType } = ctx.params;
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
const jsxRuntime = require('react/jsx-runtime');
|
|
5
5
|
const React = require('react');
|
|
6
|
+
const reactQuery = require('@tanstack/react-query');
|
|
7
|
+
const client = require('@btst/stack/plugins/client');
|
|
8
|
+
const context = require('@btst/stack/context');
|
|
9
|
+
const plugins_cms_queryKeys = require('../../../../../../../../plugins/cms/query-keys.cjs');
|
|
6
10
|
const multiSelect = require('../../../../../../../ui/src/components/multi-select.cjs');
|
|
7
11
|
const button = require('../../../../../../../ui/src/components/button.cjs');
|
|
8
12
|
const LucideIcons = require('lucide-react');
|
|
@@ -12,6 +16,14 @@ const label = require('../../../../../../../ui/src/components/label.cjs');
|
|
|
12
16
|
const textarea = require('../../../../../../../ui/src/components/textarea.cjs');
|
|
13
17
|
const cmsHooks = require('../../hooks/cms-hooks.cjs');
|
|
14
18
|
|
|
19
|
+
const RELATION_DETAIL_QUERY_OPTS = {
|
|
20
|
+
retry: false,
|
|
21
|
+
refetchOnWindowFocus: false,
|
|
22
|
+
refetchOnMount: false,
|
|
23
|
+
refetchOnReconnect: false,
|
|
24
|
+
staleTime: 1e3 * 60 * 5,
|
|
25
|
+
gcTime: 1e3 * 60 * 10
|
|
26
|
+
};
|
|
15
27
|
function RelationField({
|
|
16
28
|
field,
|
|
17
29
|
fieldConfigItem,
|
|
@@ -23,12 +35,19 @@ function RelationField({
|
|
|
23
35
|
const [newItemName, setNewItemName] = React.useState("");
|
|
24
36
|
const [newItemDescription, setNewItemDescription] = React.useState("");
|
|
25
37
|
const [createError, setCreateError] = React.useState(null);
|
|
38
|
+
const { apiBaseURL, apiBasePath, headers } = context.usePluginOverrides("cms");
|
|
39
|
+
const listClient = React.useMemo(
|
|
40
|
+
() => client.createApiClient({
|
|
41
|
+
baseURL: apiBaseURL,
|
|
42
|
+
basePath: apiBasePath
|
|
43
|
+
}),
|
|
44
|
+
[apiBaseURL, apiBasePath]
|
|
45
|
+
);
|
|
46
|
+
const cmsQueries = React.useMemo(
|
|
47
|
+
() => plugins_cms_queryKeys.createCMSQueryKeys(listClient, headers),
|
|
48
|
+
[listClient, headers]
|
|
49
|
+
);
|
|
26
50
|
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
51
|
const normalizedValue = React.useMemo(() => {
|
|
33
52
|
if (!field.value) return [];
|
|
34
53
|
if (isSingleSelect) {
|
|
@@ -40,8 +59,42 @@ function RelationField({
|
|
|
40
59
|
}
|
|
41
60
|
return field.value || [];
|
|
42
61
|
}, [field.value, isSingleSelect]);
|
|
62
|
+
const { items: availableItems, isLoading } = cmsHooks.useContent(relation.targetType, {
|
|
63
|
+
limit: 500
|
|
64
|
+
});
|
|
65
|
+
const missingDetailIds = React.useMemo(() => {
|
|
66
|
+
const loadedIds = new Set(availableItems.map((i) => i.id));
|
|
67
|
+
return normalizedValue.map((v) => v.id).filter((id) => id.length > 0 && !loadedIds.has(id));
|
|
68
|
+
}, [availableItems, normalizedValue]);
|
|
69
|
+
const hydrationResult = reactQuery.useQueries({
|
|
70
|
+
queries: missingDetailIds.map((id) => ({
|
|
71
|
+
...cmsQueries.cmsContent.detail(relation.targetType, id),
|
|
72
|
+
...RELATION_DETAIL_QUERY_OPTS,
|
|
73
|
+
enabled: Boolean(relation.targetType && id)
|
|
74
|
+
})),
|
|
75
|
+
combine: (results) => ({
|
|
76
|
+
data: results.map(
|
|
77
|
+
(r) => r.data
|
|
78
|
+
),
|
|
79
|
+
isHydrating: results.some((r) => r.isFetching)
|
|
80
|
+
})
|
|
81
|
+
});
|
|
82
|
+
const isHydratingLabels = hydrationResult.isHydrating;
|
|
83
|
+
const itemById = React.useMemo(() => {
|
|
84
|
+
const m = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const it of availableItems) {
|
|
86
|
+
m.set(it.id, it);
|
|
87
|
+
}
|
|
88
|
+
for (let i = 0; i < missingDetailIds.length; i++) {
|
|
89
|
+
const row = hydrationResult.data[i];
|
|
90
|
+
if (row?.id) {
|
|
91
|
+
m.set(row.id, row);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return m;
|
|
95
|
+
}, [availableItems, missingDetailIds, hydrationResult.data]);
|
|
43
96
|
const selectedOptions = normalizedValue.map((v) => {
|
|
44
|
-
const item =
|
|
97
|
+
const item = itemById.get(v.id);
|
|
45
98
|
if (item) {
|
|
46
99
|
const displayValue = item.parsedData?.[relation.displayField] || item.slug;
|
|
47
100
|
return {
|
|
@@ -50,14 +103,33 @@ function RelationField({
|
|
|
50
103
|
};
|
|
51
104
|
}
|
|
52
105
|
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
106
|
});
|
|
107
|
+
const options = React.useMemo(() => {
|
|
108
|
+
const merged = [
|
|
109
|
+
...availableItems
|
|
110
|
+
];
|
|
111
|
+
const seen = new Set(merged.map((x) => x.id));
|
|
112
|
+
for (let i = 0; i < missingDetailIds.length; i++) {
|
|
113
|
+
const row = hydrationResult.data[i];
|
|
114
|
+
if (row?.id && !seen.has(row.id)) {
|
|
115
|
+
merged.push(row);
|
|
116
|
+
seen.add(row.id);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return merged.map((item) => {
|
|
120
|
+
const displayValue = item.parsedData?.[relation.displayField] || item.slug;
|
|
121
|
+
return {
|
|
122
|
+
value: item.id,
|
|
123
|
+
label: String(displayValue)
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
}, [
|
|
127
|
+
availableItems,
|
|
128
|
+
hydrationResult.data,
|
|
129
|
+
missingDetailIds,
|
|
130
|
+
relation.displayField
|
|
131
|
+
]);
|
|
132
|
+
const createMutation = cmsHooks.useCreateContent(relation.targetType);
|
|
61
133
|
const handleChange = React.useCallback(
|
|
62
134
|
(newOptions) => {
|
|
63
135
|
if (isSingleSelect) {
|
|
@@ -121,8 +193,8 @@ function RelationField({
|
|
|
121
193
|
value: selectedOptions,
|
|
122
194
|
onChange: handleChange,
|
|
123
195
|
options,
|
|
124
|
-
placeholder: isLoading ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
|
|
125
|
-
disabled: isLoading,
|
|
196
|
+
placeholder: isLoading || isHydratingLabels ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
|
|
197
|
+
disabled: isLoading || isHydratingLabels,
|
|
126
198
|
hidePlaceholderWhenSelected: true,
|
|
127
199
|
emptyIndicator: /* @__PURE__ */ jsxRuntime.jsxs("p", { className: "text-center text-sm text-muted-foreground py-4", children: [
|
|
128
200
|
"No ",
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
3
3
|
import { useState, useMemo, useCallback } from 'react';
|
|
4
|
+
import { useQueries } from '@tanstack/react-query';
|
|
5
|
+
import { createApiClient } from '@btst/stack/plugins/client';
|
|
6
|
+
import { usePluginOverrides } from '@btst/stack/context';
|
|
7
|
+
import { createCMSQueryKeys } from '../../../../../../../../plugins/cms/query-keys.mjs';
|
|
4
8
|
import MultipleSelector from '../../../../../../../ui/src/components/multi-select.mjs';
|
|
5
9
|
import { Button } from '../../../../../../../ui/src/components/button.mjs';
|
|
6
10
|
import { Plus, X } from 'lucide-react';
|
|
@@ -10,6 +14,14 @@ import { Label } from '../../../../../../../ui/src/components/label.mjs';
|
|
|
10
14
|
import { Textarea } from '../../../../../../../ui/src/components/textarea.mjs';
|
|
11
15
|
import { useContent, useCreateContent } from '../../hooks/cms-hooks.mjs';
|
|
12
16
|
|
|
17
|
+
const RELATION_DETAIL_QUERY_OPTS = {
|
|
18
|
+
retry: false,
|
|
19
|
+
refetchOnWindowFocus: false,
|
|
20
|
+
refetchOnMount: false,
|
|
21
|
+
refetchOnReconnect: false,
|
|
22
|
+
staleTime: 1e3 * 60 * 5,
|
|
23
|
+
gcTime: 1e3 * 60 * 10
|
|
24
|
+
};
|
|
13
25
|
function RelationField({
|
|
14
26
|
field,
|
|
15
27
|
fieldConfigItem,
|
|
@@ -21,12 +33,19 @@ function RelationField({
|
|
|
21
33
|
const [newItemName, setNewItemName] = useState("");
|
|
22
34
|
const [newItemDescription, setNewItemDescription] = useState("");
|
|
23
35
|
const [createError, setCreateError] = useState(null);
|
|
36
|
+
const { apiBaseURL, apiBasePath, headers } = usePluginOverrides("cms");
|
|
37
|
+
const listClient = useMemo(
|
|
38
|
+
() => createApiClient({
|
|
39
|
+
baseURL: apiBaseURL,
|
|
40
|
+
basePath: apiBasePath
|
|
41
|
+
}),
|
|
42
|
+
[apiBaseURL, apiBasePath]
|
|
43
|
+
);
|
|
44
|
+
const cmsQueries = useMemo(
|
|
45
|
+
() => createCMSQueryKeys(listClient, headers),
|
|
46
|
+
[listClient, headers]
|
|
47
|
+
);
|
|
24
48
|
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
49
|
const normalizedValue = useMemo(() => {
|
|
31
50
|
if (!field.value) return [];
|
|
32
51
|
if (isSingleSelect) {
|
|
@@ -38,8 +57,42 @@ function RelationField({
|
|
|
38
57
|
}
|
|
39
58
|
return field.value || [];
|
|
40
59
|
}, [field.value, isSingleSelect]);
|
|
60
|
+
const { items: availableItems, isLoading } = useContent(relation.targetType, {
|
|
61
|
+
limit: 500
|
|
62
|
+
});
|
|
63
|
+
const missingDetailIds = useMemo(() => {
|
|
64
|
+
const loadedIds = new Set(availableItems.map((i) => i.id));
|
|
65
|
+
return normalizedValue.map((v) => v.id).filter((id) => id.length > 0 && !loadedIds.has(id));
|
|
66
|
+
}, [availableItems, normalizedValue]);
|
|
67
|
+
const hydrationResult = useQueries({
|
|
68
|
+
queries: missingDetailIds.map((id) => ({
|
|
69
|
+
...cmsQueries.cmsContent.detail(relation.targetType, id),
|
|
70
|
+
...RELATION_DETAIL_QUERY_OPTS,
|
|
71
|
+
enabled: Boolean(relation.targetType && id)
|
|
72
|
+
})),
|
|
73
|
+
combine: (results) => ({
|
|
74
|
+
data: results.map(
|
|
75
|
+
(r) => r.data
|
|
76
|
+
),
|
|
77
|
+
isHydrating: results.some((r) => r.isFetching)
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
const isHydratingLabels = hydrationResult.isHydrating;
|
|
81
|
+
const itemById = useMemo(() => {
|
|
82
|
+
const m = /* @__PURE__ */ new Map();
|
|
83
|
+
for (const it of availableItems) {
|
|
84
|
+
m.set(it.id, it);
|
|
85
|
+
}
|
|
86
|
+
for (let i = 0; i < missingDetailIds.length; i++) {
|
|
87
|
+
const row = hydrationResult.data[i];
|
|
88
|
+
if (row?.id) {
|
|
89
|
+
m.set(row.id, row);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return m;
|
|
93
|
+
}, [availableItems, missingDetailIds, hydrationResult.data]);
|
|
41
94
|
const selectedOptions = normalizedValue.map((v) => {
|
|
42
|
-
const item =
|
|
95
|
+
const item = itemById.get(v.id);
|
|
43
96
|
if (item) {
|
|
44
97
|
const displayValue = item.parsedData?.[relation.displayField] || item.slug;
|
|
45
98
|
return {
|
|
@@ -48,14 +101,33 @@ function RelationField({
|
|
|
48
101
|
};
|
|
49
102
|
}
|
|
50
103
|
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
104
|
});
|
|
105
|
+
const options = useMemo(() => {
|
|
106
|
+
const merged = [
|
|
107
|
+
...availableItems
|
|
108
|
+
];
|
|
109
|
+
const seen = new Set(merged.map((x) => x.id));
|
|
110
|
+
for (let i = 0; i < missingDetailIds.length; i++) {
|
|
111
|
+
const row = hydrationResult.data[i];
|
|
112
|
+
if (row?.id && !seen.has(row.id)) {
|
|
113
|
+
merged.push(row);
|
|
114
|
+
seen.add(row.id);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return merged.map((item) => {
|
|
118
|
+
const displayValue = item.parsedData?.[relation.displayField] || item.slug;
|
|
119
|
+
return {
|
|
120
|
+
value: item.id,
|
|
121
|
+
label: String(displayValue)
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}, [
|
|
125
|
+
availableItems,
|
|
126
|
+
hydrationResult.data,
|
|
127
|
+
missingDetailIds,
|
|
128
|
+
relation.displayField
|
|
129
|
+
]);
|
|
130
|
+
const createMutation = useCreateContent(relation.targetType);
|
|
59
131
|
const handleChange = useCallback(
|
|
60
132
|
(newOptions) => {
|
|
61
133
|
if (isSingleSelect) {
|
|
@@ -119,8 +191,8 @@ function RelationField({
|
|
|
119
191
|
value: selectedOptions,
|
|
120
192
|
onChange: handleChange,
|
|
121
193
|
options,
|
|
122
|
-
placeholder: isLoading ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
|
|
123
|
-
disabled: isLoading,
|
|
194
|
+
placeholder: isLoading || isHydratingLabels ? "Loading..." : `Select ${relation.targetType}${isSingleSelect ? "" : "(s)"}...`,
|
|
195
|
+
disabled: isLoading || isHydratingLabels,
|
|
124
196
|
hidePlaceholderWhenSelected: true,
|
|
125
197
|
emptyIndicator: /* @__PURE__ */ jsxs("p", { className: "text-center text-sm text-muted-foreground py-4", children: [
|
|
126
198
|
"No ",
|
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
const z = require('zod');
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
const DEFAULT_MAX_PAGE_SIZE = 1e3;
|
|
6
|
+
function createListContentQuerySchema(maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
|
|
7
|
+
return z.z.object({
|
|
8
|
+
slug: z.z.string().optional(),
|
|
9
|
+
limit: z.z.coerce.number().min(1).max(maxPageSize).optional().default(20),
|
|
10
|
+
offset: z.z.coerce.number().min(0).optional().default(0)
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
createListContentQuerySchema();
|
|
10
14
|
z.z.object({
|
|
11
15
|
slug: z.z.string().min(1, "Slug is required"),
|
|
12
16
|
// Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
|
|
@@ -47,7 +51,8 @@ z.z.object({
|
|
|
47
51
|
offset: z.z.number()
|
|
48
52
|
});
|
|
49
53
|
|
|
54
|
+
exports.DEFAULT_MAX_PAGE_SIZE = DEFAULT_MAX_PAGE_SIZE;
|
|
50
55
|
exports.contentItemResponseSchema = contentItemResponseSchema;
|
|
51
56
|
exports.contentItemWithDataResponseSchema = contentItemWithDataResponseSchema;
|
|
52
57
|
exports.contentTypeResponseSchema = contentTypeResponseSchema;
|
|
53
|
-
exports.
|
|
58
|
+
exports.createListContentQuerySchema = createListContentQuerySchema;
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
const DEFAULT_MAX_PAGE_SIZE = 1e3;
|
|
4
|
+
function createListContentQuerySchema(maxPageSize = DEFAULT_MAX_PAGE_SIZE) {
|
|
5
|
+
return z.object({
|
|
6
|
+
slug: z.string().optional(),
|
|
7
|
+
limit: z.coerce.number().min(1).max(maxPageSize).optional().default(20),
|
|
8
|
+
offset: z.coerce.number().min(0).optional().default(0)
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
createListContentQuerySchema();
|
|
8
12
|
z.object({
|
|
9
13
|
slug: z.string().min(1, "Slug is required"),
|
|
10
14
|
// Use passthrough object instead of z.record(z.unknown()) due to Zod v4 bug
|
|
@@ -45,4 +49,4 @@ z.object({
|
|
|
45
49
|
offset: z.number()
|
|
46
50
|
});
|
|
47
51
|
|
|
48
|
-
export { contentItemResponseSchema, contentItemWithDataResponseSchema, contentTypeResponseSchema,
|
|
52
|
+
export { DEFAULT_MAX_PAGE_SIZE, contentItemResponseSchema, contentItemWithDataResponseSchema, contentTypeResponseSchema, createListContentQuerySchema };
|