@btst/stack 2.11.8 → 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/stack/src/plugins/comments/api/query-key-defs.cjs +2 -1
- package/dist/packages/stack/src/plugins/comments/api/query-key-defs.mjs +2 -1
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.cjs +6 -2
- package/dist/packages/stack/src/plugins/comments/client/components/comment-thread.mjs +6 -2
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.cjs +5 -3
- package/dist/packages/stack/src/plugins/comments/client/hooks/use-comments.mjs +5 -3
- 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/comments/api/index.d.cts +4 -4
- package/dist/plugins/comments/api/index.d.mts +4 -4
- package/dist/plugins/comments/api/index.d.ts +4 -4
- package/dist/plugins/comments/client/components/index.d.cts +11 -0
- package/dist/plugins/comments/client/components/index.d.mts +11 -0
- package/dist/plugins/comments/client/components/index.d.ts +11 -0
- package/dist/plugins/comments/client/hooks/index.d.cts +17 -1
- package/dist/plugins/comments/client/hooks/index.d.mts +17 -1
- package/dist/plugins/comments/client/hooks/index.d.ts +17 -1
- package/dist/plugins/comments/client/index.d.cts +9 -0
- package/dist/plugins/comments/client/index.d.mts +9 -0
- package/dist/plugins/comments/client/index.d.ts +9 -0
- package/dist/plugins/comments/query-keys.cjs +1 -0
- package/dist/plugins/comments/query-keys.d.cts +3 -2
- package/dist/plugins/comments/query-keys.d.mts +3 -2
- package/dist/plugins/comments/query-keys.d.ts +3 -2
- package/dist/plugins/comments/query-keys.mjs +1 -0
- 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.CJE9sAjV.d.ts → stack.Bq3kiUaM.d.ts} +1 -1
- package/dist/shared/{stack.C-b3Sn8j.d.cts → stack.C7KNNiJm.d.cts} +3 -1
- package/dist/shared/{stack.C-b3Sn8j.d.mts → stack.C7KNNiJm.d.mts} +3 -1
- package/dist/shared/{stack.C-b3Sn8j.d.ts → stack.C7KNNiJm.d.ts} +3 -1
- package/dist/shared/{stack.Dcz6636A.d.mts → stack.CCncwK6g.d.cts} +1 -1
- package/dist/shared/{stack.CmHRdhl8.d.cts → stack.CO1g47IS.d.mts} +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/src/plugins/comments/api/query-key-defs.ts +5 -1
- package/src/plugins/comments/client/components/comment-thread.tsx +15 -0
- package/src/plugins/comments/client/hooks/use-comments.tsx +26 -3
- package/src/plugins/comments/client/overrides.ts +10 -0
- package/src/plugins/comments/query-keys.ts +2 -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 };
|
|
@@ -27,6 +27,7 @@ function commentsThreadDiscriminator(params) {
|
|
|
27
27
|
parentId: params?.parentId,
|
|
28
28
|
status: params?.status,
|
|
29
29
|
currentUserId: params?.currentUserId,
|
|
30
|
+
sort: params?.sort,
|
|
30
31
|
limit: params?.limit ?? 20
|
|
31
32
|
};
|
|
32
33
|
}
|
|
@@ -48,7 +49,7 @@ const COMMENTS_QUERY_KEYS = {
|
|
|
48
49
|
commentCount: (params) => ["comments", "count", commentCountDiscriminator(params)],
|
|
49
50
|
/**
|
|
50
51
|
* Key for the infinite thread query (top-level comments, load-more).
|
|
51
|
-
* Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
|
|
52
|
+
* Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
|
|
52
53
|
* Offset is excluded — it is driven by `pageParam`, not baked into the key.
|
|
53
54
|
*/
|
|
54
55
|
commentsThread: (params) => ["commentsThread", "list", commentsThreadDiscriminator(params)]
|
|
@@ -25,6 +25,7 @@ function commentsThreadDiscriminator(params) {
|
|
|
25
25
|
parentId: params?.parentId,
|
|
26
26
|
status: params?.status,
|
|
27
27
|
currentUserId: params?.currentUserId,
|
|
28
|
+
sort: params?.sort,
|
|
28
29
|
limit: params?.limit ?? 20
|
|
29
30
|
};
|
|
30
31
|
}
|
|
@@ -46,7 +47,7 @@ const COMMENTS_QUERY_KEYS = {
|
|
|
46
47
|
commentCount: (params) => ["comments", "count", commentCountDiscriminator(params)],
|
|
47
48
|
/**
|
|
48
49
|
* Key for the infinite thread query (top-level comments, load-more).
|
|
49
|
-
* Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, limit }]
|
|
50
|
+
* Full key: ["commentsThread", "list", { resourceId, resourceType, parentId, status, currentUserId, sort, limit }]
|
|
50
51
|
* Offset is excluded — it is driven by `pageParam`, not baked into the key.
|
|
51
52
|
*/
|
|
52
53
|
commentsThread: (params) => ["commentsThread", "list", commentsThreadDiscriminator(params)]
|
|
@@ -195,12 +195,14 @@ function CommentThreadInner({
|
|
|
195
195
|
localization: localizationProp,
|
|
196
196
|
pageSize: pageSizeProp,
|
|
197
197
|
allowPosting: allowPostingProp,
|
|
198
|
-
allowEditing: allowEditingProp
|
|
198
|
+
allowEditing: allowEditingProp,
|
|
199
|
+
sort: sortProp
|
|
199
200
|
}) {
|
|
200
201
|
const overrides = context.usePluginOverrides("comments", {});
|
|
201
202
|
const pageSize = pageSizeProp ?? overrides.defaultCommentPageSize ?? DEFAULT_PAGE_SIZE;
|
|
202
203
|
const allowPosting = allowPostingProp ?? overrides.allowPosting ?? true;
|
|
203
204
|
const allowEditing = allowEditingProp ?? overrides.allowEditing ?? true;
|
|
205
|
+
const sort = sortProp ?? overrides.defaultCommentSort ?? "desc";
|
|
204
206
|
const loc = { ...index.COMMENTS_LOCALIZATION, ...localizationProp };
|
|
205
207
|
const [replyingTo, setReplyingTo] = React.useState(null);
|
|
206
208
|
const [expandedReplies, setExpandedReplies] = React.useState(
|
|
@@ -222,6 +224,7 @@ function CommentThreadInner({
|
|
|
222
224
|
status: "approved",
|
|
223
225
|
parentId: null,
|
|
224
226
|
currentUserId,
|
|
227
|
+
sort,
|
|
225
228
|
pageSize
|
|
226
229
|
});
|
|
227
230
|
const postMutation = useComments.usePostComment(config, {
|
|
@@ -229,7 +232,8 @@ function CommentThreadInner({
|
|
|
229
232
|
resourceType,
|
|
230
233
|
currentUserId,
|
|
231
234
|
infiniteKey: threadQueryKey,
|
|
232
|
-
pageSize
|
|
235
|
+
pageSize,
|
|
236
|
+
sort
|
|
233
237
|
});
|
|
234
238
|
const handlePost = async (body) => {
|
|
235
239
|
if (!currentUserId) return;
|
|
@@ -193,12 +193,14 @@ function CommentThreadInner({
|
|
|
193
193
|
localization: localizationProp,
|
|
194
194
|
pageSize: pageSizeProp,
|
|
195
195
|
allowPosting: allowPostingProp,
|
|
196
|
-
allowEditing: allowEditingProp
|
|
196
|
+
allowEditing: allowEditingProp,
|
|
197
|
+
sort: sortProp
|
|
197
198
|
}) {
|
|
198
199
|
const overrides = usePluginOverrides("comments", {});
|
|
199
200
|
const pageSize = pageSizeProp ?? overrides.defaultCommentPageSize ?? DEFAULT_PAGE_SIZE;
|
|
200
201
|
const allowPosting = allowPostingProp ?? overrides.allowPosting ?? true;
|
|
201
202
|
const allowEditing = allowEditingProp ?? overrides.allowEditing ?? true;
|
|
203
|
+
const sort = sortProp ?? overrides.defaultCommentSort ?? "desc";
|
|
202
204
|
const loc = { ...COMMENTS_LOCALIZATION, ...localizationProp };
|
|
203
205
|
const [replyingTo, setReplyingTo] = useState(null);
|
|
204
206
|
const [expandedReplies, setExpandedReplies] = useState(
|
|
@@ -220,6 +222,7 @@ function CommentThreadInner({
|
|
|
220
222
|
status: "approved",
|
|
221
223
|
parentId: null,
|
|
222
224
|
currentUserId,
|
|
225
|
+
sort,
|
|
223
226
|
pageSize
|
|
224
227
|
});
|
|
225
228
|
const postMutation = usePostComment(config, {
|
|
@@ -227,7 +230,8 @@ function CommentThreadInner({
|
|
|
227
230
|
resourceType,
|
|
228
231
|
currentUserId,
|
|
229
232
|
infiniteKey: threadQueryKey,
|
|
230
|
-
pageSize
|
|
233
|
+
pageSize,
|
|
234
|
+
sort
|
|
231
235
|
});
|
|
232
236
|
const handlePost = async (body) => {
|
|
233
237
|
if (!currentUserId) return;
|
|
@@ -84,6 +84,7 @@ function useInfiniteComments(config, params, options) {
|
|
|
84
84
|
parentId: params.parentId ?? null,
|
|
85
85
|
status: params.status,
|
|
86
86
|
currentUserId: params.currentUserId,
|
|
87
|
+
sort: params.sort,
|
|
87
88
|
limit: pageSize
|
|
88
89
|
});
|
|
89
90
|
const query = reactQuery.useInfiniteQuery({
|
|
@@ -183,6 +184,7 @@ function usePostComment(config, params) {
|
|
|
183
184
|
};
|
|
184
185
|
if (isInfinitePost(input.parentId)) {
|
|
185
186
|
const previous2 = queryClient.getQueryData(listKey);
|
|
187
|
+
const isDesc = params.sort === "desc";
|
|
186
188
|
queryClient.setQueryData(
|
|
187
189
|
listKey,
|
|
188
190
|
(old) => {
|
|
@@ -199,15 +201,15 @@ function usePostComment(config, params) {
|
|
|
199
201
|
pageParams: [0]
|
|
200
202
|
};
|
|
201
203
|
}
|
|
202
|
-
const
|
|
204
|
+
const targetIdx = isDesc ? 0 : old.pages.length - 1;
|
|
203
205
|
return {
|
|
204
206
|
...old,
|
|
205
207
|
// Increment `total` on every page so the header count (which reads
|
|
206
208
|
// pages[0].total) stays in sync even after multiple pages are loaded.
|
|
207
209
|
pages: old.pages.map(
|
|
208
|
-
(page, idx) => idx ===
|
|
210
|
+
(page, idx) => idx === targetIdx ? {
|
|
209
211
|
...page,
|
|
210
|
-
items: [...page.items, optimistic],
|
|
212
|
+
items: isDesc ? [optimistic, ...page.items] : [...page.items, optimistic],
|
|
211
213
|
total: page.total + 1
|
|
212
214
|
} : { ...page, total: page.total + 1 }
|
|
213
215
|
)
|
|
@@ -82,6 +82,7 @@ function useInfiniteComments(config, params, options) {
|
|
|
82
82
|
parentId: params.parentId ?? null,
|
|
83
83
|
status: params.status,
|
|
84
84
|
currentUserId: params.currentUserId,
|
|
85
|
+
sort: params.sort,
|
|
85
86
|
limit: pageSize
|
|
86
87
|
});
|
|
87
88
|
const query = useInfiniteQuery({
|
|
@@ -181,6 +182,7 @@ function usePostComment(config, params) {
|
|
|
181
182
|
};
|
|
182
183
|
if (isInfinitePost(input.parentId)) {
|
|
183
184
|
const previous2 = queryClient.getQueryData(listKey);
|
|
185
|
+
const isDesc = params.sort === "desc";
|
|
184
186
|
queryClient.setQueryData(
|
|
185
187
|
listKey,
|
|
186
188
|
(old) => {
|
|
@@ -197,15 +199,15 @@ function usePostComment(config, params) {
|
|
|
197
199
|
pageParams: [0]
|
|
198
200
|
};
|
|
199
201
|
}
|
|
200
|
-
const
|
|
202
|
+
const targetIdx = isDesc ? 0 : old.pages.length - 1;
|
|
201
203
|
return {
|
|
202
204
|
...old,
|
|
203
205
|
// Increment `total` on every page so the header count (which reads
|
|
204
206
|
// pages[0].total) stays in sync even after multiple pages are loaded.
|
|
205
207
|
pages: old.pages.map(
|
|
206
|
-
(page, idx) => idx ===
|
|
208
|
+
(page, idx) => idx === targetIdx ? {
|
|
207
209
|
...page,
|
|
208
|
-
items: [...page.items, optimistic],
|
|
210
|
+
items: isDesc ? [optimistic, ...page.items] : [...page.items, optimistic],
|
|
209
211
|
total: page.total + 1
|
|
210
212
|
} : { ...page, total: page.total + 1 }
|
|
211
213
|
)
|