@carlonicora/nextjs-jsonapi 1.47.2 → 1.48.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-JQNYMLFY.js → BlockNoteEditor-4ZNFPFIR.js} +8 -8
  2. package/dist/{BlockNoteEditor-JQNYMLFY.js.map → BlockNoteEditor-4ZNFPFIR.js.map} +1 -1
  3. package/dist/{BlockNoteEditor-CZXTJL3R.mjs → BlockNoteEditor-MY2WUSZY.mjs} +4 -4
  4. package/dist/{BlockNoteEditor-CZXTJL3R.mjs.map → BlockNoteEditor-MY2WUSZY.mjs.map} +1 -1
  5. package/dist/billing/index.js +299 -299
  6. package/dist/billing/index.mjs +1 -1
  7. package/dist/{chunk-3AYMUFM6.mjs → chunk-56YOTJYS.mjs} +2570 -2053
  8. package/dist/chunk-56YOTJYS.mjs.map +1 -0
  9. package/dist/{chunk-SUJ4GXAI.js → chunk-6RE6272L.js} +768 -251
  10. package/dist/chunk-6RE6272L.js.map +1 -0
  11. package/dist/client/index.js +2 -2
  12. package/dist/client/index.mjs +1 -1
  13. package/dist/components/index.d.mts +94 -2
  14. package/dist/components/index.d.ts +94 -2
  15. package/dist/components/index.js +6 -2
  16. package/dist/components/index.js.map +1 -1
  17. package/dist/components/index.mjs +5 -1
  18. package/dist/contexts/index.js +2 -2
  19. package/dist/contexts/index.mjs +1 -1
  20. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -1
  21. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +52 -54
  22. package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
  23. package/package.json +1 -1
  24. package/scripts/generate-web-module/templates/components/multi-selector.template.ts +52 -54
  25. package/src/components/editors/BlockNoteEditor.tsx +2 -2
  26. package/src/features/company/hooks/useSubscriptionStatus.ts +12 -0
  27. package/src/features/user/components/forms/UserMultiSelect.tsx +82 -64
  28. package/src/shadcnui/custom/multi-select.tsx +86 -22
  29. package/src/shadcnui/custom/multiple-selector.tsx +608 -0
  30. package/src/shadcnui/index.ts +1 -0
  31. package/dist/chunk-3AYMUFM6.mjs.map +0 -1
  32. package/dist/chunk-SUJ4GXAI.js.map +0 -1
