@carlonicora/nextjs-jsonapi 1.115.0 → 1.117.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 (78) hide show
  1. package/dist/{AssistantMessageInterface-BuUZFkE5.d.mts → AssistantMessageInterface-Ca0G0vZw.d.mts} +1 -1
  2. package/dist/{AssistantMessageInterface-CQwh5tEb.d.ts → AssistantMessageInterface-DEZ5AnVl.d.ts} +1 -1
  3. package/dist/{BlockNoteEditor-V74ICZY3.js → BlockNoteEditor-6UREFUSJ.js} +19 -19
  4. package/dist/{BlockNoteEditor-V74ICZY3.js.map → BlockNoteEditor-6UREFUSJ.js.map} +1 -1
  5. package/dist/{BlockNoteEditor-Z3EKI3C6.mjs → BlockNoteEditor-UQOOX3UT.mjs} +4 -4
  6. package/dist/{auth.interface-CXBF8Mhi.d.mts → auth.interface-r3XKYOrQ.d.mts} +1 -1
  7. package/dist/{auth.interface-8b601idJ.d.ts → auth.interface-yivTb46w.d.ts} +1 -1
  8. package/dist/billing/index.js +346 -346
  9. package/dist/billing/index.mjs +3 -3
  10. package/dist/{chunk-3MUBMSDD.js → chunk-6KLR3WJQ.js} +7 -7
  11. package/dist/{chunk-3MUBMSDD.js.map → chunk-6KLR3WJQ.js.map} +1 -1
  12. package/dist/{chunk-6LSGIJ5W.js → chunk-HXU2HDRN.js} +1090 -956
  13. package/dist/chunk-HXU2HDRN.js.map +1 -0
  14. package/dist/{chunk-UBBTQUFB.mjs → chunk-M7XAKLSP.mjs} +11 -2
  15. package/dist/{chunk-UBBTQUFB.mjs.map → chunk-M7XAKLSP.mjs.map} +1 -1
  16. package/dist/{chunk-J3DFHESB.js → chunk-S2PTWBN6.js} +27 -18
  17. package/dist/chunk-S2PTWBN6.js.map +1 -0
  18. package/dist/{chunk-BKKVLD42.mjs → chunk-SVEPL5J2.mjs} +514 -380
  19. package/dist/chunk-SVEPL5J2.mjs.map +1 -0
  20. package/dist/{chunk-SJRSR3AF.mjs → chunk-WEPQD5MP.mjs} +2 -2
  21. package/dist/client/index.d.mts +2 -2
  22. package/dist/client/index.d.ts +2 -2
  23. package/dist/client/index.js +4 -4
  24. package/dist/client/index.mjs +3 -3
  25. package/dist/components/index.d.mts +71 -24
  26. package/dist/components/index.d.ts +71 -24
  27. package/dist/components/index.js +6 -4
  28. package/dist/components/index.js.map +1 -1
  29. package/dist/components/index.mjs +5 -3
  30. package/dist/contexts/index.d.mts +3 -3
  31. package/dist/contexts/index.d.ts +3 -3
  32. package/dist/contexts/index.js +4 -4
  33. package/dist/contexts/index.mjs +3 -3
  34. package/dist/core/index.d.mts +10 -8
  35. package/dist/core/index.d.ts +10 -8
  36. package/dist/core/index.js +2 -2
  37. package/dist/core/index.mjs +1 -1
  38. package/dist/features/help/index.js +36 -36
  39. package/dist/features/help/index.mjs +3 -3
  40. package/dist/index.d.mts +5 -5
  41. package/dist/index.d.ts +5 -5
  42. package/dist/index.js +3 -3
  43. package/dist/index.mjs +2 -2
  44. package/dist/{notification.interface-DIxR23eS.d.mts → notification.interface-B3kNUTZs.d.mts} +2 -1
  45. package/dist/{notification.interface-C1T1C2ee.d.ts → notification.interface-LKFmZndR.d.ts} +2 -1
  46. package/dist/{s3.service-CAL5dhGG.d.mts → s3.service-CihgBy97.d.mts} +2 -2
  47. package/dist/{s3.service-COLYvcW2.d.ts → s3.service-Cyvfk3qV.d.ts} +2 -2
  48. package/dist/server/index.d.mts +3 -3
  49. package/dist/server/index.d.ts +3 -3
  50. package/dist/server/index.js +3 -3
  51. package/dist/server/index.mjs +1 -1
  52. package/dist/{useSocket-B1fMIr17.d.mts → useSocket-BSjn1Rlb.d.mts} +1 -1
  53. package/dist/{useSocket-BdJTBXKv.d.ts → useSocket-BsbaGPqE.d.ts} +1 -1
  54. package/package.json +1 -1
  55. package/src/components/forms/CommonDeleter.tsx +49 -18
  56. package/src/components/forms/CommonEditorButtons.tsx +4 -2
  57. package/src/components/forms/CommonEditorHeader.tsx +20 -14
  58. package/src/components/forms/CommonEditorTrigger.tsx +18 -4
  59. package/src/components/forms/DateRangeSelector.tsx +55 -19
  60. package/src/components/forms/EditorSheet.tsx +36 -9
  61. package/src/components/forms/FileUploader.tsx +51 -21
  62. package/src/components/forms/FormDate.tsx +6 -2
  63. package/src/components/forms/FormDateTime.tsx +11 -2
  64. package/src/components/forms/FormFieldWrapper.tsx +3 -1
  65. package/src/components/forms/FormInput.tsx +3 -1
  66. package/src/components/forms/FormPassword.tsx +10 -1
  67. package/src/components/forms/FormPlaceAutocomplete.tsx +8 -3
  68. package/src/components/forms/FormSelect.tsx +3 -0
  69. package/src/components/forms/FormSlider.tsx +3 -1
  70. package/src/components/forms/FormTextarea.tsx +15 -1
  71. package/src/features/notification/data/notification.interface.ts +2 -2
  72. package/src/features/notification/data/notification.ts +21 -5
  73. package/src/shadcnui/ui/badge.tsx +15 -0
  74. package/dist/chunk-6LSGIJ5W.js.map +0 -1
  75. package/dist/chunk-BKKVLD42.mjs.map +0 -1
  76. package/dist/chunk-J3DFHESB.js.map +0 -1
  77. /package/dist/{BlockNoteEditor-Z3EKI3C6.mjs.map → BlockNoteEditor-UQOOX3UT.mjs.map} +0 -0
  78. /package/dist/{chunk-SJRSR3AF.mjs.map → chunk-WEPQD5MP.mjs.map} +0 -0
