@carlonicora/nextjs-jsonapi 1.74.0 → 1.76.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 (32) hide show
  1. package/dist/{BlockNoteEditor-KJZ7FGBA.mjs → BlockNoteEditor-MB6LMBQQ.mjs} +2 -2
  2. package/dist/{BlockNoteEditor-A37P3FA7.js → BlockNoteEditor-SSFD4U5L.js} +6 -6
  3. package/dist/{BlockNoteEditor-A37P3FA7.js.map → BlockNoteEditor-SSFD4U5L.js.map} +1 -1
  4. package/dist/billing/index.js +299 -299
  5. package/dist/billing/index.mjs +1 -1
  6. package/dist/{chunk-XUTMY6K5.js → chunk-4C5ZDJV6.js} +844 -696
  7. package/dist/chunk-4C5ZDJV6.js.map +1 -0
  8. package/dist/{chunk-ZNODEBMI.mjs → chunk-JLZBOSTY.mjs} +2971 -2823
  9. package/dist/chunk-JLZBOSTY.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 +37 -3
  13. package/dist/components/index.d.ts +37 -3
  14. package/dist/components/index.js +6 -2
  15. package/dist/components/index.js.map +1 -1
  16. package/dist/components/index.mjs +5 -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/multi-selector.template.d.ts.map +1 -1
  20. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +14 -120
  21. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/generate-web-module/templates/components/multi-selector.template.ts +14 -120
  24. package/src/components/forms/EditorSheet.tsx +6 -2
  25. package/src/components/forms/EntityMultiSelector.tsx +325 -0
  26. package/src/components/forms/index.ts +1 -0
  27. package/src/features/how-to/components/forms/HowToMultiSelector.tsx +14 -120
  28. package/src/features/user/components/forms/UserMultiSelect.tsx +34 -181
  29. package/src/features/user/components/widgets/index.ts +1 -0
  30. package/dist/chunk-XUTMY6K5.js.map +0 -1
  31. package/dist/chunk-ZNODEBMI.mjs.map +0 -1
  32. /package/dist/{BlockNoteEditor-KJZ7FGBA.mjs.map → BlockNoteEditor-MB6LMBQQ.mjs.map} +0 -0
@@ -1,41 +1,13 @@
1
1
  "use client";
2
2
 
3
- import { Loader2 } from "lucide-react";
4
3
  import { useTranslations } from "next-intl";
5
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
- import { useWatch } from "react-hook-form";
7
- import { FormFieldWrapper } from "../../../../components/forms";
4
+ import { EntityMultiSelector } from "../../../../components/forms/EntityMultiSelector";
8
5
  import { Modules } from "../../../../core";
9
- import { DataListRetriever, useDataListRetriever, useDebounce } from "../../../../hooks";
10
- import { Avatar, AvatarFallback, AvatarImage, MultipleSelector } from "../../../../shadcnui";
11
- import { Option } from "../../../../shadcnui";
6
+ import { Avatar, AvatarFallback, AvatarImage } from "../../../../shadcnui";
12
7
  import { useCurrentUserContext } from "../../contexts";
13
8
  import { UserInterface } from "../../data";
14
9
  import { UserService } from "../../data/user.service";
15
10
 
