@carlonicora/nextjs-jsonapi 1.59.0 → 1.61.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 (34) hide show
  1. package/dist/{BlockNoteEditor-V46DP6BW.mjs → BlockNoteEditor-HTD3BPTX.mjs} +2 -2
  2. package/dist/{BlockNoteEditor-LM45SVSD.js → BlockNoteEditor-LUAMTXEK.js} +6 -6
  3. package/dist/{BlockNoteEditor-LM45SVSD.js.map → BlockNoteEditor-LUAMTXEK.js.map} +1 -1
  4. package/dist/billing/index.js +299 -299
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-4QXIOFK5.js → chunk-T2Z5OJCX.js} +317 -127
  7. package/dist/chunk-T2Z5OJCX.js.map +1 -0
  8. package/dist/{chunk-FDTDSTD6.mjs → chunk-W5ADYJZO.mjs} +2254 -2064
  9. package/dist/chunk-W5ADYJZO.mjs.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 +55 -3
  13. package/dist/components/index.d.ts +55 -3
  14. package/dist/components/index.js +8 -2
  15. package/dist/components/index.js.map +1 -1
  16. package/dist/components/index.mjs +7 -1
  17. package/dist/contexts/index.js +2 -2
  18. package/dist/contexts/index.mjs +1 -1
  19. package/dist/scripts/generate-web-module/templates/components/editor.template.d.ts.map +1 -1
  20. package/dist/scripts/generate-web-module/templates/components/editor.template.js +17 -39
  21. package/dist/scripts/generate-web-module/templates/components/editor.template.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/generate-web-module/templates/components/editor.template.ts +17 -39
  24. package/src/components/forms/CommonEditorDiscardDialog.tsx +40 -0
  25. package/src/components/forms/EditorSheet.tsx +179 -0
  26. package/src/components/forms/__tests__/CommonEditorDiscardDialog.test.tsx +43 -0
  27. package/src/components/forms/__tests__/EditorSheet.test.tsx +217 -0
  28. package/src/components/forms/__tests__/useEditorDialog.test.ts +145 -0
  29. package/src/components/forms/index.ts +3 -0
  30. package/src/components/forms/useEditorDialog.ts +93 -0
  31. package/src/components/tables/ContentListTable.tsx +13 -14
  32. package/dist/chunk-4QXIOFK5.js.map +0 -1
  33. package/dist/chunk-FDTDSTD6.mjs.map +0 -1
  34. /package/dist/{BlockNoteEditor-V46DP6BW.mjs.map → BlockNoteEditor-HTD3BPTX.mjs.map} +0 -0