@@ -1,7 +1,7 @@
1
1
  import { F as FeatureInterface } from './feature.interface-CXb1-vNq.mjs';
2
- import { e as RoleInterface, U as UserInterface, f as UserInput, c as CompanyInterface, b as CompanyInput, C as ContentInterface, N as NotificationInterface } from './notification.interface-DIxR23eS.mjs';
2
+ import { e as RoleInterface, U as UserInterface, f as UserInput, c as CompanyInterface, b as CompanyInput, C as ContentInterface, N as NotificationInterface } from './notification.interface-B3kNUTZs.mjs';
3
3
  import { A as ApiDataInterface, J as JsonApiHydratedDataInterface } from './ApiDataInterface-BcZeXy5X.mjs';
4
- import { A as AuthInput, b as AuthInterface } from './auth.interface-CXBF8Mhi.mjs';
4
+ import { A as AuthInput, b as AuthInterface } from './auth.interface-r3XKYOrQ.mjs';
5
5
  import { A as ApiRequestDataTypeInterface } from './ApiRequestDataTypeInterface-CYEcRUrh.mjs';
6
6
 
7
7
  declare enum HttpMethod {
@@ -1,7 +1,7 @@
1
1
  import { F as FeatureInterface } from './feature.interface-BO25VLlx.js';
2
- import { e as RoleInterface, U as UserInterface, f as UserInput, c as CompanyInterface, b as CompanyInput, C as ContentInterface, N as NotificationInterface } from './notification.interface-C1T1C2ee.js';
2
+ import { e as RoleInterface, U as UserInterface, f as UserInput, c as CompanyInterface, b as CompanyInput, C as ContentInterface, N as NotificationInterface } from './notification.interface-LKFmZndR.js';
3
3
  import { A as ApiDataInterface, J as JsonApiHydratedDataInterface } from './ApiDataInterface-BcZeXy5X.js';
4
- import { A as AuthInput, b as AuthInterface } from './auth.interface-8b601idJ.js';
4
+ import { A as AuthInput, b as AuthInterface } from './auth.interface-yivTb46w.js';
5
5
  import { A as ApiRequestDataTypeInterface } from './ApiRequestDataTypeInterface-CYEcRUrh.js';
6
6
 
7
7
  declare enum HttpMethod {
@@ -2,12 +2,12 @@ import { A as ApiData } from '../ApiData-DPKNfY-9.mjs';
2
2
  import { M as ModuleWithPermissions, A as Action } from '../types-CQSjy7et.mjs';
3
3
  import { A as ApiRequestDataTypeInterface } from '../ApiRequestDataTypeInterface-CYEcRUrh.mjs';
4
4
  import { A as ApiResponseInterface } from '../ApiResponseInterface-rsXRL_Hn.mjs';
5
- export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-CAL5dhGG.mjs';
5
+ export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-CihgBy97.mjs';
6
6
  import 'lucide-react';
7
7
  import '../ApiDataInterface-BcZeXy5X.mjs';
8
8
  import '../feature.interface-CXb1-vNq.mjs';
9
- import '../notification.interface-DIxR23eS.mjs';
10
- import '../auth.interface-CXBF8Mhi.mjs';
9
+ import '../notification.interface-B3kNUTZs.mjs';
10
+ import '../auth.interface-r3XKYOrQ.mjs';
11
11
 
12
12
  type CacheProfile = "seconds" | "minutes" | "hours" | "days" | "weeks" | "max" | "default";
13
13
  /**
@@ -2,12 +2,12 @@ import { A as ApiData } from '../ApiData-DPKNfY-9.js';
2
2
  import { M as ModuleWithPermissions, A as Action } from '../types-DHOxe8rc.js';
3
3
  import { A as ApiRequestDataTypeInterface } from '../ApiRequestDataTypeInterface-CYEcRUrh.js';
4
4
  import { A as ApiResponseInterface } from '../ApiResponseInterface-CWLvSCvS.js';
5
- export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-COLYvcW2.js';
5
+ export { A as ServerAuthService, C as ServerCompanyService, a as ServerContentService, F as ServerFeatureService, N as ServerNotificationService, P as ServerPushService, R as ServerRoleService, S as ServerS3Service, U as ServerUserService } from '../s3.service-Cyvfk3qV.js';
6
6
  import 'lucide-react';
7
7
  import '../ApiDataInterface-BcZeXy5X.js';
8
8
  import '../feature.interface-BO25VLlx.js';
9
- import '../notification.interface-C1T1C2ee.js';
10
- import '../auth.interface-8b601idJ.js';
9
+ import '../notification.interface-LKFmZndR.js';
10
+ import '../auth.interface-yivTb46w.js';
11
11
 
12
12
  type CacheProfile = "seconds" | "minutes" | "hours" | "days" | "weeks" | "max" | "default";
13
13
  /**
@@ -15,7 +15,7 @@ var _chunkIOLU4ASDjs = require('../chunk-IOLU4ASD.js');
15
15
 
16
16
 
17
17
 
18
- var _chunkJ3DFHESBjs = require('../chunk-J3DFHESB.js');
18
+ var _chunkS2PTWBN6js = require('../chunk-S2PTWBN6.js');
19
19
  require('../chunk-LXKSUWAV.js');
20
20
  require('../chunk-IBS6NI7D.js');
21
21
 
@@ -86,7 +86,7 @@ var ServerSession = class {
86
86
  if (!rawModules) return false;
87
87
  const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
88
88
  const selectedModule = modules.find((module) => module.id === params.module.moduleId);
89
- return _chunkJ3DFHESBjs.checkPermissionsFromServer.call(void 0, {
89
+ return _chunkS2PTWBN6js.checkPermissionsFromServer.call(void 0, {
90
90
  module: params.module,
91
91
  action: params.action,
92
92
  data: params.data,
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
296
296
 
297
297
 
298
298
 
299
- exports.ServerAuthService = _chunkJ3DFHESBjs.AuthService; exports.ServerCompanyService = _chunkJ3DFHESBjs.CompanyService; exports.ServerContentService = _chunkJ3DFHESBjs.ContentService; exports.ServerFeatureService = _chunkJ3DFHESBjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkJ3DFHESBjs.NotificationService; exports.ServerPushService = _chunkJ3DFHESBjs.PushService; exports.ServerRoleService = _chunkJ3DFHESBjs.RoleService; exports.ServerS3Service = _chunkJ3DFHESBjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkJ3DFHESBjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunkIOLU4ASDjs.serverRequest;
299
+ exports.ServerAuthService = _chunkS2PTWBN6js.AuthService; exports.ServerCompanyService = _chunkS2PTWBN6js.CompanyService; exports.ServerContentService = _chunkS2PTWBN6js.ContentService; exports.ServerFeatureService = _chunkS2PTWBN6js.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkS2PTWBN6js.NotificationService; exports.ServerPushService = _chunkS2PTWBN6js.PushService; exports.ServerRoleService = _chunkS2PTWBN6js.RoleService; exports.ServerS3Service = _chunkS2PTWBN6js.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkS2PTWBN6js.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunkIOLU4ASDjs.serverRequest;
300
300
  //# sourceMappingURL=index.js.map
@@ -15,7 +15,7 @@ import {
15
15
  S3Service,
16
16
  UserService,
17
17
  checkPermissionsFromServer
18
- } from "../chunk-UBBTQUFB.mjs";
18
+ } from "../chunk-M7XAKLSP.mjs";
19
19
  import "../chunk-AUXK7QSA.mjs";
20
20
  import "../chunk-C7C7VY4F.mjs";
21
21
  import {
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-DIxR23eS.mjs';
1
+ import { N as NotificationInterface } from './notification.interface-B3kNUTZs.mjs';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-C1T1C2ee.js';
1
+ import { N as NotificationInterface } from './notification.interface-LKFmZndR.js';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.115.0",
3
+ "version": "1.117.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",
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { LoaderCircleIcon, Trash2Icon } from "lucide-react";
4
- import { useState } from "react";
4
+ import { ReactNode, useState } from "react";
5
5
  import { useI18nRouter, useI18nTranslations } from "../../i18n";
6
6
  import {
7
7
  AlertDialog,
@@ -15,15 +15,37 @@ import {
15
15
  import { errorToast } from "../errors";
16
16
 
17
17
  type CommonDeleterProps = {
18
- type: string;
18
+ type?: string;
19
+ /** Override the type-derived dialog title. */
20
+ title?: string;
21
+ /** Override the type-derived dialog subtitle. */
22
+ subtitle?: string;
23
+ /** Override the type-derived dialog body; rendered as-is. */
24
+ description?: ReactNode;
19
25
  deleteFunction: () => Promise<void>;
20
26
  redirectTo?: string;
21
27
  forceShow?: boolean;
22
28
  testId?: string;
29
+ /** Custom trigger element replacing the default trash button. */
30
+ trigger?: ReactNode;
23
31
  onSuccess?: () => void | Promise<void>;
32
+ /** Fired after a successful delete, in addition to redirect/onSuccess. */
33
+ propagateChanges?: () => void;
24
34
  };
25
35
 
26
- export function CommonDeleter({ deleteFunction, redirectTo, type, forceShow, testId, onSuccess }: CommonDeleterProps) {
36
+ export function CommonDeleter({
37
+ deleteFunction,
38
+ redirectTo,
39
+ type,
40
+ title,
41
+ subtitle,
42
+ description,
43
+ forceShow,
44
+ testId,
45
+ trigger,
46
+ onSuccess,
47
+ propagateChanges,
48
+ }: CommonDeleterProps) {
27
49
  const t = useI18nTranslations();
28
50
  const router = useI18nRouter();
29
51
  const [open, setOpen] = useState<boolean>(forceShow || false);
@@ -37,6 +59,7 @@ export function CommonDeleter({ deleteFunction, redirectTo, type, forceShow, tes
37
59
  setOpen(false);
38
60
  if (onSuccess) await onSuccess();
39
61
  else if (redirectTo) router.push(redirectTo);
62
+ if (propagateChanges) propagateChanges();
40
63
  } catch (error) {
41
64
  errorToast({ title: t(`common.errors.delete`), error: error });
42
65
  }
@@ -47,28 +70,36 @@ export function CommonDeleter({ deleteFunction, redirectTo, type, forceShow, tes
47
70
  <AlertDialog open={open} onOpenChange={setOpen}>
48
71
  {forceShow ? null : (
49
72
  <AlertDialogTrigger>
50
- <Button
51
- render={<div />}
52
- nativeButton={false}
53
- size="sm"
54
- variant={"ghost"}
55
- className="text-muted-foreground hover:text-destructive"
56
- data-testid={testId}
57
- >
58
- <Trash2Icon />
59
- </Button>
73
+ {trigger || (
74
+ <Button
75
+ render={<div />}
76
+ nativeButton={false}
77
+ size="sm"
78
+ variant={"ghost"}
79
+ className="text-muted-foreground hover:text-destructive"
80
+ data-testid={testId}
81
+ >
82
+ <Trash2Icon />
83
+ </Button>
84
+ )}
60
85
  </AlertDialogTrigger>
61
86
  )}
62
87
  <AlertDialogContent className={`flex max-h-[70vh] max-w-3xl flex-col overflow-y-auto`}>
63
88
  <AlertDialogHeader>
64
- <AlertDialogTitle>{t(`common.delete.title`, { type: t(`entities.${type}`, { count: 1 }) })}</AlertDialogTitle>
89
+ <AlertDialogTitle>
90
+ {title ?? t(`common.delete.title`, { type: t(`entities.${type}`, { count: 1 }) })}
91
+ </AlertDialogTitle>
65
92
  <AlertDialogDescription>
66
- {t(`common.delete.subtitle`, { type: t(`entities.${type}`, { count: 1 }) })}
93
+ {subtitle ?? t(`common.delete.subtitle`, { type: t(`entities.${type}`, { count: 1 }) })}
67
94
  </AlertDialogDescription>
68
95
  </AlertDialogHeader>
69
- <div className="text-destructive p-4 text-sm">
70
- {t(`common.delete.description`, { type: t(`entities.${type}`, { count: 1 }) })}
71
- </div>
96
+ {description !== undefined ? (
97
+ description
98
+ ) : (
99
+ <div className="text-destructive p-4 text-sm">
100
+ {t(`common.delete.description`, { type: t(`entities.${type}`, { count: 1 }) })}
101
+ </div>
102
+ )}
72
103
  <div className="flex justify-end">
73
104
  <Button
74
105
  className="mr-2"
@@ -9,6 +9,7 @@ type CommonEditorButtonsProps = {
9
9
  setOpen: (open: boolean) => void;
10
10
  hideSubmit?: boolean;
11
11
  centerButtons?: ReactNode;
12
+ buttonTitle?: string;
12
13
  };
13
14
  export function CommonEditorButtons({
14
15
  isEdit,
@@ -17,6 +18,7 @@ export function CommonEditorButtons({
17
18
  setOpen,
18
19
  hideSubmit,
19
20
  centerButtons,
21
+ buttonTitle,
20
22
  }: CommonEditorButtonsProps) {
21
23
  const t = useTranslations();
22
24
 
@@ -31,7 +33,7 @@ export function CommonEditorButtons({
31
33
 
32
34
  {!hideSubmit ? (
33
35
  <Button type="submit" disabled={form.formState.isSubmitting || disabled} data-testid={`modal-button-create`}>
34
- {isEdit ? t(`ui.buttons.confirm_update`) : t(`ui.buttons.confirm_create`)}
36
+ {buttonTitle ? buttonTitle : isEdit ? t(`ui.buttons.confirm_update`) : t(`ui.buttons.confirm_create`)}
35
37
  </Button>
36
38
  ) : (
37
39
  <div />
@@ -54,7 +56,7 @@ export function CommonEditorButtons({
54
56
 
55
57
  {!hideSubmit && (
56
58
  <Button type="submit" disabled={form.formState.isSubmitting || disabled} data-testid={`modal-button-create`}>
57
- {isEdit ? t(`ui.buttons.confirm_update`) : t(`ui.buttons.confirm_create`)}
59
+ {buttonTitle ? buttonTitle : isEdit ? t(`ui.buttons.confirm_update`) : t(`ui.buttons.confirm_create`)}
58
60
  </Button>
59
61
  )}
60
62
  </div>
@@ -6,29 +6,35 @@ import { DialogDescription, DialogHeader, DialogTitle } from "../../shadcnui";
6
6
  type CommonEditorHeaderProps = {
7
7
  type: string;
8
8
  name?: string;
9
+ title?: string;
10
+ description?: string;
9
11
  };
10
12
 
11
- export function CommonEditorHeader({ type, name }: CommonEditorHeaderProps) {
13
+ export function CommonEditorHeader({ type, name, title, description }: CommonEditorHeaderProps) {
12
14
  const t = useTranslations();
13
15
 
14
16
  return (
15
17
  <DialogHeader>
16
18
  <DialogTitle>
17
- {name
18
- ? t(`common.edit.update.title`, {
19
- type: type,
20
- })
21
- : t(`common.edit.create.title`, { type: type })}
19
+ {title
20
+ ? title
21
+ : name
22
+ ? t(`common.edit.update.title`, {
23
+ type: type,
24
+ })
25
+ : t(`common.edit.create.title`, { type: type })}
22
26
  </DialogTitle>
23
27
  <DialogDescription>
24
- {name
25
- ? t(`common.edit.update.description`, {
26
- type: type,
27
- name: name,
28
- })
29
- : t(`common.edit.create.description`, {
30
- type: type,
31
- })}
28
+ {description
29
+ ? description
30
+ : name
31
+ ? t(`common.edit.update.description`, {
32
+ type: type,
33
+ name: name,
34
+ })
35
+ : t(`common.edit.create.description`, {
36
+ type: type,
37
+ })}
32
38
  </DialogDescription>
33
39
  </DialogHeader>
34
40
  );
@@ -1,18 +1,23 @@
1
1
  "use client";
2
2
 
3
- import { PencilIcon } from "lucide-react";
3
+ import { PencilIcon, PlusCircleIcon } from "lucide-react";
4
4
 
5
5
  import { useTranslations } from "next-intl";
6
+ import React from "react";
6
7
  import { Button, DialogTrigger } from "../../shadcnui";
7
8
 
8
- type CommonEditorTriggerProps = { isEdit: boolean; edit?: string; create?: string; testId?: string };
9
+ type CommonEditorTriggerProps = { isEdit: boolean; edit?: string; create?: string; testId?: string; title?: string };
9
10
 
10
- export function CommonEditorTrigger({ isEdit, edit: _edit, create, testId }: CommonEditorTriggerProps) {
11
+ export function CommonEditorTrigger({ isEdit, edit: _edit, create, testId, title }: CommonEditorTriggerProps) {
11
12
  const t = useTranslations();
12
13
 
13
14
  return (
14
15
  <DialogTrigger>
15
- {isEdit ? (
16
+ {title ? (
17
+ <Button render={<div />} nativeButton={false} size="sm" variant={`outline`} data-testid={testId}>
18
+ {title}
19
+ </Button>
20
+ ) : isEdit ? (
16
21
  <Button
17
22
  render={<div />}
18
23
  nativeButton={false}
@@ -31,3 +36,12 @@ export function CommonEditorTrigger({ isEdit, edit: _edit, create, testId }: Com
31
36
  </DialogTrigger>
32
37
  );
33
38
  }
39
+
40
+ export const CommonAddTrigger = React.forwardRef<HTMLButtonElement, React.ComponentPropsWithoutRef<typeof Button>>(
41
+ (props, ref) => (
42
+ <Button ref={ref} variant="ghost" size="sm" className="text-muted-foreground" {...props}>
43
+ <PlusCircleIcon />
44
+ </Button>
45
+ ),
46
+ );
47
+ CommonAddTrigger.displayName = "CommonAddTrigger";
@@ -2,6 +2,7 @@
2
2
 
3
3
  import { format } from "date-fns";
4
4
  import { CalendarIcon } from "lucide-react";
5
+ import { useTranslations } from "next-intl";
5
6
  import { useEffect, useState } from "react";
6
7
  import { DateRange } from "react-day-picker";
7
8
  import { Button, Calendar, Popover, PopoverContent, PopoverTrigger } from "../../shadcnui";
@@ -14,6 +15,7 @@ type DateRangeSelectorProps = {
14
15
  };
15
16
 
16
17
  export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviousMonth }: DateRangeSelectorProps) {
18
+ const t = useTranslations();
17
19
  const [date, setDate] = useState<DateRange | undefined>(
18
20
  avoidSettingDates
19
21
  ? undefined
@@ -24,6 +26,12 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
24
26
  );
25
27
 
26
28
  const [open, setOpen] = useState(false);
29
+ const [mounted, setMounted] = useState(false);
30
+
31
+ // Prevent hydration mismatch by only rendering Popover after mount
32
+ useEffect(() => {
33
+ setMounted(true);
34
+ }, []);
27
35
 
28
36
  const [prevRange, setPrevRange] = useState<DateRange | undefined>(date);
29
37
  useEffect(() => {
@@ -53,28 +61,56 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
53
61
  }
54
62
  };
55
63
 
64
+ // Show placeholder button during SSR to prevent hydration mismatch
65
+ if (!mounted) {
66
+ return (
67
+ <div className={cn("grid gap-2")}>
68
+ <Button
69
+ id="date"
70
+ variant={"outline"}
71
+ className={cn("w-[300px] justify-start text-left font-normal", !date && "text-muted-foreground")}
72
+ >
73
+ <CalendarIcon />
74
+ {date?.from ? (
75
+ date.to ? (
76
+ <>
77
+ {format(date.from, "LLL dd, y")} - {format(date.to, "LLL dd, y")}
78
+ </>
79
+ ) : (
80
+ format(date.from, "LLL dd, y")
81
+ )
82
+ ) : (
83
+ <span>{t("ui.labels.pick_a_date")}</span>
84
+ )}
85
+ </Button>
86
+ </div>
87
+ );
88
+ }
89
+
56
90
  return (
57
91
  <div className={cn("grid gap-2")}>
58
92
  <Popover open={open} onOpenChange={setOpen}>
59
- <PopoverTrigger>
60
- <Button
61
- id="date"
62
- variant={"outline"}
63
- className={cn("w-[300px] justify-start text-left font-normal", !date && "text-muted-foreground")}
64
- >
65
- <CalendarIcon />
66
- {date?.from ? (
67
- date.to ? (
68
- <>
69
- {format(date.from, "LLL dd, y")} - {format(date.to, "LLL dd, y")}
70
- </>
71
- ) : (
72
- format(date.from, "LLL dd, y")
73
- )
93
+ <PopoverTrigger
94
+ render={
95
+ <Button
96
+ id="date"
97
+ variant={"outline"}
98
+ className={cn("w-[300px] justify-start text-left font-normal", !date && "text-muted-foreground")}
99
+ />
100
+ }
101
+ >
102
+ <CalendarIcon />
103
+ {date?.from ? (
104
+ date.to ? (
105
+ <>
106
+ {format(date.from, "LLL dd, y")} - {format(date.to, "LLL dd, y")}
107
+ </>
74
108
  ) : (
75
- <span>Pick a date</span>
76
- )}
77
- </Button>
109
+ format(date.from, "LLL dd, y")
110
+ )
111
+ ) : (
112
+ <span>{t("ui.labels.pick_a_date")}</span>
113
+ )}
78
114
  </PopoverTrigger>
79
115
  <PopoverContent className="w-auto p-0" align="start">
80
116
  <div className="flex flex-col gap-2 p-2">
@@ -100,7 +136,7 @@ export function DateRangeSelector({ onDateChange, avoidSettingDates, showPreviou
100
136
  className="cursor-pointer"
101
137
  disabled={!date}
102
138
  >
103
- Clear
139
+ {t("ui.buttons.clear")}
104
140
  </Button>
105
141
  </div>
106
142
  </PopoverContent>
@@ -62,6 +62,17 @@ export type EditorSheetProps<T extends FieldValues> = {
62
62
  dialogOpen?: boolean;
63
63
  onDialogOpenChange?: (open: boolean) => void;
64
64
 
65
+ /** Render a fully custom footer instead of the default CommonEditorButtons.
66
+ * `setOpen` is the dirty-checked open handler (shows the discard dialog);
67
+ * `closeWithoutConfirm` is the raw setter that bypasses the discard dialog
68
+ * (use after a successful submit when no confirmation is needed). */
69
+ renderFooter?: (props: {
70
+ form: UseFormReturn<T>;
71
+ isEdit: boolean;
72
+ setOpen: (open: boolean) => void;
73
+ closeWithoutConfirm: (open: boolean) => void;
74
+ }) => ReactNode;
75
+
65
76
  children: ReactNode;
66
77
  };
67
78
 
@@ -90,6 +101,7 @@ export function EditorSheet<T extends FieldValues>({
90
101
  onClose,
91
102
  dialogOpen,
92
103
  onDialogOpenChange,
104
+ renderFooter,
93
105
  children,
94
106
  }: EditorSheetProps<T>) {
95
107
  const t = useTranslations();
@@ -147,6 +159,7 @@ export function EditorSheet<T extends FieldValues>({
147
159
  <>
148
160
  <Sheet open={open} onOpenChange={handleOpenChange}>
149
161
  {dialogOpen === undefined &&
162
+ forceShow === undefined &&
150
163
  (trigger ? (
151
164
  <SheetTrigger>{trigger}</SheetTrigger>
152
165
  ) : (
@@ -184,17 +197,31 @@ export function EditorSheet<T extends FieldValues>({
184
197
  </SheetDescription>
185
198
  </SheetHeader>
186
199
  <Form {...form}>
187
- <form onSubmit={form.handleSubmit(wrappedOnSubmit)} className="flex min-h-0 flex-1 flex-col">
200
+ <form
201
+ onSubmit={(e) => {
202
+ // The Sheet content is portaled, but React synthetic events still
203
+ // bubble up the React tree — so without stopPropagation an inner
204
+ // EditorSheet's submit also triggers the outer form's submit
205
+ // (e.g. a create dialog opened from within another editor).
206
+ e.stopPropagation();
207
+ return form.handleSubmit(wrappedOnSubmit)(e);
208
+ }}
209
+ className="flex min-h-0 flex-1 flex-col"
210
+ >
188
211
  <div className="flex-1 overflow-y-auto px-6 py-4">{children}</div>
189
212
  <SheetFooter className="shrink-0 border-t px-6 py-4">
190
- <CommonEditorButtons
191
- form={form}
192
- setOpen={handleOpenChange}
193
- isEdit={isEdit}
194
- disabled={disabled}
195
- hideSubmit={hideSubmit}
196
- centerButtons={centerButtons}
197
- />
213
+ {renderFooter ? (
214
+ renderFooter({ form, isEdit, setOpen: handleOpenChange, closeWithoutConfirm: setOpen })
215
+ ) : (
216
+ <CommonEditorButtons
217
+ form={form}
218
+ setOpen={handleOpenChange}
219
+ isEdit={isEdit}
220
+ disabled={disabled}
221
+ hideSubmit={hideSubmit}
222
+ centerButtons={centerButtons}
223
+ />
224
+ )}
198
225
  </SheetFooter>
199
226
  </form>
200
227
  </Form>
@@ -10,12 +10,13 @@ import {
10
10
  useCallback,
11
11
  useContext,
12
12
  useEffect,
13
+ useMemo,
13
14
  useRef,
14
15
  useState,
15
16
  } from "react";
16
- import { DropzoneOptions, DropzoneState, FileRejection, useDropzone } from "react-dropzone";
17
+ import { Accept, DropzoneOptions, DropzoneState, FileRejection, useDropzone } from "react-dropzone";
17
18
  import { showError } from "../../utils/toast";
18
- import { buttonVariants, Input } from "../../shadcnui";
19
+ import { buttonVariants, Input, Tooltip, TooltipContent, TooltipTrigger } from "../../shadcnui";
19
20
  import { cn } from "../../utils";
20
21
 
21
22
  export type { DropzoneOptions } from "react-dropzone";
@@ -31,6 +32,7 @@ type FileUploaderContextType = {
31
32
  setActiveIndex: Dispatch<SetStateAction<number>>;
32
33
  orientation: "horizontal" | "vertical";
33
34
  direction: DirectionOptions;
35
+ accept?: Accept;
34
36
  };
35
37
 
36
38
  const FileUploaderContext = createContext<FileUploaderContextType | null>(null);
@@ -195,6 +197,7 @@ export const FileUploader = forwardRef<HTMLDivElement, FileUploaderProps & React
195
197
  setActiveIndex,
196
198
  orientation,
197
199
  direction,
200
+ accept: dropzoneOptions.accept,
198
201
  }}
199
202
  >
200
203
  <div
@@ -283,35 +286,62 @@ FileUploaderItem.displayName = "FileUploaderItem";
283
286
 
284
287
  export const FileInput = forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
285
288
  ({ className, children, ...props }, ref) => {
286
- const { dropzoneState, isFileTooBig, isLOF } = useFileUpload();
289
+ const { dropzoneState, isFileTooBig, isLOF, accept } = useFileUpload();
290
+ const t = useTranslations();
287
291
  const rootProps = isLOF ? {} : dropzoneState.getRootProps();
288
292
  // Get isDragActive from the context for FileInput as well, to ensure it can react if needed, or to simplify its own styling.
289
293
  const { isDragActive: parentIsDragActive } = dropzoneState;
290
294
 
295
+ const acceptedLabels = useMemo(() => {
296
+ if (!accept) return null;
297
+ const extensions = new Set<string>();
298
+ let hasWildcardImages = false;
299
+ for (const [mime, exts] of Object.entries(accept)) {
300
+ if (mime === "image/*") hasWildcardImages = true;
301
+ for (const ext of exts) extensions.add(ext);
302
+ }
303
+ const labels = Array.from(extensions).sort();
304
+ if (hasWildcardImages) labels.push(t("ui.labels.images"));
305
+ return labels.length > 0 ? labels : null;
306
+ }, [accept, t]);
307
+
308
+ const dropArea = (
309
+ <div
310
+ className={cn(
311
+ "w-full rounded-lg duration-300 ease-in-out",
312
+ // Simpler border logic: if parent is drag-active, it controls the border.
313
+ // Otherwise, FileInput can show its own accept/reject/default border.
314
+ {
315
+ "border-green-500": dropzoneState.isDragAccept && !parentIsDragActive,
316
+ "border-red-500": (dropzoneState.isDragReject || isFileTooBig) && !parentIsDragActive,
317
+ "border-gray-300":
318
+ !dropzoneState.isDragAccept && !dropzoneState.isDragReject && !isFileTooBig && !parentIsDragActive,
319
+ },
320
+ // className from props should be last to allow overrides if necessary
321
+ className,
322
+ )}
323
+ {...rootProps}
324
+ >
325
+ {children}
326
+ </div>
327
+ );
328
+
291
329
  return (
292
330
  <div
293
331
  ref={ref}
294
332
  {...props}
295
333
  className={cn(`relative w-full ${isLOF ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`, className)}
296
334
  >
297
- <div
298
- className={cn(
299
- "w-full rounded-lg duration-300 ease-in-out",
300
- // Simpler border logic: if parent is drag-active, it controls the border.
301
- // Otherwise, FileInput can show its own accept/reject/default border.
302
- {
303
- "border-green-500": dropzoneState.isDragAccept && !parentIsDragActive,
304
- "border-red-500": (dropzoneState.isDragReject || isFileTooBig) && !parentIsDragActive,
305
- "border-gray-300":
306
- !dropzoneState.isDragAccept && !dropzoneState.isDragReject && !isFileTooBig && !parentIsDragActive,
307
- },
308
- // className from props should be last to allow overrides if necessary
309
- className,
310
- )}
311
- {...rootProps}
312
- >
313
- {children}
314
- </div>
335
+ {acceptedLabels ? (
336
+ <Tooltip>
337
+ <TooltipTrigger render={dropArea} />
338
+ <TooltipContent>
339
+ {t("ui.labels.accepted_file_types")}: {acceptedLabels.join(", ")}
340
+ </TooltipContent>
341
+ </Tooltip>
342
+ ) : (
343
+ dropArea
344
+ )}
315
345
  <Input
316
346
  ref={dropzoneState.inputRef}
317
347
  disabled={isLOF}