16
- // Type for user objects in the form
17
- type UserSelectType = {
18
- id: string;
19
- name: string;
20
- avatar?: string;
21
- };
22
-
23
- type UserMultiSelectProps = {
24
- id: string;
25
- form: any;
26
- currentUser?: UserInterface;
27
- label?: string;
28
- placeholder?: string;
29
- onChange?: (users?: UserInterface[]) => void;
30
- maxCount?: number;
31
- isRequired?: boolean;
32
- };
33
-
34
- type UserOption = Option & {
35
- userData?: UserInterface;
36
- avatar?: string;
37
- };
38
-
39
11
  function UserAvatarIcon({ className, url, name }: { className?: string; url?: string; name?: string }) {
40
12
  return (
41
13
  <Avatar className={`${className || "h-4 w-4"}`}>
@@ -52,6 +24,17 @@ function UserAvatarIcon({ className, url, name }: { className?: string; url?: st
52
24
  );
53
25
  }
54
26
 
27
+ type UserMultiSelectProps = {
28
+ id: string;
29
+ form: any;
30
+ currentUser?: UserInterface;
31
+ label?: string;
32
+ placeholder?: string;
33
+ onChange?: (users?: UserInterface[]) => void;
34
+ maxCount?: number;
35
+ isRequired?: boolean;
36
+ };
37
+
55
38
  export function UserMultiSelect({
56
39
  id,
57
40
  form,
@@ -59,162 +42,32 @@ export function UserMultiSelect({
59
42
  label,
60
43
  placeholder,
61
44
  onChange,
62
- maxCount = 3,
63
45
  isRequired = false,
64
46
  }: UserMultiSelectProps) {
65
47
  const t = useTranslations();
66
48
  const { company } = useCurrentUserContext<UserInterface>();
67
49
 
68
- const searchTermRef = useRef<string>("");
69
- const [searchTerm, _setSearchTerm] = useState<string>("");
70
- const [isSearching, setIsSearching] = useState<boolean>(false);
71
- const [userOptions, setUserOptions] = useState<UserOption[]>([]);
72
-
73
- // Get the current selected users from the form
74
- const selectedUsers: UserSelectType[] = useWatch({ control: form.control, name: id }) || [];
75
-
76
- const data: DataListRetriever<UserInterface> = useDataListRetriever({
77
- ready: !!company,
78
- retriever: (params) => {
79
- return UserService.findAllUsers(params);
80
- },
81
- retrieverParams: { companyId: company?.id },
82
- module: Modules.User,
83
- }) as DataListRetriever<UserInterface>;
84
-
85
- useEffect(() => {
86
- if (company) data.setReady(true);
87
- }, [company]);
88
-
89
- const search = useCallback(
90
- async (searchedTerm: string) => {
91
- try {
92
- if (searchedTerm === searchTermRef.current) return;
93
- setIsSearching(true);
94
- searchTermRef.current = searchedTerm;
95
- await data.search(searchedTerm);
96
- } finally {
97
- setIsSearching(false);
98
- }
99
- },
100
- [searchTermRef, data],
101
- );
102
-
103
- const updateSearchTerm = useDebounce(search, 500);
104
-
105
- useEffect(() => {
106
- setIsSearching(true);
107
- updateSearchTerm(searchTerm);
108
- }, [updateSearchTerm, searchTerm]);
109
-
110
- // Update userOptions when data changes or when initial selected users are available
111
- useEffect(() => {
112
- if (data.data && data.data.length > 0) {
113
- const users = data.data as UserInterface[];
114
- const filteredUsers = users.filter((user) => user.id !== currentUser?.id);
115
-
116
- const options: UserOption[] = filteredUsers.map((user) => ({
117
- label: user.name,
118
- value: user.id,
119
- userData: user,
120
- avatar: user.avatar,
121
- }));
122
-
123
- // Add options for any already selected users that aren't in search results
124
- const existingOptionIds = new Set(options.map((option) => option.value));
125
- const missingOptions: UserOption[] = selectedUsers
126
- .filter((user) => !existingOptionIds.has(user.id))
127
- .map((user) => ({
128
- label: user.name,
129
- value: user.id,
130
- userData: user as unknown as UserInterface,
131
- avatar: user.avatar,
132
- }));
133
-
134
- setUserOptions([...options, ...missingOptions]);
135
- }
136
- }, [data.data, currentUser, selectedUsers]);
137
-
138
- // Convert selected users to Option[] format
139
- const selectedOptions = useMemo(() => {
140
- return selectedUsers.map((user) => ({
141
- value: user.id,
142
- label: user.name,
143
- }));
144
- }, [selectedUsers]);
145
-
146
- const handleChange = (options: Option[]) => {
147
- // Convert to form format
148
- const formValues = options.map((option) => {
149
- const userOption = userOptions.find((opt) => opt.value === option.value);
150
- return {
151
- id: option.value,
152
- name: option.label,
153
- avatar: userOption?.avatar,
154
- };
155
- });
156
-
157
- form.setValue(id, formValues, { shouldDirty: true, shouldTouch: true });
158
-
159
- if (onChange) {
160
- // Get full user data for onChange callback
161
- const fullUsers = options
162
- .map((option) => {
163
- const userOption = userOptions.find((opt) => opt.value === option.value);
164
- return userOption?.userData;
165
- })
166
- .filter(Boolean) as UserInterface[];
167
- onChange(fullUsers);
168
- }
169
- };
170
-
171
- // Custom render function for dropdown options (with avatar)
172
- const renderOption = (option: Option) => {
173
- const userOption = option as UserOption;
174
- return (
175
- <span className="flex items-center gap-2">
176
- <UserAvatarIcon url={userOption.avatar} name={option.label} />
177
- {option.label}
178
- </span>
179
- );
180
- };
181
-
182
- // Search handler
183
- const handleSearchSync = (search: string): Option[] => {
184
- _setSearchTerm(search);
185
- return userOptions;
186
- };
187
-
188
50
  return (
189
- <div className="flex w-full flex-col">
190
- <FormFieldWrapper form={form} name={id} label={label} isRequired={isRequired}>
191
- {() => (
192
- <MultipleSelector
193
- value={selectedOptions}
194
- onChange={handleChange}
195
- options={userOptions}
196
- placeholder={placeholder}
197
- maxDisplayCount={maxCount}
198
- hideClearAllButton
199
- onSearchSync={handleSearchSync}
200
- delay={0}
201
- renderOption={renderOption}
202
- loadingIndicator={
203
- isSearching ? (
204
- <div className="flex items-center justify-center py-2">
205
- <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
206
- <span className="ml-2 text-sm text-muted-foreground">{t("ui.search.button")}</span>
207
- </div>
208
- ) : undefined
209
- }
210
- emptyIndicator={
211
- <span className="text-muted-foreground">
212
- {t("ui.search.no_results", { type: t("entities.users", { count: 2 }) })}
213
- </span>
214
- }
215
- />
216
- )}
217
- </FormFieldWrapper>
218
- </div>
51
+ <EntityMultiSelector<UserInterface>
52
+ id={id}
53
+ form={form}
54
+ label={label}
55
+ placeholder={placeholder || t("ui.search.button")}
56
+ emptyText={t("ui.search.no_results", { type: t("entities.users", { count: 2 }) })}
57
+ isRequired={isRequired}
58
+ retriever={(params) => UserService.findAllUsers(params)}
59
+ retrieverParams={{ companyId: company?.id }}
60
+ module={Modules.User}
61
+ getLabel={(user) => user.name}
62
+ toFormValue={(user) => ({ id: user.id, name: user.name, avatar: user.avatar })}
63
+ excludeId={currentUser?.id}
64
+ onChange={onChange}
65
+ renderOption={(user) => (
66
+ <span className="flex items-center gap-2">
67
+ <UserAvatarIcon url={user.avatar} name={user.name} />
68
+ {user.name}
69
+ </span>
70
+ )}
71
+ />
219
72
  );
220
73
  }
@@ -1,2 +1,3 @@
1
1
  export * from "./UserAvatar";
2
2
  export * from "./UserAvatarList";
3
+ export * from "./UserSearchPopover";