@@ -16,14 +16,13 @@ function generateMultiSelectorTemplate(data) {
16
16
  const { names } = data;
17
17
  return `"use client";
18
18
 
19
- import { FormFieldWrapper, MultiSelect } from "@carlonicora/nextjs-jsonapi/components";
20
19
  import { ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
21
20
  import { ${names.pascalCase}Service } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
22
- import { DataListRetriever, useDataListRetriever } from "@carlonicora/nextjs-jsonapi/client";
23
- import { useDebounce } from "@carlonicora/nextjs-jsonapi/client";
21
+ import { DataListRetriever, useDataListRetriever, useDebounce } from "@carlonicora/nextjs-jsonapi/client";
22
+ import { FormFieldWrapper, MultipleSelector } from "@carlonicora/nextjs-jsonapi/components";
23
+ import { Option } from "@carlonicora/nextjs-jsonapi/components";
24
24
  import { Modules } from "@carlonicora/nextjs-jsonapi/core";
25
- import { useTranslations } from "next-intl";
26
- import { useCallback, useEffect, useState } from "react";
25
+ import { useCallback, useEffect, useMemo, useState } from "react";
27
26
  import { useWatch } from "react-hook-form";
28
27
 
29
28
  type ${names.pascalCase}MultiSelectType = {
@@ -42,6 +41,10 @@ type ${names.pascalCase}MultiSelectorProps = {
42
41
  isRequired?: boolean;
43
42
  };
44
43
 
44
+ type ${names.pascalCase}Option = Option & {
45
+ ${names.camelCase}Data?: ${names.pascalCase}Interface;
46
+ };
47
+
45
48
  export default function ${names.pascalCase}MultiSelector({
46
49
  id,
47
50
  form,
@@ -52,8 +55,7 @@ export default function ${names.pascalCase}MultiSelector({
52
55
  maxCount = 3,
53
56
  isRequired = false,
54
57
  }: ${names.pascalCase}MultiSelectorProps) {
55
- const t = useTranslations("features.${names.camelCase}");
56
- const [${names.camelCase}Options, set${names.pascalCase}Options] = useState<any[]>([]);
58
+ const [${names.camelCase}Options, set${names.pascalCase}Options] = useState<${names.pascalCase}Option[]>([]);
57
59
  const [searchTerm, setSearchTerm] = useState<string>("");
58
60
 
59
61
  const selected${names.pluralPascal}: ${names.pascalCase}MultiSelectType[] = useWatch({ control: form.control, name: id }) || [];
@@ -73,7 +75,7 @@ export default function ${names.pascalCase}MultiSelector({
73
75
  data.removeAdditionalParameter("search");
74
76
  }
75
77
  },
76
- [data],
78
+ [data]
77
79
  );
78
80
 
79
81
  const debouncedUpdateSearch = useDebounce(updateSearch, 500);
@@ -87,79 +89,75 @@ export default function ${names.pascalCase}MultiSelector({
87
89
  const ${names.pluralCamel} = data.data as ${names.pascalCase}Interface[];
88
90
  const filtered${names.pluralPascal} = ${names.pluralCamel}.filter((${names.camelCase}) => ${names.camelCase}.id !== current${names.pascalCase}?.id);
89
91
 
90
- const options = filtered${names.pluralPascal}.map((${names.camelCase}) => ({
92
+ const options: ${names.pascalCase}Option[] = filtered${names.pluralPascal}.map((${names.camelCase}) => ({
91
93
  label: ${names.camelCase}.name,
92
94
  value: ${names.camelCase}.id,
93
95
  ${names.camelCase}Data: ${names.camelCase},
94
96
  }));
95
97
 
96
- set${names.pascalCase}Options(options);
97
- }
98
- }, [data.data, current${names.pascalCase}]);
99
-
100
- // Add options for any already selected ${names.pluralCamel} that aren't in search results
101
- useEffect(() => {
102
- if (selected${names.pluralPascal}.length > 0) {
103
- // Create a map of existing option IDs for quick lookup
104
- const existingOptionIds = new Set(${names.camelCase}Options.map((option) => option.value));
105
-
106
- // Find selected ${names.pluralCamel} that don't have an option yet
107
- const missingOptions = selected${names.pluralPascal}
98
+ // Add options for any already selected that aren't in search results
99
+ const existingOptionIds = new Set(options.map((option) => option.value));
100
+ const missingOptions: ${names.pascalCase}Option[] = selected${names.pluralPascal}
108
101
  .filter((${names.camelCase}) => !existingOptionIds.has(${names.camelCase}.id))
109
102
  .map((${names.camelCase}) => ({
110
103
  label: ${names.camelCase}.name,
111
104
  value: ${names.camelCase}.id,
112
- ${names.camelCase}Data: ${names.camelCase},
105
+ ${names.camelCase}Data: ${names.camelCase} as unknown as ${names.pascalCase}Interface,
113
106
  }));
114
107
 
115
- if (missingOptions.length > 0) {
116
- set${names.pascalCase}Options((prev) => [...prev, ...missingOptions]);
117
- }
108
+ set${names.pascalCase}Options([...options, ...missingOptions]);
118
109
  }
119
- }, [selected${names.pluralPascal}, ${names.camelCase}Options]);
110
+ }, [data.data, current${names.pascalCase}, selected${names.pluralPascal}]);
120
111
 
121
- const handleValueChange = (selectedIds: string[]) => {
122
- const updatedSelected${names.pluralPascal} = selectedIds.map((id) => {
123
- const existing${names.pascalCase} = selected${names.pluralPascal}.find((${names.camelCase}) => ${names.camelCase}.id === id);
124
- if (existing${names.pascalCase}) {
125
- return existing${names.pascalCase};
126
- }
112
+ // Convert selected to Option[] format
113
+ const selectedOptions = useMemo(() => {
114
+ return selected${names.pluralPascal}.map((${names.camelCase}) => ({
115
+ value: ${names.camelCase}.id,
116
+ label: ${names.camelCase}.name,
117
+ }));
118
+ }, [selected${names.pluralPascal}]);
127
119
 
128
- const option = ${names.camelCase}Options.find((option) => option.value === id);
129
- if (option?.${names.camelCase}Data) {
130
- return {
131
- id: option.${names.camelCase}Data.id,
132
- name: option.${names.camelCase}Data.name,
133
- };
134
- }
120
+ const handleChange = (options: Option[]) => {
121
+ // Convert to form format
122
+ const formValues = options.map((option) => ({
123
+ id: option.value,
124
+ name: option.label,
125
+ }));
135
126
 
136
- return { id, name: id };
137
- });
138
-
139
- form.setValue(id, updatedSelected${names.pluralPascal});
127
+ form.setValue(id, formValues, { shouldDirty: true, shouldTouch: true });
140
128
 
141
129
  if (onChange) {
142
- const fullSelected${names.pluralPascal} = selectedIds
143
- .map((id) => ${names.camelCase}Options.find((option) => option.value === id)?.${names.camelCase}Data)
130
+ // Get full data for onChange callback
131
+ const fullData = options
132
+ .map((option) => {
133
+ const ${names.camelCase}Option = ${names.camelCase}Options.find((opt) => opt.value === option.value);
134
+ return ${names.camelCase}Option?.${names.camelCase}Data;
135
+ })
144
136
  .filter(Boolean) as ${names.pascalCase}Interface[];
145
- onChange(fullSelected${names.pluralPascal});
137
+ onChange(fullData);
146
138
  }
147
139
  };
148
140
 
149
- const selected${names.pascalCase}Ids = selected${names.pluralPascal}.map((${names.camelCase}: ${names.pascalCase}MultiSelectType) => ${names.camelCase}.id);
141
+ // Search handler
142
+ const handleSearchSync = (search: string): Option[] => {
143
+ setSearchTerm(search);
144
+ return ${names.camelCase}Options;
145
+ };
150
146
 
151
147
  return (
152
148
  <div className="flex w-full flex-col">
153
149
  <FormFieldWrapper form={form} name={id} label={label} isRequired={isRequired}>
154
- {(field) => (
155
- <MultiSelect
150
+ {() => (
151
+ <MultipleSelector
152
+ value={selectedOptions}
153
+ onChange={handleChange}
156
154
  options={${names.camelCase}Options}
157
- onValueChange={handleValueChange}
158
- defaultValue={selected${names.pascalCase}Ids}
159
155
  placeholder={placeholder}
160
- maxCount={maxCount}
161
- animation={0}
162
- onSearchChange={setSearchTerm}
156
+ maxDisplayCount={maxCount}
157
+ hideClearAllButton
158
+ onSearchSync={handleSearchSync}
159
+ delay={0}
160
+ emptyIndicator={<span className="text-muted-foreground">No results found</span>}
163
161
  />
164
162
  )}
165
163
  </FormFieldWrapper>
@@ -1 +1 @@
1
- {"version":3,"file":"multi-selector.template.js","sourceRoot":"","sources":["../../../../../scripts/generate-web-module/templates/components/multi-selector.template.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAUH,sEA4JC;AAlKD;;;;;GAKG;AACH,SAAgB,6BAA6B,CAAC,IAA0B;IACtE,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAEvB,OAAO;;;WAGE,KAAK,CAAC,UAAU,gCAAgC,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,UAAU;WAChH,KAAK,CAAC,UAAU,8BAA8B,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,UAAU;;;;;;;;OAQlH,KAAK,CAAC,UAAU;;;;;OAKhB,KAAK,CAAC,UAAU;;;WAGZ,KAAK,CAAC,UAAU,MAAM,KAAK,CAAC,UAAU;;;gBAGjC,KAAK,CAAC,WAAW,MAAM,KAAK,CAAC,UAAU;;;;;0BAK7B,KAAK,CAAC,UAAU;;;WAG/B,KAAK,CAAC,UAAU;;;;;;KAMtB,KAAK,CAAC,UAAU;wCACmB,KAAK,CAAC,SAAS;WAC5C,KAAK,CAAC,SAAS,eAAe,KAAK,CAAC,UAAU;;;kBAGvC,KAAK,CAAC,YAAY,KAAK,KAAK,CAAC,UAAU;;kCAEvB,KAAK,CAAC,UAAU;6BACrB,KAAK,CAAC,UAAU;;;sBAGvB,KAAK,CAAC,UAAU;;;;;;;;;;;;;;;;;;;;;;cAsBxB,KAAK,CAAC,WAAW,mBAAmB,KAAK,CAAC,UAAU;sBAC5C,KAAK,CAAC,YAAY,MAAM,KAAK,CAAC,WAAW,YAAY,KAAK,CAAC,SAAS,QAAQ,KAAK,CAAC,SAAS,kBAAkB,KAAK,CAAC,UAAU;;gCAEnH,KAAK,CAAC,YAAY,SAAS,KAAK,CAAC,SAAS;iBACzD,KAAK,CAAC,SAAS;iBACf,KAAK,CAAC,SAAS;UACtB,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,SAAS;;;WAGtC,KAAK,CAAC,UAAU;;0BAED,KAAK,CAAC,UAAU;;4CAEE,KAAK,CAAC,WAAW;;kBAE3C,KAAK,CAAC,YAAY;;0CAEM,KAAK,CAAC,SAAS;;yBAEhC,KAAK,CAAC,WAAW;uCACH,KAAK,CAAC,YAAY;mBACtC,KAAK,CAAC,SAAS,+BAA+B,KAAK,CAAC,SAAS;gBAChE,KAAK,CAAC,SAAS;mBACZ,KAAK,CAAC,SAAS;mBACf,KAAK,CAAC,SAAS;YACtB,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,SAAS;;;;aAItC,KAAK,CAAC,UAAU;;;gBAGb,KAAK,CAAC,YAAY,KAAK,KAAK,CAAC,SAAS;;;2BAG3B,KAAK,CAAC,YAAY;sBACvB,KAAK,CAAC,UAAU,cAAc,KAAK,CAAC,YAAY,UAAU,KAAK,CAAC,SAAS,QAAQ,KAAK,CAAC,SAAS;oBAClG,KAAK,CAAC,UAAU;yBACX,KAAK,CAAC,UAAU;;;uBAGlB,KAAK,CAAC,SAAS;oBAClB,KAAK,CAAC,SAAS;;uBAEZ,KAAK,CAAC,SAAS;yBACb,KAAK,CAAC,SAAS;;;;;;;uCAOD,KAAK,CAAC,YAAY;;;0BAG/B,KAAK,CAAC,YAAY;uBACrB,KAAK,CAAC,SAAS,kDAAkD,KAAK,CAAC,SAAS;8BACzE,KAAK,CAAC,UAAU;6BACjB,KAAK,CAAC,YAAY;;;;kBAI7B,KAAK,CAAC,UAAU,iBAAiB,KAAK,CAAC,YAAY,SAAS,KAAK,CAAC,SAAS,KAAK,KAAK,CAAC,UAAU,uBAAuB,KAAK,CAAC,SAAS;;;;;;;uBAOjI,KAAK,CAAC,SAAS;;oCAEF,KAAK,CAAC,UAAU;;;;;;;;;;;CAWnD,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"multi-selector.template.js","sourceRoot":"","sources":["../../../../../scripts/generate-web-module/templates/components/multi-selector.template.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;AAUH,sEA0JC;AAhKD;;;;;GAKG;AACH,SAAgB,6BAA6B,CAAC,IAA0B;IACtE,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IAEvB,OAAO;;WAEE,KAAK,CAAC,UAAU,gCAAgC,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,UAAU;WAChH,KAAK,CAAC,UAAU,8BAA8B,IAAI,CAAC,eAAe,IAAI,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,UAAU;;;;;;;;OAQlH,KAAK,CAAC,UAAU;;;;;OAKhB,KAAK,CAAC,UAAU;;;WAGZ,KAAK,CAAC,UAAU,MAAM,KAAK,CAAC,UAAU;;;gBAGjC,KAAK,CAAC,WAAW,MAAM,KAAK,CAAC,UAAU;;;;;OAKhD,KAAK,CAAC,UAAU;IACnB,KAAK,CAAC,SAAS,UAAU,KAAK,CAAC,UAAU;;;0BAGnB,KAAK,CAAC,UAAU;;;WAG/B,KAAK,CAAC,UAAU;;;;;;KAMtB,KAAK,CAAC,UAAU;WACV,KAAK,CAAC,SAAS,eAAe,KAAK,CAAC,UAAU,uBAAuB,KAAK,CAAC,UAAU;;;kBAG9E,KAAK,CAAC,YAAY,KAAK,KAAK,CAAC,UAAU;;kCAEvB,KAAK,CAAC,UAAU;6BACrB,KAAK,CAAC,UAAU;;;sBAGvB,KAAK,CAAC,UAAU;;;;;;;;;;;;;;;;;;;;;;cAsBxB,KAAK,CAAC,WAAW,mBAAmB,KAAK,CAAC,UAAU;sBAC5C,KAAK,CAAC,YAAY,MAAM,KAAK,CAAC,WAAW,YAAY,KAAK,CAAC,SAAS,QAAQ,KAAK,CAAC,SAAS,kBAAkB,KAAK,CAAC,UAAU;;uBAE5H,KAAK,CAAC,UAAU,sBAAsB,KAAK,CAAC,YAAY,SAAS,KAAK,CAAC,SAAS;iBACtF,KAAK,CAAC,SAAS;iBACf,KAAK,CAAC,SAAS;UACtB,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,SAAS;;;;;8BAKnB,KAAK,CAAC,UAAU,sBAAsB,KAAK,CAAC,YAAY;mBACnE,KAAK,CAAC,SAAS,+BAA+B,KAAK,CAAC,SAAS;gBAChE,KAAK,CAAC,SAAS;mBACZ,KAAK,CAAC,SAAS;mBACf,KAAK,CAAC,SAAS;YACtB,KAAK,CAAC,SAAS,SAAS,KAAK,CAAC,SAAS,kBAAkB,KAAK,CAAC,UAAU;;;WAG1E,KAAK,CAAC,UAAU;;0BAED,KAAK,CAAC,UAAU,aAAa,KAAK,CAAC,YAAY;;;;qBAIpD,KAAK,CAAC,YAAY,SAAS,KAAK,CAAC,SAAS;eAChD,KAAK,CAAC,SAAS;eACf,KAAK,CAAC,SAAS;;gBAEd,KAAK,CAAC,YAAY;;;;;;;;;;;;;;;kBAehB,KAAK,CAAC,SAAS,YAAY,KAAK,CAAC,SAAS;mBACzC,KAAK,CAAC,SAAS,WAAW,KAAK,CAAC,SAAS;;8BAE9B,KAAK,CAAC,UAAU;;;;;;;;aAQjC,KAAK,CAAC,SAAS;;;;;;;;;;uBAUL,KAAK,CAAC,SAAS;;;;;;;;;;;;;CAarC,CAAC;AACF,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.47.2",
3
+ "version": "1.48.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",
@@ -17,14 +17,13 @@ export function generateMultiSelectorTemplate(data: FrontendTemplateData): strin
17
17
 
18
18
  return `"use client";
19
19
 
20
- import { FormFieldWrapper, MultiSelect } from "@carlonicora/nextjs-jsonapi/components";
21
20
  import { ${names.pascalCase}Interface } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Interface";
22
21
  import { ${names.pascalCase}Service } from "@/features/${data.importTargetDir}/${names.kebabCase}/data/${names.pascalCase}Service";
23
- import { DataListRetriever, useDataListRetriever } from "@carlonicora/nextjs-jsonapi/client";
24
- import { useDebounce } from "@carlonicora/nextjs-jsonapi/client";
22
+ import { DataListRetriever, useDataListRetriever, useDebounce } from "@carlonicora/nextjs-jsonapi/client";
23
+ import { FormFieldWrapper, MultipleSelector } from "@carlonicora/nextjs-jsonapi/components";
24
+ import { Option } from "@carlonicora/nextjs-jsonapi/components";
25
25
  import { Modules } from "@carlonicora/nextjs-jsonapi/core";
26
- import { useTranslations } from "next-intl";
27
- import { useCallback, useEffect, useState } from "react";
26
+ import { useCallback, useEffect, useMemo, useState } from "react";
28
27
  import { useWatch } from "react-hook-form";
29
28
 
30
29
  type ${names.pascalCase}MultiSelectType = {
@@ -43,6 +42,10 @@ type ${names.pascalCase}MultiSelectorProps = {
43
42
  isRequired?: boolean;
44
43
  };
45
44
 
45
+ type ${names.pascalCase}Option = Option & {
46
+ ${names.camelCase}Data?: ${names.pascalCase}Interface;
47
+ };
48
+
46
49
  export default function ${names.pascalCase}MultiSelector({
47
50
  id,
48
51
  form,
@@ -53,8 +56,7 @@ export default function ${names.pascalCase}MultiSelector({
53
56
  maxCount = 3,
54
57
  isRequired = false,
55
58
  }: ${names.pascalCase}MultiSelectorProps) {
56
- const t = useTranslations("features.${names.camelCase}");
57
- const [${names.camelCase}Options, set${names.pascalCase}Options] = useState<any[]>([]);
59
+ const [${names.camelCase}Options, set${names.pascalCase}Options] = useState<${names.pascalCase}Option[]>([]);
58
60
  const [searchTerm, setSearchTerm] = useState<string>("");
59
61
 
60
62
  const selected${names.pluralPascal}: ${names.pascalCase}MultiSelectType[] = useWatch({ control: form.control, name: id }) || [];
@@ -74,7 +76,7 @@ export default function ${names.pascalCase}MultiSelector({
74
76
  data.removeAdditionalParameter("search");
75
77
  }
76
78
  },
77
- [data],
79
+ [data]
78
80
  );
79
81
 
80
82
  const debouncedUpdateSearch = useDebounce(updateSearch, 500);
@@ -88,79 +90,75 @@ export default function ${names.pascalCase}MultiSelector({
88
90
  const ${names.pluralCamel} = data.data as ${names.pascalCase}Interface[];
89
91
  const filtered${names.pluralPascal} = ${names.pluralCamel}.filter((${names.camelCase}) => ${names.camelCase}.id !== current${names.pascalCase}?.id);
90
92
 
91
- const options = filtered${names.pluralPascal}.map((${names.camelCase}) => ({
93
+ const options: ${names.pascalCase}Option[] = filtered${names.pluralPascal}.map((${names.camelCase}) => ({
92
94
  label: ${names.camelCase}.name,
93
95
  value: ${names.camelCase}.id,
94
96
  ${names.camelCase}Data: ${names.camelCase},
95
97
  }));
96
98
 
97
- set${names.pascalCase}Options(options);
98
- }
99
- }, [data.data, current${names.pascalCase}]);
100
-
101
- // Add options for any already selected ${names.pluralCamel} that aren't in search results
102
- useEffect(() => {
103
- if (selected${names.pluralPascal}.length > 0) {
104
- // Create a map of existing option IDs for quick lookup
105
- const existingOptionIds = new Set(${names.camelCase}Options.map((option) => option.value));
106
-
107
- // Find selected ${names.pluralCamel} that don't have an option yet
108
- const missingOptions = selected${names.pluralPascal}
99
+ // Add options for any already selected that aren't in search results
100
+ const existingOptionIds = new Set(options.map((option) => option.value));
101
+ const missingOptions: ${names.pascalCase}Option[] = selected${names.pluralPascal}
109
102
  .filter((${names.camelCase}) => !existingOptionIds.has(${names.camelCase}.id))
110
103
  .map((${names.camelCase}) => ({
111
104
  label: ${names.camelCase}.name,
112
105
  value: ${names.camelCase}.id,
113
- ${names.camelCase}Data: ${names.camelCase},
106
+ ${names.camelCase}Data: ${names.camelCase} as unknown as ${names.pascalCase}Interface,
114
107
  }));
115
108
 
116
- if (missingOptions.length > 0) {
117
- set${names.pascalCase}Options((prev) => [...prev, ...missingOptions]);
118
- }
109
+ set${names.pascalCase}Options([...options, ...missingOptions]);
119
110
  }
120
- }, [selected${names.pluralPascal}, ${names.camelCase}Options]);
111
+ }, [data.data, current${names.pascalCase}, selected${names.pluralPascal}]);
121
112
 
122
- const handleValueChange = (selectedIds: string[]) => {
123
- const updatedSelected${names.pluralPascal} = selectedIds.map((id) => {
124
- const existing${names.pascalCase} = selected${names.pluralPascal}.find((${names.camelCase}) => ${names.camelCase}.id === id);
125
- if (existing${names.pascalCase}) {
126
- return existing${names.pascalCase};
127
- }
113
+ // Convert selected to Option[] format
114
+ const selectedOptions = useMemo(() => {
115
+ return selected${names.pluralPascal}.map((${names.camelCase}) => ({
116
+ value: ${names.camelCase}.id,
117
+ label: ${names.camelCase}.name,
118
+ }));
119
+ }, [selected${names.pluralPascal}]);
128
120
 
129
- const option = ${names.camelCase}Options.find((option) => option.value === id);
130
- if (option?.${names.camelCase}Data) {
131
- return {
132
- id: option.${names.camelCase}Data.id,
133
- name: option.${names.camelCase}Data.name,
134
- };
135
- }
121
+ const handleChange = (options: Option[]) => {
122
+ // Convert to form format
123
+ const formValues = options.map((option) => ({
124
+ id: option.value,
125
+ name: option.label,
126
+ }));
136
127
 
137
- return { id, name: id };
138
- });
139
-
140
- form.setValue(id, updatedSelected${names.pluralPascal});
128
+ form.setValue(id, formValues, { shouldDirty: true, shouldTouch: true });
141
129
 
142
130
  if (onChange) {
143
- const fullSelected${names.pluralPascal} = selectedIds
144
- .map((id) => ${names.camelCase}Options.find((option) => option.value === id)?.${names.camelCase}Data)
131
+ // Get full data for onChange callback
132
+ const fullData = options
133
+ .map((option) => {
134
+ const ${names.camelCase}Option = ${names.camelCase}Options.find((opt) => opt.value === option.value);
135
+ return ${names.camelCase}Option?.${names.camelCase}Data;
136
+ })
145
137
  .filter(Boolean) as ${names.pascalCase}Interface[];
146
- onChange(fullSelected${names.pluralPascal});
138
+ onChange(fullData);
147
139
  }
148
140
  };
149
141
 
150
- const selected${names.pascalCase}Ids = selected${names.pluralPascal}.map((${names.camelCase}: ${names.pascalCase}MultiSelectType) => ${names.camelCase}.id);
142
+ // Search handler
143
+ const handleSearchSync = (search: string): Option[] => {
144
+ setSearchTerm(search);
145
+ return ${names.camelCase}Options;
146
+ };
151
147
 
152
148
  return (
153
149
  <div className="flex w-full flex-col">
154
150
  <FormFieldWrapper form={form} name={id} label={label} isRequired={isRequired}>
155
- {(field) => (
156
- <MultiSelect
151
+ {() => (
152
+ <MultipleSelector
153
+ value={selectedOptions}
154
+ onChange={handleChange}
157
155
  options={${names.camelCase}Options}
158
- onValueChange={handleValueChange}
159
- defaultValue={selected${names.pascalCase}Ids}
160
156
  placeholder={placeholder}
161
- maxCount={maxCount}
162
- animation={0}
163
- onSearchChange={setSearchTerm}
157
+ maxDisplayCount={maxCount}
158
+ hideClearAllButton
159
+ onSearchSync={handleSearchSync}
160
+ delay={0}
161
+ emptyIndicator={<span className="text-muted-foreground">No results found</span>}
164
162
  />
165
163
  )}
166
164
  </FormFieldWrapper>
@@ -390,14 +390,14 @@ export default function BlockNoteEditor({
390
390
  );
391
391
 
392
392
  return (
393
- <div ref={editorRef} className={cn(bordered ? "rounded-md border" : "", "w-full")}>
393
+ <div ref={editorRef} className={cn(bordered ? "rounded-md border" : "", "flex flex-col w-full", className)}>
394
394
  <BlockNoteView
395
395
  editor={editor}
396
396
  onChange={handleChange}
397
397
  editable={onChange !== undefined}
398
398
  formattingToolbar={false}
399
399
  theme="light"
400
- className={cn(`BlockNoteView ${onChange ? "min-h-96 p-4" : ""}`, className, size === "sm" && "small")}
400
+ className={cn(`BlockNoteView flex-1 ${onChange ? "p-4" : ""}`, size === "sm" && "small")}
401
401
  >
402
402
  <BlockNoteEditorFormattingToolbar />
403
403
  </BlockNoteView>
@@ -3,6 +3,7 @@
3
3
  import { useMemo } from "react";
4
4
  import { useCurrentUserContext } from "../../user/contexts/CurrentUserContext";
5
5
  import { getRoleId, isRolesConfigured } from "../../../roles";
6
+ import { getStripePublishableKey } from "../../../client/config";
6
7
 
7
8
  export interface TrialSubscriptionStatus {
8
9
  status: "loading" | "trial" | "active" | "expired";
@@ -36,6 +37,17 @@ export function useSubscriptionStatus(): TrialSubscriptionStatus {
36
37
  };
37
38
  }
38
39
 
40
+ // If Stripe is not configured, treat as active subscription (no trial/blocking features)
41
+ if (!getStripePublishableKey()) {
42
+ return {
43
+ status: "active",
44
+ trialEndsAt: null,
45
+ daysRemaining: 0,
46
+ isGracePeriod: false,
47
+ isBlocked: false,
48
+ };
49
+ }
50
+
39
51
  // Administrator users are never blocked by trial
40
52
  if (isAdministrator(currentUser)) {
41
53
  return {
@@ -1,12 +1,14 @@
1
1
  "use client";
2
2
 
3
+ import { Loader2 } from "lucide-react";
3
4
  import { useTranslations } from "next-intl";
4
- import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
6
  import { useWatch } from "react-hook-form";
6
7
  import { FormFieldWrapper } from "../../../../components/forms";
7
8
  import { Modules } from "../../../../core";
8
9
  import { DataListRetriever, useDataListRetriever, useDebounce } from "../../../../hooks";
9
- import { Avatar, AvatarFallback, AvatarImage, MultiSelect } from "../../../../shadcnui";
10
+ import { Avatar, AvatarFallback, AvatarImage, MultipleSelector } from "../../../../shadcnui";
11
+ import { Option } from "../../../../shadcnui";
10
12
  import { useCurrentUserContext } from "../../contexts";
11
13
  import { UserInterface } from "../../data";
12
14
  import { UserService } from "../../data/user.service";
@@ -29,6 +31,11 @@ type UserMultiSelectProps = {
29
31
  isRequired?: boolean;
30
32
  };
31
33
 
34
+ type UserOption = Option & {
35
+ userData?: UserInterface;
36
+ avatar?: string;
37
+ };
38
+
32
39
  function UserAvatarIcon({ className, url, name }: { className?: string; url?: string; name?: string }) {
33
40
  return (
34
41
  <Avatar className={`${className || "h-4 w-4"}`}>
@@ -61,7 +68,7 @@ export function UserMultiSelect({
61
68
  const searchTermRef = useRef<string>("");
62
69
  const [searchTerm, _setSearchTerm] = useState<string>("");
63
70
  const [isSearching, setIsSearching] = useState<boolean>(false);
64
- const [userOptions, setUserOptions] = useState<any[]>([]);
71
+ const [userOptions, setUserOptions] = useState<UserOption[]>([]);
65
72
 
66
73
  // Get the current selected users from the form
67
74
  const selectedUsers: UserSelectType[] = useWatch({ control: form.control, name: id }) || [];
@@ -106,94 +113,105 @@ export function UserMultiSelect({
106
113
  const users = data.data as UserInterface[];
107
114
  const filteredUsers = users.filter((user) => user.id !== currentUser?.id);
108
115
 
109
- const options = filteredUsers.map((user) => ({
116
+ const options: UserOption[] = filteredUsers.map((user) => ({
110
117
  label: user.name,
111
118
  value: user.id,
112
- icon: ({ className }: { className?: string }) => (
113
- <UserAvatarIcon className={className} url={user.avatar} name={user.name} />
114
- ),
115
119
  userData: user,
120
+ avatar: user.avatar,
116
121
  }));
117
122
 
118
- setUserOptions(options);
119
- }
120
- }, [data.data, currentUser]);
121
-
122
- // Add options for any already selected users that aren't in search results
123
- useEffect(() => {
124
- if (selectedUsers.length > 0) {
125
- // Create a map of existing option IDs for quick lookup
126
- const existingOptionIds = new Set(userOptions.map((option) => option.value));
127
-
128
- // Find selected users that don't have an option yet
129
- const missingOptions = selectedUsers
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
130
126
  .filter((user) => !existingOptionIds.has(user.id))
131
127
  .map((user) => ({
132
128
  label: user.name,
133
129
  value: user.id,
134
- icon: ({ className }: { className?: string }) => (
135
- <UserAvatarIcon className={className} url={user.avatar} name={user.name} />
136
- ),
137
- userData: user,
130
+ userData: user as unknown as UserInterface,
131
+ avatar: user.avatar,
138
132
  }));
139
133
 
140
- // Add missing options if there are any
141
- if (missingOptions.length > 0) {
142
- setUserOptions((prev) => [...prev, ...missingOptions]);
143
- }
134
+ setUserOptions([...options, ...missingOptions]);
144
135
  }
145
- }, [selectedUsers, userOptions]);
146
-
147
- const handleValueChange = (selectedIds: string[]) => {
148
- // Map selected IDs to user objects for the form
149
- const updatedSelectedUsers = selectedIds.map((id) => {
150
- // First check if user is already in the selected users (preserve existing data)
151
- const existingUser = selectedUsers.find((user) => user.id === id);
152
- if (existingUser) {
153
- return existingUser;
154
- }
155
-
156
- // Otherwise, get user data from the options
157
- const option = userOptions.find((option) => option.value === id);
158
- if (option?.userData) {
159
- return {
160
- id: option.userData.id,
161
- name: option.userData.name,
162
- avatar: option.userData.avatar,
163
- };
164
- }
165
-
166
- // Fallback to just the ID if no data is available
167
- return { id, name: id };
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
+ };
168
155
  });
169
156
 
170
- form.setValue(id, updatedSelectedUsers);
157
+ form.setValue(id, formValues, { shouldDirty: true, shouldTouch: true });
171
158
 
172
159
  if (onChange) {
173
- const fullSelectedUsers = selectedIds
174
- .map((id) => userOptions.find((option) => option.value === id)?.userData)
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
+ })
175
166
  .filter(Boolean) as UserInterface[];
176
- onChange(fullSelectedUsers);
167
+ onChange(fullUsers);
177
168
  }
178
169
  };
179
170
 
180
- // Extract just the IDs for the MultiSelect component
181
- const selectedUserIds = selectedUsers.map((user: UserSelectType) => user.id);
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
+ };
182
187
 
183
188
  return (
184
189
  <div className="flex w-full flex-col">
185
190
  <FormFieldWrapper form={form} name={id} label={label} isRequired={isRequired}>
186
191
  {() => (
187
- <MultiSelect
192
+ <MultipleSelector
193
+ value={selectedOptions}
194
+ onChange={handleChange}
188
195
  options={userOptions}
189
- onValueChange={handleValueChange}
190
- defaultValue={selectedUserIds}
191
196
  placeholder={placeholder}
192
- maxCount={maxCount}
193
- animation={0}
194
- loading={isSearching}
195
- loadingText={t("ui.search.button")}
196
- emptyText={t("ui.search.no_results", { type: t("entities.users", { count: 2 }) })}
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
+ }
197
215
  />
198
216
  )}
199
217
  </FormFieldWrapper>