@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.
- package/dist/{BlockNoteEditor-JQNYMLFY.js → BlockNoteEditor-4ZNFPFIR.js} +8 -8
- package/dist/{BlockNoteEditor-JQNYMLFY.js.map → BlockNoteEditor-4ZNFPFIR.js.map} +1 -1
- package/dist/{BlockNoteEditor-CZXTJL3R.mjs → BlockNoteEditor-MY2WUSZY.mjs} +4 -4
- package/dist/{BlockNoteEditor-CZXTJL3R.mjs.map → BlockNoteEditor-MY2WUSZY.mjs.map} +1 -1
- package/dist/billing/index.js +299 -299
- package/dist/billing/index.mjs +1 -1
- package/dist/{chunk-3AYMUFM6.mjs → chunk-56YOTJYS.mjs} +2570 -2053
- package/dist/chunk-56YOTJYS.mjs.map +1 -0
- package/dist/{chunk-SUJ4GXAI.js → chunk-6RE6272L.js} +768 -251
- package/dist/chunk-6RE6272L.js.map +1 -0
- package/dist/client/index.js +2 -2
- package/dist/client/index.mjs +1 -1
- package/dist/components/index.d.mts +94 -2
- package/dist/components/index.d.ts +94 -2
- package/dist/components/index.js +6 -2
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +5 -1
- package/dist/contexts/index.js +2 -2
- package/dist/contexts/index.mjs +1 -1
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.d.ts.map +1 -1
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js +52 -54
- package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map +1 -1
- package/package.json +1 -1
- package/scripts/generate-web-module/templates/components/multi-selector.template.ts +52 -54
- package/src/components/editors/BlockNoteEditor.tsx +2 -2
- package/src/features/company/hooks/useSubscriptionStatus.ts +12 -0
- package/src/features/user/components/forms/UserMultiSelect.tsx +82 -64
- package/src/shadcnui/custom/multi-select.tsx +86 -22
- package/src/shadcnui/custom/multiple-selector.tsx +608 -0
- package/src/shadcnui/index.ts +1 -0
- package/dist/chunk-3AYMUFM6.mjs.map +0 -1
- 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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
116
|
-
set${names.pascalCase}Options((prev) => [...prev, ...missingOptions]);
|
|
117
|
-
}
|
|
108
|
+
set${names.pascalCase}Options([...options, ...missingOptions]);
|
|
118
109
|
}
|
|
119
|
-
}, [
|
|
110
|
+
}, [data.data, current${names.pascalCase}, selected${names.pluralPascal}]);
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
143
|
-
|
|
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(
|
|
137
|
+
onChange(fullData);
|
|
146
138
|
}
|
|
147
139
|
};
|
|
148
140
|
|
|
149
|
-
|
|
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
|
-
{(
|
|
155
|
-
<
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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>
|
package/dist/scripts/generate-web-module/templates/components/multi-selector.template.js.map
CHANGED
|
@@ -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,
|
|
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
|
@@ -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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
117
|
-
set${names.pascalCase}Options((prev) => [...prev, ...missingOptions]);
|
|
118
|
-
}
|
|
109
|
+
set${names.pascalCase}Options([...options, ...missingOptions]);
|
|
119
110
|
}
|
|
120
|
-
}, [
|
|
111
|
+
}, [data.data, current${names.pascalCase}, selected${names.pluralPascal}]);
|
|
121
112
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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(
|
|
138
|
+
onChange(fullData);
|
|
147
139
|
}
|
|
148
140
|
};
|
|
149
141
|
|
|
150
|
-
|
|
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
|
-
{(
|
|
156
|
-
<
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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 ? "
|
|
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,
|
|
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<
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
),
|
|
137
|
-
userData: user,
|
|
130
|
+
userData: user as unknown as UserInterface,
|
|
131
|
+
avatar: user.avatar,
|
|
138
132
|
}));
|
|
139
133
|
|
|
140
|
-
|
|
141
|
-
if (missingOptions.length > 0) {
|
|
142
|
-
setUserOptions((prev) => [...prev, ...missingOptions]);
|
|
143
|
-
}
|
|
134
|
+
setUserOptions([...options, ...missingOptions]);
|
|
144
135
|
}
|
|
145
|
-
}, [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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,
|
|
157
|
+
form.setValue(id, formValues, { shouldDirty: true, shouldTouch: true });
|
|
171
158
|
|
|
172
159
|
if (onChange) {
|
|
173
|
-
|
|
174
|
-
|
|
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(
|
|
167
|
+
onChange(fullUsers);
|
|
177
168
|
}
|
|
178
169
|
};
|
|
179
170
|
|
|
180
|
-
//
|
|
181
|
-
const
|
|
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
|
-
<
|
|
192
|
+
<MultipleSelector
|
|
193
|
+
value={selectedOptions}
|
|
194
|
+
onChange={handleChange}
|
|
188
195
|
options={userOptions}
|
|
189
|
-
onValueChange={handleValueChange}
|
|
190
|
-
defaultValue={selectedUserIds}
|
|
191
196
|
placeholder={placeholder}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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>
|