@@ -0,0 +1,217 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { useForm } from "react-hook-form";
5
+ import { zodResolver } from "@hookform/resolvers/zod";
6
+ import { z } from "zod";
7
+ import { EditorSheet } from "../EditorSheet";
8
+
9
+ // Mock next-intl
10
+ vi.mock("next-intl", () => ({
11
+ useTranslations: () => {
12
+ const t = (key: string, params?: Record<string, string>) => {
13
+ if (params?.type) return `${key}:${params.type}`;
14
+ return key;
15
+ };
16
+ return t;
17
+ },
18
+ }));
19
+
20
+ // Mock usePageUrlGenerator
21
+ vi.mock("../../../hooks/usePageUrlGenerator", () => ({
22
+ usePageUrlGenerator: () => (params: { page?: any; id?: string }) => `/test/${params.id || ""}`,
23
+ }));
24
+
25
+ // Mock errorToast
26
+ vi.mock("../../errors/errorToast", () => ({
27
+ errorToast: vi.fn(),
28
+ }));
29
+
30
+ const testModule = { pageUrl: "/test", name: "tests" } as any;
31
+
32
+ const schema = z.object({
33
+ name: z.string().min(1),
34
+ });
35
+
36
+ function TestEditor({
37
+ onSubmit = vi.fn().mockResolvedValue({ id: "123" }),
38
+ isEdit = false,
39
+ ...props
40
+ }: Partial<React.ComponentProps<typeof EditorSheet>> & { onSubmit?: any }) {
41
+ const form = useForm({
42
+ resolver: zodResolver(schema),
43
+ defaultValues: { name: "" },
44
+ });
45
+
46
+ return (
47
+ <EditorSheet
48
+ form={form}
49
+ onSubmit={onSubmit}
50
+ onReset={() => ({ name: "" })}
51
+ entityType="Test Entity"
52
+ isEdit={isEdit}
53
+ module={testModule}
54
+ {...props}
55
+ >
56
+ <input {...form.register("name")} data-testid="name-input" />
57
+ </EditorSheet>
58
+ );
59
+ }
60
+
61
+ describe("EditorSheet", () => {
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+ });
65
+
66
+ describe("trigger rendering", () => {
67
+ it("should render create button when not editing", () => {
68
+ render(<TestEditor />);
69
+ expect(screen.getByText("ui.buttons.create")).toBeTruthy();
70
+ });
71
+
72
+ it("should render custom trigger when provided", () => {
73
+ render(<TestEditor trigger={<button>Custom</button>} />);
74
+ expect(screen.getByText("Custom")).toBeTruthy();
75
+ });
76
+
77
+ it("should not render trigger when dialogOpen is controlled", () => {
78
+ render(<TestEditor dialogOpen={false} />);
79
+ expect(screen.queryByText("ui.buttons.create")).toBeNull();
80
+ });
81
+ });
82
+
83
+ describe("sheet header", () => {
84
+ it("should show create title when not editing", () => {
85
+ render(<TestEditor dialogOpen={true} />);
86
+ expect(screen.getByText("common.edit.create.title:Test Entity")).toBeTruthy();
87
+ });
88
+
89
+ it("should show update title when editing", () => {
90
+ render(<TestEditor dialogOpen={true} isEdit={true} entityName="My Item" />);
91
+ expect(screen.getByText("common.edit.update.title:Test Entity")).toBeTruthy();
92
+ });
93
+ });
94
+
95
+ describe("size presets", () => {
96
+ it("should default to xl size", () => {
97
+ render(<TestEditor dialogOpen={true} />);
98
+ const content = document.querySelector("[data-side]");
99
+ expect(content?.className).toContain("max-w-7xl");
100
+ });
101
+
102
+ it("should apply sm size", () => {
103
+ render(<TestEditor dialogOpen={true} size="sm" />);
104
+ const content = document.querySelector("[data-side]");
105
+ expect(content?.className).toContain("max-w-2xl");
106
+ });
107
+ });
108
+
109
+ describe("submit flow", () => {
110
+ it("should call onSubmit with form values on submit", async () => {
111
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc" });
112
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} />);
113
+
114
+ const input = screen.getByTestId("name-input");
115
+ await userEvent.type(input, "Test Name");
116
+
117
+ const submitButton = screen.getByTestId("modal-button-create");
118
+ await userEvent.click(submitButton);
119
+
120
+ expect(onSubmit).toHaveBeenCalledWith(expect.objectContaining({ name: "Test Name" }));
121
+ });
122
+
123
+ it("should call onSuccess instead of navigation when provided", async () => {
124
+ const onSubmit = vi.fn().mockResolvedValue(undefined);
125
+ const onSuccess = vi.fn();
126
+ const onNavigate = vi.fn();
127
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onSuccess={onSuccess} onNavigate={onNavigate} />);
128
+
129
+ const input = screen.getByTestId("name-input");
130
+ await userEvent.type(input, "Test Name");
131
+
132
+ const submitButton = screen.getByTestId("modal-button-create");
133
+ await userEvent.click(submitButton);
134
+
135
+ expect(onSuccess).toHaveBeenCalled();
136
+ expect(onNavigate).not.toHaveBeenCalled();
137
+ });
138
+
139
+ it("should call onNavigate when onSuccess is not provided", async () => {
140
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc" });
141
+ const onNavigate = vi.fn();
142
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onNavigate={onNavigate} />);
143
+
144
+ const input = screen.getByTestId("name-input");
145
+ await userEvent.type(input, "Test Name");
146
+
147
+ const submitButton = screen.getByTestId("modal-button-create");
148
+ await userEvent.click(submitButton);
149
+
150
+ expect(onNavigate).toHaveBeenCalledWith("/test/abc");
151
+ });
152
+
153
+ it("should call onRevalidate on successful submit", async () => {
154
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc" });
155
+ const onRevalidate = vi.fn();
156
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} onRevalidate={onRevalidate} />);
157
+
158
+ const input = screen.getByTestId("name-input");
159
+ await userEvent.type(input, "Test Name");
160
+
161
+ const submitButton = screen.getByTestId("modal-button-create");
162
+ await userEvent.click(submitButton);
163
+
164
+ expect(onRevalidate).toHaveBeenCalled();
165
+ });
166
+
167
+ it("should call propagateChanges instead of onNavigate when editing", async () => {
168
+ const onSubmit = vi.fn().mockResolvedValue({ id: "abc", name: "Updated" });
169
+ const propagateChanges = vi.fn();
170
+ const onNavigate = vi.fn();
171
+ render(
172
+ <TestEditor
173
+ dialogOpen={true}
174
+ isEdit={true}
175
+ onSubmit={onSubmit}
176
+ propagateChanges={propagateChanges}
177
+ onNavigate={onNavigate}
178
+ />,
179
+ );
180
+
181
+ const input = screen.getByTestId("name-input");
182
+ await userEvent.type(input, "Test Name");
183
+
184
+ const submitButton = screen.getByTestId("modal-button-create");
185
+ await userEvent.click(submitButton);
186
+
187
+ expect(propagateChanges).toHaveBeenCalledWith({ id: "abc", name: "Updated" });
188
+ expect(onNavigate).not.toHaveBeenCalled();
189
+ });
190
+
191
+ it("should show error toast on submit failure", async () => {
192
+ const { errorToast: mockErrorToast } = await import("../../errors/errorToast");
193
+ const error = new Error("Network error");
194
+ const onSubmit = vi.fn().mockRejectedValue(error);
195
+ render(<TestEditor dialogOpen={true} onSubmit={onSubmit} />);
196
+
197
+ const input = screen.getByTestId("name-input");
198
+ await userEvent.type(input, "Test Name");
199
+
200
+ const submitButton = screen.getByTestId("modal-button-create");
201
+ await userEvent.click(submitButton);
202
+
203
+ expect(mockErrorToast).toHaveBeenCalledWith({
204
+ title: "generic.errors.create",
205
+ error,
206
+ });
207
+ });
208
+ });
209
+
210
+ describe("disabled prop", () => {
211
+ it("should disable submit button when disabled prop is true", () => {
212
+ render(<TestEditor dialogOpen={true} disabled={true} />);
213
+ const submitButton = screen.getByTestId("modal-button-create");
214
+ expect(submitButton).toBeDisabled();
215
+ });
216
+ });
217
+ });
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { renderHook, act } from "@testing-library/react";
3
+ import { useEditorDialog } from "../useEditorDialog";
4
+
5
+ describe("useEditorDialog", () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks();
8
+ });
9
+
10
+ describe("open state", () => {
11
+ it("should start closed by default", () => {
12
+ const { result } = renderHook(() => useEditorDialog(() => false));
13
+ expect(result.current.open).toBe(false);
14
+ });
15
+
16
+ it("should allow setting open state directly", () => {
17
+ const { result } = renderHook(() => useEditorDialog(() => false));
18
+ act(() => result.current.setOpen(true));
19
+ expect(result.current.open).toBe(true);
20
+ });
21
+ });
22
+
23
+ describe("handleOpenChange", () => {
24
+ it("should open dialog when called with true", () => {
25
+ const { result } = renderHook(() => useEditorDialog(() => false));
26
+ act(() => result.current.handleOpenChange(true));
27
+ expect(result.current.open).toBe(true);
28
+ });
29
+
30
+ it("should close dialog when form is not dirty", () => {
31
+ const { result } = renderHook(() => useEditorDialog(() => false));
32
+ act(() => result.current.setOpen(true));
33
+ act(() => result.current.handleOpenChange(false));
34
+ expect(result.current.open).toBe(false);
35
+ });
36
+
37
+ it("should show discard confirmation when form is dirty and closing", () => {
38
+ const { result } = renderHook(() => useEditorDialog(() => true));
39
+ act(() => result.current.setOpen(true));
40
+ act(() => result.current.handleOpenChange(false));
41
+ expect(result.current.open).toBe(true);
42
+ expect(result.current.discardDialogProps.open).toBe(true);
43
+ });
44
+ });
45
+
46
+ describe("discardDialogProps", () => {
47
+ it("should close both dialogs on discard", () => {
48
+ const { result } = renderHook(() => useEditorDialog(() => true));
49
+ act(() => result.current.setOpen(true));
50
+ act(() => result.current.handleOpenChange(false));
51
+ expect(result.current.discardDialogProps.open).toBe(true);
52
+ act(() => result.current.discardDialogProps.onDiscard());
53
+ expect(result.current.discardDialogProps.open).toBe(false);
54
+ expect(result.current.open).toBe(false);
55
+ });
56
+
57
+ it("should allow dismissing discard dialog via onOpenChange", () => {
58
+ const { result } = renderHook(() => useEditorDialog(() => true));
59
+ act(() => result.current.setOpen(true));
60
+ act(() => result.current.handleOpenChange(false));
61
+ act(() => result.current.discardDialogProps.onOpenChange(false));
62
+ expect(result.current.discardDialogProps.open).toBe(false);
63
+ expect(result.current.open).toBe(true);
64
+ });
65
+ });
66
+
67
+ describe("options.dialogOpen", () => {
68
+ it("should sync open state from external dialogOpen prop", () => {
69
+ const { result, rerender } = renderHook(({ dialogOpen }) => useEditorDialog(() => false, { dialogOpen }), {
70
+ initialProps: { dialogOpen: false },
71
+ });
72
+ expect(result.current.open).toBe(false);
73
+ rerender({ dialogOpen: true });
74
+ expect(result.current.open).toBe(true);
75
+ });
76
+ });
77
+
78
+ describe("options.onDialogOpenChange", () => {
79
+ it("should notify parent when open state changes", () => {
80
+ const onDialogOpenChange = vi.fn();
81
+ const { result } = renderHook(() => useEditorDialog(() => false, { onDialogOpenChange }));
82
+ act(() => result.current.setOpen(true));
83
+ expect(onDialogOpenChange).toHaveBeenCalledWith(true);
84
+ });
85
+ });
86
+
87
+ describe("options.forceShow", () => {
88
+ it("should open dialog when forceShow becomes true", () => {
89
+ const { result, rerender } = renderHook(({ forceShow }) => useEditorDialog(() => false, { forceShow }), {
90
+ initialProps: { forceShow: false },
91
+ });
92
+ expect(result.current.open).toBe(false);
93
+ rerender({ forceShow: true });
94
+ expect(result.current.open).toBe(true);
95
+ });
96
+ });
97
+
98
+ describe("options.onClose", () => {
99
+ it("should call onClose when dialog closes", () => {
100
+ const onClose = vi.fn();
101
+ const { result } = renderHook(() => useEditorDialog(() => false, { onClose }));
102
+ act(() => result.current.setOpen(true));
103
+ act(() => result.current.setOpen(false));
104
+ expect(onClose).toHaveBeenCalled();
105
+ });
106
+
107
+ it("should call onClose when discard is confirmed", () => {
108
+ const onClose = vi.fn();
109
+ const { result } = renderHook(() => useEditorDialog(() => true, { onClose }));
110
+ act(() => result.current.setOpen(true));
111
+ act(() => result.current.handleOpenChange(false));
112
+ act(() => result.current.discardDialogProps.onDiscard());
113
+ expect(onClose).toHaveBeenCalled();
114
+ });
115
+ });
116
+
117
+ describe("escape key handler", () => {
118
+ it("should trigger handleOpenChange(false) on Escape when open", () => {
119
+ const { result } = renderHook(() => useEditorDialog(() => true));
120
+ act(() => result.current.setOpen(true));
121
+
122
+ const event = new KeyboardEvent("keydown", {
123
+ key: "Escape",
124
+ bubbles: true,
125
+ cancelable: true,
126
+ });
127
+ act(() => document.dispatchEvent(event));
128
+
129
+ expect(result.current.discardDialogProps.open).toBe(true);
130
+ });
131
+
132
+ it("should not react to Escape when closed", () => {
133
+ const { result } = renderHook(() => useEditorDialog(() => true));
134
+
135
+ const event = new KeyboardEvent("keydown", {
136
+ key: "Escape",
137
+ bubbles: true,
138
+ cancelable: true,
139
+ });
140
+ act(() => document.dispatchEvent(event));
141
+
142
+ expect(result.current.discardDialogProps.open).toBe(false);
143
+ });
144
+ });
145
+ });
@@ -2,7 +2,10 @@ export * from "./CommonAssociationForm";
2
2
  export * from "./CommonDeleter";
