@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.
Files changed (57) hide show
  1. package/dist/{BlockNoteEditor-HNQNKCFP.js → BlockNoteEditor-DOAPXS7S.js} +9 -9
  2. package/dist/{BlockNoteEditor-HNQNKCFP.js.map → BlockNoteEditor-DOAPXS7S.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-A5LNFWHW.mjs → BlockNoteEditor-F3HSL4LX.mjs} +2 -2
  4. package/dist/billing/index.js +299 -299
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-LYLGU2BK.mjs → chunk-CI36BXIG.mjs} +2940 -2837
  7. package/dist/chunk-CI36BXIG.mjs.map +1 -0
  8. package/dist/{chunk-DRJUN4R6.js → chunk-UBFVV2DA.js} +203 -100
  9. package/dist/chunk-UBFVV2DA.js.map +1 -0
  10. package/dist/client/index.js +2 -2
  11. package/dist/client/index.mjs +1 -1
  12. package/dist/components/index.d.mts +33 -2
  13. package/dist/components/index.d.ts +33 -2
  14. package/dist/components/index.js +10 -2
  15. package/dist/components/index.js.map +1 -1
  16. package/dist/components/index.mjs +9 -1
  17. package/dist/contexts/index.js +2 -2
  18. package/dist/contexts/index.mjs +1 -1
  19. package/dist/features/help/index.js +30 -30
  20. package/dist/features/help/index.mjs +1 -1
  21. package/dist/scripts/generate-web-module/__tests__/selector.spec.js +19 -14
  22. package/dist/scripts/generate-web-module/__tests__/selector.spec.js.map +1 -1
  23. package/dist/scripts/generate-web-module/templates/components/content.template.js +7 -7
  24. package/dist/scripts/generate-web-module/templates/components/content.template.js.map +1 -1
  25. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts +4 -0
  26. package/dist/scripts/generate-web-module/templates/components/selector.template.d.ts.map +1 -1
  27. package/dist/scripts/generate-web-module/templates/components/selector.template.js +55 -48
  28. package/dist/scripts/generate-web-module/templates/components/selector.template.js.map +1 -1
  29. package/dist/scripts/generate-web-module/templates/module.template.js +1 -1
  30. package/dist/scripts/generate-web-module/templates/module.template.js.map +1 -1
  31. package/dist/scripts/generate-web-module/templates/table-hook.template.js +1 -1
  32. package/dist/scripts/generate-web-module/templates/table-hook.template.js.map +1 -1
  33. package/dist/scripts/generate-web-module/transformers/i18n-generator.d.ts.map +1 -1
  34. package/dist/scripts/generate-web-module/transformers/i18n-generator.js +1 -0
  35. package/dist/scripts/generate-web-module/transformers/i18n-generator.js.map +1 -1
  36. package/dist/scripts/generate-web-module/utils/i18n-updater.d.ts.map +1 -1
  37. package/dist/scripts/generate-web-module/utils/i18n-updater.js +25 -26
  38. package/dist/scripts/generate-web-module/utils/i18n-updater.js.map +1 -1
  39. package/package.json +1 -1
  40. package/scripts/generate-web-module/__tests__/selector.spec.ts +19 -14
  41. package/scripts/generate-web-module/templates/components/content.template.ts +7 -7
  42. package/scripts/generate-web-module/templates/components/selector.template.ts +55 -48
  43. package/scripts/generate-web-module/templates/module.template.ts +1 -1
  44. package/scripts/generate-web-module/templates/table-hook.template.ts +1 -1
  45. package/scripts/generate-web-module/transformers/i18n-generator.ts +1 -0
  46. package/scripts/generate-web-module/utils/i18n-updater.ts +25 -30
  47. package/src/components/tables/ContentListTable.tsx +2 -1
  48. package/src/shadcnui/index.ts +3 -0
  49. package/src/shadcnui/ui/__tests__/confirm-dialog.test.tsx +46 -0
  50. package/src/shadcnui/ui/__tests__/empty-state.test.tsx +26 -0
  51. package/src/shadcnui/ui/__tests__/spinner.test.tsx +18 -0
  52. package/src/shadcnui/ui/confirm-dialog.tsx +79 -0
  53. package/src/shadcnui/ui/empty-state.tsx +32 -0
  54. package/src/shadcnui/ui/spinner.tsx +36 -0
  55. package/dist/chunk-DRJUN4R6.js.map +0 -1
  56. package/dist/chunk-LYLGU2BK.mjs.map +0 -1
  57. /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
