@carlonicora/nextjs-jsonapi 1.112.0 → 1.113.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/{BlockNoteEditor-HNQNKCFP.js → BlockNoteEditor-DOAPXS7S.js} +9 -9
- package/dist/{BlockNoteEditor-HNQNKCFP.js.map → BlockNoteEditor-DOAPXS7S.js.map} +1 -1
- package/dist/{BlockNoteEditor-A5LNFWHW.mjs → BlockNoteEditor-F3HSL4LX.mjs} +2 -2
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-LYLGU2BK.mjs → chunk-CI36BXIG.mjs} +2940 -2837
- package/dist/chunk-CI36BXIG.mjs.map +1 -0
- package/dist/{chunk-DRJUN4R6.js → chunk-UBFVV2DA.js} +203 -100
- package/dist/chunk-UBFVV2DA.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +33 -2
- package/dist/components/index.d.ts +33 -2
- package/dist/components/index.js +10 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +9 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/features/help/index.js +30 -30
- package/dist/features/help/index.mjs +1 -1
- package/dist/scripts/generate-web-module/__tests__/selector.spec.js +19 -14
- package/dist/scripts/generate-web-module/__tests__/selector.spec.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/content.template.js +7 -7
- package/dist/scripts/generate-web-module/templates/components/content.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts +4 -0
- package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/selector.template.js +55 -48
- package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/module.template.js +1 -1
- package/dist/scripts/generate-web-module/templates/module.template.js.map +1 -1
- package/dist/scripts/generate-web-module/templates/table-hook.template.js +1 -1
- package/dist/scripts/generate-web-module/templates/table-hook.template.js.map +1 -1
- package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/transformers/i18n-generator.js +1 -0
- package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -1
- package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/utils/i18n-updater.js +25 -26
- package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-web-module/__tests__/selector.spec.ts +19 -14
- package/scripts/generate-web-module/templates/components/content.template.ts +7 -7
- package/scripts/generate-web-module/templates/components/selector.template.ts +55 -48
- package/scripts/generate-web-module/templates/module.template.ts +1 -1
- package/scripts/generate-web-module/templates/table-hook.template.ts +1 -1
- package/scripts/generate-web-module/transformers/i18n-generator.ts +1 -0
- package/scripts/generate-web-module/utils/i18n-updater.ts +25 -30
- package/src/components/tables/ContentListTable.tsx +2 -1
- package/src/shadcnui/index.ts +3 -0
- package/src/shadcnui/ui/__tests__/confirm-dialog.test.tsx +46 -0
- package/src/shadcnui/ui/__tests__/empty-state.test.tsx +26 -0
- package/src/shadcnui/ui/__tests__/spinner.test.tsx +18 -0
- package/src/shadcnui/ui/confirm-dialog.tsx +79 -0
- package/src/shadcnui/ui/empty-state.tsx +32 -0
- package/src/shadcnui/ui/spinner.tsx +36 -0
- package/dist/chunk-DRJUN4R6.js.map +0 -1
- package/dist/chunk-LYLGU2BK.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-A5LNFWHW.mjs.map → BlockNoteEditor-F3HSL4LX.mjs.map} +0 -0
|
@@ -86,45 +86,44 @@ function updateI18n(data, webBasePath, language = "en", dryRun = false) {
|
|
|
86
86
|
messages.entities[lowercasePluralKey] = moduleMessages.entities[entitiesKey];
|
|
87
87
|
entitiesUpdated = true;
|
|
88
88
|
}
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
89
|
+
// Additively merge this module's feature keys (fields / relationships /
|
|
90
|
+
// sections), FILLING missing keys without overwriting existing translations.
|
|
91
|
+
// Handles a brand-new module AND an existing-but-incomplete block (e.g. one
|
|
92
|
+
// first generated before its fields were known — which previously stayed an
|
|
93
|
+
// empty `{ fields: {}, relationships: {} }` skeleton forever).
|
|
94
|
+
const mergeMissing = (target, source) => {
|
|
95
|
+
const out = target && typeof target === "object" && !Array.isArray(target) ? { ...target } : {};
|
|
96
|
+
for (const k of Object.keys(source)) {
|
|
97
|
+
const v = source[k];
|
|
98
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
99
|
+
out[k] = mergeMissing(out[k], v);
|
|
100
|
+
}
|
|
101
|
+
else if (out[k] === undefined) {
|
|
102
|
+
out[k] = v;
|
|
100
103
|
}
|
|
101
|
-
// Write updated content (entities were added)
|
|
102
|
-
const updatedContent = JSON.stringify(messages, null, 2);
|
|
103
|
-
fs.writeFileSync(messagesPath, updatedContent, "utf-8");
|
|
104
|
-
return {
|
|
105
|
-
success: true,
|
|
106
|
-
message: `Module ${names.camelCase} exists, added entities.${lowercasePluralKey}`,
|
|
107
|
-
alreadyExists: true,
|
|
108
|
-
};
|
|
109
104
|
}
|
|
105
|
+
return out;
|
|
106
|
+
};
|
|
107
|
+
if (!messages.features) {
|
|
108
|
+
messages.features = {};
|
|
109
|
+
}
|
|
110
|
+
const moduleBlock = moduleMessages.features[i18nKeys.moduleName];
|
|
111
|
+
const beforeFeatures = JSON.stringify(messages.features[lowercaseModuleName] ?? null);
|
|
112
|
+
messages.features[lowercaseModuleName] = mergeMissing(messages.features[lowercaseModuleName], moduleBlock);
|
|
113
|
+
const featuresUpdated = JSON.stringify(messages.features[lowercaseModuleName]) !== beforeFeatures;
|
|
114
|
+
if (!entitiesUpdated && !featuresUpdated) {
|
|
110
115
|
return {
|
|
111
116
|
success: true,
|
|
112
|
-
message: `Module ${names.camelCase} already
|
|
117
|
+
message: `Module ${names.camelCase} already up to date in messages/${language}.json`,
|
|
113
118
|
alreadyExists: true,
|
|
114
119
|
};
|
|
115
120
|
}
|
|
116
|
-
// Add to features section (new module)
|
|
117
|
-
if (!messages.features) {
|
|
118
|
-
messages.features = {};
|
|
119
|
-
}
|
|
120
|
-
messages.features[lowercaseModuleName] = moduleMessages.features[i18nKeys.moduleName];
|
|
121
121
|
if (dryRun) {
|
|
122
122
|
return {
|
|
123
123
|
success: true,
|
|
124
124
|
message: `[DRY RUN] Would update messages/${language}.json with ${names.camelCase} translations`,
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
|
-
// Write updated content with proper formatting
|
|
128
127
|
const updatedContent = JSON.stringify(messages, null, 2);
|
|
129
128
|
fs.writeFileSync(messagesPath, updatedContent, "utf-8");
|
|
130
129
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n-updater.js","sourceRoot":"","sources":["../../../../scripts/generate-web-module/utils/i18n-updater.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqBH,
|
|
1
|
+
{"version":3,"file":"i18n-updater.js","sourceRoot":"","sources":["../../../../scripts/generate-web-module/utils/i18n-updater.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -2,23 +2,28 @@ import { describe, it, expect } from "vitest";
|
|
|
2
2
|
import { makeFrontendData } from "./fixtures";
|
|
3
3
|
import { generateSelectorTemplate } from "../templates/components/selector.template";
|
|
4
4
|
|
|
5
|
-
describe("selector template —
|
|
6
|
-
it("uses
|
|
5
|
+
describe("selector template — UserSelector-style UI", () => {
|
|
6
|
+
it("uses the input-styled trigger + custom search Input (mirrors UserSelector)", () => {
|
|
7
7
|
const out = generateSelectorTemplate(makeFrontendData());
|
|
8
|
-
expect(out).toContain(
|
|
9
|
-
expect(out).toContain("
|
|
10
|
-
expect(out).toContain("
|
|
11
|
-
expect(out).toContain("
|
|
12
|
-
expect(out).toContain("
|
|
13
|
-
expect(out).toContain("
|
|
14
|
-
|
|
8
|
+
expect(out).toContain("CircleX");
|
|
9
|
+
expect(out).toContain("SearchIcon");
|
|
10
|
+
expect(out).toContain("RefreshCwIcon");
|
|
11
|
+
expect(out).toContain("bg-input/20");
|
|
12
|
+
expect(out).toContain("w-(--anchor-width)");
|
|
13
|
+
expect(out).toContain("ui.search.placeholder");
|
|
14
|
+
// custom <Input> search, not the cmdk CommandInput
|
|
15
|
+
expect(out).toContain("<Input");
|
|
16
|
+
expect(out).toContain("CommandList");
|
|
17
|
+
expect(out).toContain("CommandItem");
|
|
15
18
|
});
|
|
16
19
|
|
|
17
|
-
it("drops the legacy
|
|
20
|
+
it("drops the legacy Button-combobox styling", () => {
|
|
18
21
|
const out = generateSelectorTemplate(makeFrontendData());
|
|
19
|
-
expect(out).not.toContain("
|
|
20
|
-
expect(out).not.toContain("
|
|
21
|
-
expect(out).not.toContain("
|
|
22
|
-
expect(out).not.toContain("
|
|
22
|
+
expect(out).not.toContain('role="combobox"');
|
|
23
|
+
expect(out).not.toContain("ChevronsUpDown");
|
|
24
|
+
expect(out).not.toContain("CommandInput");
|
|
25
|
+
expect(out).not.toContain("CommandEmpty");
|
|
26
|
+
expect(out).not.toContain("CommandGroup");
|
|
27
|
+
expect(out).not.toContain("w-(--radix-popover-trigger-width)");
|
|
23
28
|
});
|
|
24
29
|
});
|
|
@@ -53,7 +53,7 @@ export function ${names.pascalCase}Content({ ${camel} }: ${names.pascalCase}Cont
|
|
|
53
53
|
.map(
|
|
54
54
|
(field) => ` <div className="flex flex-col gap-y-3">
|
|
55
55
|
<h3 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
56
|
-
{t(\`features.${names.camelCase}.fields.${field.name}.label\`)}
|
|
56
|
+
{t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.label\`)}
|
|
57
57
|
</h3>
|
|
58
58
|
<Card className="flex w-full flex-col p-4">
|
|
59
59
|
<BlockNoteEditorContainer id={${camel}.id} type="${camel}" initialContent={${camel}.${field.name}} />
|
|
@@ -65,7 +65,7 @@ export function ${names.pascalCase}Content({ ${camel} }: ${names.pascalCase}Cont
|
|
|
65
65
|
const attributesBlock = hasAttributes
|
|
66
66
|
? `\n <div className="flex flex-col gap-y-3">
|
|
67
67
|
<h3 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
68
|
-
{t(\`features.${names.camelCase}.sections.details\`)}
|
|
68
|
+
{t(\`features.${names.camelCase.toLowerCase()}.sections.details\`)}
|
|
69
69
|
</h3>
|
|
70
70
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
71
71
|
${attributeElements}
|
|
@@ -113,7 +113,7 @@ export function ${names.pascalCase}Content({ ${camel} }: ${names.pascalCase}Cont
|
|
|
113
113
|
<div className="flex flex-col gap-y-8">
|
|
114
114
|
<div className="flex flex-col gap-y-3">
|
|
115
115
|
<h3 className="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
116
|
-
{t(\`features.${names.camelCase}.sections.details\`)}
|
|
116
|
+
{t(\`features.${names.camelCase.toLowerCase()}.sections.details\`)}
|
|
117
117
|
</h3>
|
|
118
118
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
119
119
|
${attributeElements}
|
|
@@ -145,17 +145,17 @@ function generateAttributeElements(data: FrontendTemplateData): string {
|
|
|
145
145
|
displayableFields.forEach((field) => {
|
|
146
146
|
if (field.type === "string") {
|
|
147
147
|
elements.push(` <AttributeElement
|
|
148
|
-
title={t(\`features.${names.camelCase}.fields.${field.name}.label\`)}
|
|
148
|
+
title={t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.label\`)}
|
|
149
149
|
value={${camel}.${field.name}}
|
|
150
150
|
/>`);
|
|
151
151
|
} else if (field.type === "boolean") {
|
|
152
152
|
elements.push(` <AttributeElement
|
|
153
|
-
title={t(\`features.${names.camelCase}.fields.${field.name}.label\`)}
|
|
153
|
+
title={t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.label\`)}
|
|
154
154
|
value={${camel}.${field.name} ? t(\`generic.yes\`) : t(\`generic.no\`)}
|
|
155
155
|
/>`);
|
|
156
156
|
} else if (field.type === "number") {
|
|
157
157
|
elements.push(` <AttributeElement
|
|
158
|
-
title={t(\`features.${names.camelCase}.fields.${field.name}.label\`)}
|
|
158
|
+
title={t(\`features.${names.camelCase.toLowerCase()}.fields.${field.name}.label\`)}
|
|
159
159
|
value={${camel}.${field.name}?.toString()}
|
|
160
160
|
/>`);
|
|
161
161
|
}
|
|
@@ -170,7 +170,7 @@ function generateAttributeElements(data: FrontendTemplateData): string {
|
|
|
170
170
|
const propName = toCamelCase(rel.alias || rel.variant || rel.name);
|
|
171
171
|
elements.push(` {${camel}.${propName} && (
|
|
172
172
|
<AttributeElement
|
|
173
|
-
title={t(\`
|
|
173
|
+
title={t(\`features.${names.camelCase.toLowerCase()}.relationships.${propName.toLowerCase()}.label\`)}
|
|
174
174
|
value={${camel}.${propName}.${displayProp}}
|
|
175
175
|
/>
|
|
176
176
|
)}`);
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* Selector Template
|
|
3
3
|
*
|
|
4
4
|
* Generates {Module}Selector.tsx searchable combobox component.
|
|
5
|
+
*
|
|
6
|
+
* UI mirrors the framework's UserSelector (packages/.../features/user/components/forms/UserSelector.tsx):
|
|
7
|
+
* an input-styled trigger, a CircleX clear affordance, an `--anchor-width` popover, and a custom
|
|
8
|
+
* search Input with a SearchIcon + spinner/clear — minus the user-specific avatar.
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
11
|
import { FrontendTemplateData } from "../../types/template-data.interface";
|
|
@@ -21,25 +25,21 @@ export function generateSelectorTemplate(data: FrontendTemplateData): string {
|
|
|
21
25
|
return `"use client";
|
|
22
26
|
|
|
23
27
|
import {
|
|
24
|
-
Button,
|
|
25
28
|
Command,
|
|
26
|
-
CommandEmpty,
|
|
27
|
-
CommandGroup,
|
|
28
|
-
CommandInput,
|
|
29
29
|
CommandItem,
|
|
30
30
|
CommandList,
|
|
31
31
|
FormFieldWrapper,
|
|
32
|
+
Input,
|
|
32
33
|
Popover,
|
|
33
34
|
PopoverContent,
|
|
34
35
|
PopoverTrigger,
|
|
35
36
|
} from "@carlonicora/nextjs-jsonapi/components";
|
|
36
37
|
import { ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
|
|
37
38
|
import { ${names.pascalCase}Service } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
|
|
38
|
-
import { DataListRetriever, useDataListRetriever } from "@carlonicora/nextjs-jsonapi/client";
|
|
39
|
-
import { useDebounce } from "@carlonicora/nextjs-jsonapi/client";
|
|
39
|
+
import { DataListRetriever, useDataListRetriever, useDebounce } from "@carlonicora/nextjs-jsonapi/client";
|
|
40
40
|
import { Modules } from "@carlonicora/nextjs-jsonapi/core";
|
|
41
41
|
|
|
42
|
-
import {
|
|
42
|
+
import { CircleX, RefreshCwIcon, SearchIcon, XIcon } from "lucide-react";
|
|
43
43
|
import { useTranslations } from "next-intl";
|
|
44
44
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
45
45
|
|
|
@@ -121,55 +121,62 @@ export default function ${names.pascalCase}Selector({
|
|
|
121
121
|
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
|
122
122
|
<div className="flex w-full flex-row items-center justify-between">
|
|
123
123
|
<PopoverTrigger className="w-full">
|
|
124
|
-
<
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</
|
|
124
|
+
<div className="flex w-full flex-row items-center justify-start rounded-md">
|
|
125
|
+
{field.value ? (
|
|
126
|
+
<div className="bg-input/20 dark:bg-input/30 border-input flex h-7 w-full flex-row items-center justify-start rounded-md border px-2 py-0.5 text-sm md:text-xs/relaxed">
|
|
127
|
+
<span>{field.value?.name ?? ""}</span>
|
|
128
|
+
</div>
|
|
129
|
+
) : (
|
|
130
|
+
<div className="bg-input/20 dark:bg-input/30 border-input text-muted-foreground flex h-7 w-full flex-row items-center justify-start rounded-md border px-2 py-0.5 text-sm md:text-xs/relaxed">
|
|
131
|
+
{placeholder ?? t(\`ui.search.placeholder\`, { type: t(\`entities.${i18nKey}\`, { count: 1 }) })}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
135
|
</PopoverTrigger>
|
|
136
136
|
{field.value && (
|
|
137
|
-
<
|
|
138
|
-
className="text-muted
|
|
137
|
+
<CircleX
|
|
138
|
+
className="text-muted hover:text-destructive ml-2 h-4 w-4 shrink-0 cursor-pointer"
|
|
139
139
|
onClick={() => set${names.pascalCase}()}
|
|
140
140
|
/>
|
|
141
141
|
)}
|
|
142
142
|
</div>
|
|
143
|
-
<PopoverContent className="w-(--
|
|
143
|
+
<PopoverContent align="start" className="w-(--anchor-width)">
|
|
144
144
|
<Command shouldFilter={false}>
|
|
145
|
-
<
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
<
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
className="hover:bg-muted data-selected:hover:bg-muted bg-transparent data-selected:bg-transparent cursor-pointer"
|
|
167
|
-
>
|
|
168
|
-
{${names.camelCase}.${displayProp}}
|
|
169
|
-
</CommandItem>
|
|
170
|
-
))}
|
|
171
|
-
</CommandGroup>
|
|
145
|
+
<div className="relative mb-2 w-full">
|
|
146
|
+
<SearchIcon className="text-muted-foreground absolute top-2.5 left-2.5 h-4 w-4" />
|
|
147
|
+
<Input
|
|
148
|
+
placeholder={t(\`ui.search.placeholder\`, { type: t(\`entities.${i18nKey}\`, { count: 1 }) })}
|
|
149
|
+
type="text"
|
|
150
|
+
className="w-full pr-8 pl-8"
|
|
151
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
152
|
+
value={searchTerm}
|
|
153
|
+
/>
|
|
154
|
+
{isSearching ? (
|
|
155
|
+
<RefreshCwIcon className="text-muted-foreground absolute top-2.5 right-2.5 h-4 w-4 animate-spin" />
|
|
156
|
+
) : searchTermRef.current ? (
|
|
157
|
+
<XIcon
|
|
158
|
+
className={\`absolute top-2.5 right-2.5 h-4 w-4 \${searchTermRef.current ? "cursor-pointer" : "text-muted-foreground"}\`}
|
|
159
|
+
onClick={() => {
|
|
160
|
+
setSearchTerm("");
|
|
161
|
+
search("");
|
|
162
|
+
}}
|
|
163
|
+
/>
|
|
164
|
+
) : (
|
|
165
|
+
<></>
|
|
172
166
|
)}
|
|
167
|
+
</div>
|
|
168
|
+
<CommandList>
|
|
169
|
+
{data.data &&
|
|
170
|
+
data.data.length > 0 &&
|
|
171
|
+
(data.data as ${names.pascalCase}Interface[]).map((${names.camelCase}: ${names.pascalCase}Interface) => (
|
|
172
|
+
<CommandItem
|
|
173
|
+
className="cursor-pointer hover:bg-muted data-selected:hover:bg-muted bg-transparent data-selected:bg-transparent"
|
|
174
|
+
key={${names.camelCase}.id}
|
|
175
|
+
onSelect={() => set${names.pascalCase}(${names.camelCase})}
|
|
176
|
+
>
|
|
177
|
+
<span className="">{${names.camelCase}.${displayProp}}</span>
|
|
178
|
+
</CommandItem>
|
|
179
|
+
))}
|
|
173
180
|
</CommandList>
|
|
174
181
|
</Command>
|
|
175
182
|
</PopoverContent>
|
|
@@ -42,7 +42,7 @@ ${featureImport}
|
|
|
42
42
|
export const ${names.pascalCase}Module = (factory: ModuleFactory) =>
|
|
43
43
|
factory({
|
|
44
44
|
pageUrl: "/${names.pluralKebab}",
|
|
45
|
-
name: "${
|
|
45
|
+
name: "${endpoint}",
|
|
46
46
|
model: ${names.pascalCase},
|
|
47
47
|
moduleId: "${moduleId}",
|
|
48
48
|
icon: ${DEFAULT_MODULE_ICON},
|
|
@@ -205,7 +205,7 @@ function generateFieldColumnEntries(data: FrontendTemplateData): string {
|
|
|
205
205
|
entries.push(` [${names.pascalCase}Fields.${key}]: () => ({
|
|
206
206
|
id: "${key}",
|
|
207
207
|
accessorKey: "${key}",
|
|
208
|
-
header: t(\`
|
|
208
|
+
header: t(\`features.${names.camelCase.toLowerCase()}.relationships.${key.toLowerCase()}.label\`),
|
|
209
209
|
cell: ({ row }: { row: TableContent<${names.pascalCase}Interface> }) => {
|
|
210
210
|
const ${names.camelCase}: ${names.pascalCase}Interface = row.original.jsonApiData;
|
|
211
211
|
return <span>{${accessor}}</span>;
|
|
@@ -68,44 +68,40 @@ export function updateI18n(
|
|
|
68
68
|
entitiesUpdated = true;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
// Additively merge this module's feature keys (fields / relationships /
|
|
72
|
+
// sections), FILLING missing keys without overwriting existing translations.
|
|
73
|
+
// Handles a brand-new module AND an existing-but-incomplete block (e.g. one
|
|
74
|
+
// first generated before its fields were known — which previously stayed an
|
|
75
|
+
// empty `{ fields: {}, relationships: {} }` skeleton forever).
|
|
76
|
+
const mergeMissing = (target: Record<string, any>, source: Record<string, any>): Record<string, any> => {
|
|
77
|
+
const out = target && typeof target === "object" && !Array.isArray(target) ? { ...target } : {};
|
|
78
|
+
for (const k of Object.keys(source)) {
|
|
79
|
+
const v = source[k];
|
|
80
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
81
|
+
out[k] = mergeMissing(out[k], v);
|
|
82
|
+
} else if (out[k] === undefined) {
|
|
83
|
+
out[k] = v;
|
|
83
84
|
}
|
|
84
|
-
|
|
85
|
-
// Write updated content (entities were added)
|
|
86
|
-
const updatedContent = JSON.stringify(messages, null, 2);
|
|
87
|
-
fs.writeFileSync(messagesPath, updatedContent, "utf-8");
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
success: true,
|
|
91
|
-
message: `Module ${names.camelCase} exists, added entities.${lowercasePluralKey}`,
|
|
92
|
-
alreadyExists: true,
|
|
93
|
-
};
|
|
94
85
|
}
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (!messages.features) {
|
|
90
|
+
messages.features = {};
|
|
91
|
+
}
|
|
92
|
+
const moduleBlock = moduleMessages.features[i18nKeys.moduleName];
|
|
93
|
+
const beforeFeatures = JSON.stringify(messages.features[lowercaseModuleName] ?? null);
|
|
94
|
+
messages.features[lowercaseModuleName] = mergeMissing(messages.features[lowercaseModuleName], moduleBlock);
|
|
95
|
+
const featuresUpdated = JSON.stringify(messages.features[lowercaseModuleName]) !== beforeFeatures;
|
|
95
96
|
|
|
97
|
+
if (!entitiesUpdated && !featuresUpdated) {
|
|
96
98
|
return {
|
|
97
99
|
success: true,
|
|
98
|
-
message: `Module ${names.camelCase} already
|
|
100
|
+
message: `Module ${names.camelCase} already up to date in messages/${language}.json`,
|
|
99
101
|
alreadyExists: true,
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
// Add to features section (new module)
|
|
104
|
-
if (!messages.features) {
|
|
105
|
-
messages.features = {};
|
|
106
|
-
}
|
|
107
|
-
messages.features[lowercaseModuleName] = moduleMessages.features[i18nKeys.moduleName];
|
|
108
|
-
|
|
109
105
|
if (dryRun) {
|
|
110
106
|
return {
|
|
111
107
|
success: true,
|
|
@@ -113,7 +109,6 @@ export function updateI18n(
|
|
|
113
109
|
};
|
|
114
110
|
}
|
|
115
111
|
|
|
116
|
-
// Write updated content with proper formatting
|
|
117
112
|
const updatedContent = JSON.stringify(messages, null, 2);
|
|
118
113
|
fs.writeFileSync(messagesPath, updatedContent, "utf-8");
|
|
119
114
|
|
|
@@ -47,6 +47,7 @@ type ContentListTableProps = {
|
|
|
47
47
|
defaultExpanded?: boolean | ExpandedState;
|
|
48
48
|
fullWidth?: boolean;
|
|
49
49
|
groupBy?: string;
|
|
50
|
+
emptyState?: ReactNode;
|
|
50
51
|
};
|
|
51
52
|
|
|
52
53
|
export const ContentListTable = memo(function ContentListTable(props: ContentListTableProps) {
|
|
@@ -219,7 +220,7 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
|
|
|
219
220
|
) : (
|
|
220
221
|
<TableRow>
|
|
221
222
|
<TableCell colSpan={tableColumns.length} className="h-24 text-center">
|
|
222
|
-
No results.
|
|
223
|
+
{props.emptyState ?? "No results."}
|
|
223
224
|
</TableCell>
|
|
224
225
|
</TableRow>
|
|
225
226
|
)}
|
package/src/shadcnui/index.ts
CHANGED
|
@@ -16,10 +16,12 @@ export * from "./ui/checkbox";
|
|
|
16
16
|
export * from "./ui/collapsible";
|
|
17
17
|
export * from "./ui/combobox";
|
|
18
18
|
export * from "./ui/command";
|
|
19
|
+
export * from "./ui/confirm-dialog";
|
|
19
20
|
export * from "./ui/context-menu";
|
|
20
21
|
export * from "./ui/dialog";
|
|
21
22
|
export * from "./ui/drawer";
|
|
22
23
|
export * from "./ui/dropdown-menu";
|
|
24
|
+
export * from "./ui/empty-state";
|
|
23
25
|
export * from "./ui/field";
|
|
24
26
|
export * from "./ui/form";
|
|
25
27
|
export * from "./ui/hover-card";
|
|
@@ -40,6 +42,7 @@ export * from "./ui/sidebar";
|
|
|
40
42
|
export * from "./ui/skeleton";
|
|
41
43
|
export * from "./ui/slider";
|
|
42
44
|
export * from "./ui/sonner";
|
|
45
|
+
export * from "./ui/spinner";
|
|
43
46
|
export * from "./ui/switch";
|
|
44
47
|
export * from "./ui/table";
|
|
45
48
|
export * from "./ui/tabs";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// packages/nextjs-jsonapi/src/shadcnui/ui/__tests__/confirm-dialog.test.tsx
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import userEvent from "@testing-library/user-event";
|
|
5
|
+
import { ConfirmDialog } from "../confirm-dialog";
|
|
6
|
+
|
|
7
|
+
describe("ConfirmDialog", () => {
|
|
8
|
+
it("renders title/description and calls onConfirm then closes", async () => {
|
|
9
|
+
const onConfirm = vi.fn();
|
|
10
|
+
const onOpenChange = vi.fn();
|
|
11
|
+
render(
|
|
12
|
+
<ConfirmDialog
|
|
13
|
+
open
|
|
14
|
+
onOpenChange={onOpenChange}
|
|
15
|
+
title="Reveal plot secrets?"
|
|
16
|
+
description="Spoilers ahead."
|
|
17
|
+
confirmLabel="Reveal"
|
|
18
|
+
cancelLabel="Keep hidden"
|
|
19
|
+
destructive
|
|
20
|
+
onConfirm={onConfirm}
|
|
21
|
+
/>,
|
|
22
|
+
);
|
|
23
|
+
expect(screen.getByText("Reveal plot secrets?")).toBeInTheDocument();
|
|
24
|
+
expect(screen.getByText("Spoilers ahead.")).toBeInTheDocument();
|
|
25
|
+
await userEvent.click(screen.getByRole("button", { name: "Reveal" }));
|
|
26
|
+
expect(onConfirm).toHaveBeenCalledTimes(1);
|
|
27
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("cancel closes without confirming", async () => {
|
|
31
|
+
const onConfirm = vi.fn();
|
|
32
|
+
const onOpenChange = vi.fn();
|
|
33
|
+
render(
|
|
34
|
+
<ConfirmDialog
|
|
35
|
+
open
|
|
36
|
+
onOpenChange={onOpenChange}
|
|
37
|
+
title="t"
|
|
38
|
+
confirmLabel="Yes"
|
|
39
|
+
cancelLabel="No"
|
|
40
|
+
onConfirm={onConfirm}
|
|
41
|
+
/>,
|
|
42
|
+
);
|
|
43
|
+
await userEvent.click(screen.getByRole("button", { name: "No" }));
|
|
44
|
+
expect(onConfirm).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// packages/nextjs-jsonapi/src/shadcnui/ui/__tests__/empty-state.test.tsx
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { InboxIcon } from "lucide-react";
|
|
5
|
+
import { EmptyState } from "../empty-state";
|
|
6
|
+
|
|
7
|
+
describe("EmptyState", () => {
|
|
8
|
+
it("renders title, description and action", () => {
|
|
9
|
+
render(
|
|
10
|
+
<EmptyState
|
|
11
|
+
icon={InboxIcon}
|
|
12
|
+
title="No games yet"
|
|
13
|
+
description="Create your first game."
|
|
14
|
+
action={<button type="button">New game</button>}
|
|
15
|
+
/>,
|
|
16
|
+
);
|
|
17
|
+
expect(screen.getByText("No games yet")).toBeInTheDocument();
|
|
18
|
+
expect(screen.getByText("Create your first game.")).toBeInTheDocument();
|
|
19
|
+
expect(screen.getByRole("button", { name: "New game" })).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("renders without optional parts", () => {
|
|
23
|
+
render(<EmptyState title="Empty" />);
|
|
24
|
+
expect(screen.getByText("Empty")).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// packages/nextjs-jsonapi/src/shadcnui/ui/__tests__/spinner.test.tsx
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { render, screen } from "@testing-library/react";
|
|
4
|
+
import { Spinner } from "../spinner";
|
|
5
|
+
|
|
6
|
+
describe("Spinner", () => {
|
|
7
|
+
it("renders a status indicator with animate-spin", () => {
|
|
8
|
+
render(<Spinner />);
|
|
9
|
+
const spinner = screen.getByRole("status");
|
|
10
|
+
expect(spinner).toHaveClass("animate-spin");
|
|
11
|
+
expect(spinner).toHaveClass("size-4");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("applies the size variant", () => {
|
|
15
|
+
render(<Spinner size="lg" />);
|
|
16
|
+
expect(screen.getByRole("status")).toHaveClass("size-8");
|
|
17
|
+
});
|
|
18
|
+
});
|