3
3
  export * from "./CommonEditorButtons";
4
4
  export * from "./CommonEditorHeader";
5
+ export * from "./CommonEditorDiscardDialog";
5
6
  export * from "./CommonEditorTrigger";
7
+ export * from "./useEditorDialog";
8
+ export * from "./EditorSheet";
6
9
  export * from "./DatePickerPopover";
7
10
  export * from "./DateRangeSelector";
8
11
  export * from "./FileUploader";
@@ -0,0 +1,93 @@
1
+ "use client";
2
+
3
+ import { useCallback, useEffect, useState } from "react";
4
+
5
+ type UseEditorDialogOptions = {
6
+ dialogOpen?: boolean;
7
+ onDialogOpenChange?: (open: boolean) => void;
8
+ forceShow?: boolean;
9
+ onClose?: () => void;
10
+ };
11
+
12
+ type UseEditorDialogReturn = {
13
+ open: boolean;
14
+ setOpen: (open: boolean) => void;
15
+ handleOpenChange: (nextOpen: boolean) => void;
16
+ discardDialogProps: {
17
+ open: boolean;
18
+ onOpenChange: (open: boolean) => void;
19
+ onDiscard: () => void;
20
+ };
21
+ };
22
+
23
+ export function useEditorDialog(isFormDirty: () => boolean, options?: UseEditorDialogOptions): UseEditorDialogReturn {
24
+ const [open, setOpen] = useState<boolean>(false);
25
+ const [showDiscardConfirm, setShowDiscardConfirm] = useState<boolean>(false);
26
+
27
+ // Sync open state from external dialogOpen prop
28
+ useEffect(() => {
29
+ if (options?.dialogOpen !== undefined) {
30
+ setOpen(options.dialogOpen);
31
+ }
32
+ }, [options?.dialogOpen]);
33
+
34
+ // Notify parent when open state changes
35
+ useEffect(() => {
36
+ if (typeof options?.onDialogOpenChange === "function") {
37
+ options.onDialogOpenChange(open);
38
+ }
39
+ }, [open, options?.onDialogOpenChange]);
40
+
41
+ // Force show
42
+ useEffect(() => {
43
+ if (options?.forceShow) setOpen(true);
44
+ }, [options?.forceShow]);
45
+
46
+ // Call onClose when dialog closes
47
+ useEffect(() => {
48
+ if (!open) {
49
+ if (options?.onClose) options.onClose();
50
+ }
51
+ }, [open]);
52
+
53
+ const handleOpenChange = useCallback(
54
+ (nextOpen: boolean) => {
55
+ if (!nextOpen && isFormDirty()) {
56
+ setShowDiscardConfirm(true);
57
+ return;
58
+ }
59
+ setOpen(nextOpen);
60
+ },
61
+ [isFormDirty],
62
+ );
63
+
64
+ // Escape key handler
65
+ useEffect(() => {
66
+ const handleKeyDown = (event: KeyboardEvent) => {
67
+ if (event.key === "Escape" && open) {
68
+ event.preventDefault();
69
+ event.stopPropagation();
70
+ handleOpenChange(false);
71
+ }
72
+ };
73
+
74
+ if (open) {
75
+ document.addEventListener("keydown", handleKeyDown, true);
76
+ }
77
+
78
+ return () => {
79
+ document.removeEventListener("keydown", handleKeyDown, true);
80
+ };
81
+ }, [open, handleOpenChange]);
82
+
83
+ const discardDialogProps = {
84
+ open: showDiscardConfirm,
85
+ onOpenChange: setShowDiscardConfirm,
86
+ onDiscard: () => {
87
+ setShowDiscardConfirm(false);
88
+ setOpen(false);
89
+ },
90
+ };
91
+
92
+ return { open, setOpen, handleOpenChange, discardDialogProps };
93
+ }
@@ -101,20 +101,19 @@ export const ContentListTable = memo(function ContentListTable(props: ContentLis
101
101
  <div className="flex w-full items-center justify-between gap-x-2">
102
102
  {/* <div className="w-full">{fullWidth ? `` : props.title}</div> */}
103
103
  <div className="w-full">
104
- {fullWidth ? (
105
- <div
106
- className={
107
- "text-muted-foreground flex items-center gap-x-2 text-lg font-light whitespace-nowrap"
108
- }
109
- >
110
- {props.tableGeneratorType.icon && (
111
- <props.tableGeneratorType.icon className="text-primary h-6 w-6" />
112
- )}
113
- {props.title}
114
- </div>
115
- ) : (
116
- props.title
117
- )}
104
+ <div
105
+ className={cn(
106
+ "text-muted-foreground flex items-center gap-x-2 font-light whitespace-nowrap",
107
+ fullWidth ? `text-lg` : `text-sm`,
108
+ )}
109
+ >
110
+ {props.tableGeneratorType.icon && (
111
+ <props.tableGeneratorType.icon
112
+ className={cn(`text-primary`, fullWidth ? `h-6 w-6` : `h-4 w-4`)}
113
+ />
114
+ )}
115
+ {props.title}
116
+ </div>
118
117
  </div>
119
118
  {(props.functions || props.filters || allowSearch) && (
120
119
  <>