- // Check if module already exists in features
90
- const featuresAlreadyExist = messages.features && messages.features[lowercaseModuleName];
91
- if (featuresAlreadyExist) {
92
- // Features exist, but we may have added entities
93
- if (entitiesUpdated) {
94
- if (dryRun) {
95
- return {
96
- success: true,
97
- message: `[DRY RUN] Module ${names.camelCase} exists, would add entities.${lowercasePluralKey}`,
98
- alreadyExists: true,
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 exists in messages/${language}.json`,
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":"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"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.112.0",
3
+ "version": "1.113.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -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 — combobox style", () => {
6
- it("uses Button combobox trigger + Command primitives + Loader2", () => {
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('role="combobox"');
9
- expect(out).toContain("ChevronsUpDown");
10
- expect(out).toContain("CommandInput");
11
- expect(out).toContain("CommandEmpty");
12
- expect(out).toContain("CommandGroup");
13
- expect(out).toContain("Loader2");
14
- expect(out).toContain("w-(--radix-popover-trigger-width) p-0");
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 bespoke styling", () => {
20
+ it("drops the legacy Button-combobox styling", () => {
18
21
  const out = generateSelectorTemplate(makeFrontendData());
19
- expect(out).not.toContain("CircleX");
20
- expect(out).not.toContain("RefreshCwIcon");
21
- expect(out).not.toContain("SearchIcon");
22
- expect(out).not.toContain("bg-input/20");
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(\`generic.relationships.${propName}.label\`)}
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 { ChevronsUpDown, Loader2, XIcon } from "lucide-react";
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
- <Button
125
- variant="outline"
126
- role="combobox"
127
- aria-expanded={open}
128
- className="h-auto min-h-10 w-full justify-between px-3 py-2"
129
- >
130
- <span className={field.value ? "" : "text-muted-foreground"}>
131
- {field.value?.name ?? placeholder ?? t(\`generic.search.placeholder\`, { type: t(\`entities.${i18nKey}\`, { count: 1 }) })}
132
- </span>
133
- <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
134
- </Button>
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
- <XIcon
138
- className="text-muted-foreground hover:text-destructive ml-2 h-5 w-5 cursor-pointer"
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-(--radix-popover-trigger-width) p-0" align="start">
143
+ <PopoverContent align="start" className="w-(--anchor-width)">
144
144
  <Command shouldFilter={false}>
145
- <CommandInput
146
- placeholder={t(\`generic.search.placeholder\`, { type: t(\`entities.${i18nKey}\`, { count: 1 }) })}
147
- value={searchTerm}
148
- onValueChange={setSearchTerm}
149
- />
150
- <CommandList>
151
- {isSearching && (
152
- <div className="flex items-center justify-center py-6">
153
- <Loader2 className="text-muted-foreground h-4 w-4 animate-spin" />
154
- </div>
155
- )}
156
- {!isSearching && data.data && data.data.length === 0 && (
157
- <CommandEmpty>{t(\`generic.search.no_results\`)}</CommandEmpty>
158
- )}
159
- {!isSearching && data.data && data.data.length > 0 && (
160
- <CommandGroup>
161
- {(data.data as ${names.pascalCase}Interface[]).map((${names.camelCase}: ${names.pascalCase}Interface) => (
162
- <CommandItem
163
- key={${names.camelCase}.id}
164
- value={${names.camelCase}.id}
165
- onSelect={() => set${names.pascalCase}(${names.camelCase})}
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: "${names.pluralCamel}",
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(\`generic.relationships.${key}.label\`),
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>;
@@ -88,6 +88,7 @@ export function buildI18nMessages(i18nKeys: I18nKeySet): Record<string, any> {
88
88
  [i18nKeys.moduleName]: {
89
89
  fields: i18nKeys.fields,
90
90
  relationships: i18nKeys.relationships,
91
+ sections: { details: "Details" },
91
92
  },
92
93
  },
93
94
  entities: {
@@ -68,44 +68,40 @@ export function updateI18n(
68
68
  entitiesUpdated = true;
69
69
  }
70
70
 
71
- // Check if module already exists in features
72
- const featuresAlreadyExist = messages.features && messages.features[lowercaseModuleName];
73
-
74
- if (featuresAlreadyExist) {
75
- // Features exist, but we may have added entities
76
- if (entitiesUpdated) {
77
- if (dryRun) {
78
- return {
79
- success: true,
80
- message: `[DRY RUN] Module ${names.camelCase} exists, would add entities.${lowercasePluralKey}`,
81
- alreadyExists: true,
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 exists in messages/${language}.json`,
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
  )}
@@ -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
+ });