@carlonicora/nextjs-jsonapi 1.68.0 → 1.70.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/{AuthComponent-NwQ_ZXsv.d.mts → AuthComponent-DXe3kPzb.d.mts} +1 -1
- package/dist/{AuthComponent-DL1D3y7f.d.ts → AuthComponent-Di8DsZ2I.d.ts} +1 -1
- package/dist/{BlockNoteEditor-6FDECIS2.mjs → BlockNoteEditor-6XV2IXLY.mjs} +15 -9
- package/dist/BlockNoteEditor-6XV2IXLY.mjs.map +1 -0
- package/dist/{BlockNoteEditor-DXHROT4C.js → BlockNoteEditor-NVPUPZXB.js} +25 -19
- package/dist/BlockNoteEditor-NVPUPZXB.js.map +1 -0
- package/dist/HowToInterface-DtVWAE1s.d.mts +17 -0
- package/dist/HowToInterface-NaqSG9sE.d.ts +17 -0
- package/dist/{auth.interface-BX_1qZZJ.d.ts → auth.interface-BTco8PWs.d.ts} +1 -1
- package/dist/{auth.interface-yeLelxdI.d.mts → auth.interface-C4uJzBec.d.mts} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-37KYO2UD.js → chunk-56VU7A4I.js} +172 -18
- package/dist/chunk-56VU7A4I.js.map +1 -0
- package/dist/{chunk-WOJIRXIP.js → chunk-6ROMPIIP.js} +11 -11
- package/dist/{chunk-WOJIRXIP.js.map → chunk-6ROMPIIP.js.map} +1 -1
- package/dist/{chunk-IOMDNRX5.mjs → chunk-GZNHBAZF.mjs} +155 -1
- package/dist/chunk-GZNHBAZF.mjs.map +1 -0
- package/dist/{chunk-H4ZS3R76.mjs → chunk-LQEKQYUJ.mjs} +2569 -1603
- package/dist/chunk-LQEKQYUJ.mjs.map +1 -0
- package/dist/{chunk-WVTBEVAL.mjs → chunk-WJYWWOTG.mjs} +2 -2
- package/dist/{chunk-ELTHSXBI.js → chunk-ZKOLKFAS.js} +1664 -698
- package/dist/chunk-ZKOLKFAS.js.map +1 -0
- package/dist/client/index.d.mts +5 -6
- package/dist/client/index.d.ts +5 -6
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +83 -10
- package/dist/components/index.d.ts +83 -10
- package/dist/components/index.js +26 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +25 -3
- package/dist/{config-D-mqttuF.d.mts → config-Bmr_0qTn.d.mts} +1 -1
- package/dist/{config-CyCAWW-d.d.ts → config-n0lfSf27.d.ts} +1 -1
- package/dist/contexts/index.d.mts +16 -4
- package/dist/contexts/index.d.ts +16 -4
- package/dist/contexts/index.js +8 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +7 -3
- package/dist/core/index.d.mts +61 -11
- package/dist/core/index.d.ts +61 -11
- package/dist/core/index.js +10 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +9 -1
- package/dist/index.d.mts +9 -10
- package/dist/index.d.ts +9 -10
- package/dist/index.js +11 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +10 -2
- package/dist/{notification.interface-ItBxq2au.d.ts → notification.interface-DYDZENx2.d.ts} +18 -1
- package/dist/{notification.interface-C6UcmJqu.d.mts → notification.interface-DrHu_1MM.d.mts} +18 -1
- package/dist/{s3.service-N1g0piXD.d.ts → s3.service-DK2KKXbR.d.ts} +2 -3
- package/dist/{s3.service-CHOTwfWA.d.mts → s3.service-TsN2unZr.d.mts} +2 -3
- package/dist/server/index.d.mts +3 -4
- package/dist/server/index.d.ts +3 -4
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{useRbacState-CUj0hp8t.d.ts → useRbacState-BYaSdA78.d.ts} +1 -1
- package/dist/{useRbacState-Btk1gkQg.d.mts → useRbacState-CQEJ_ysV.d.mts} +1 -1
- package/dist/{useSocket-BSUN9s3p.d.ts → useSocket-Cjt_qvkI.d.ts} +1 -1
- package/dist/{useSocket-DKI92Fbg.d.mts → useSocket-VAGetcT3.d.mts} +1 -1
- package/package.json +1 -1
- package/src/components/editors/BlockNoteEditor.tsx +7 -1
- package/src/components/forms/FormBlockNote.tsx +6 -0
- package/src/components/forms/FormSelect.tsx +3 -0
- package/src/components/index.ts +1 -0
- package/src/contexts/index.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +19 -0
- package/src/features/how-to/HowToModule.ts +18 -0
- package/src/features/how-to/components/containers/HowToCommand.tsx +230 -0
- package/src/features/how-to/components/containers/HowToCommandViewer.tsx +76 -0
- package/src/features/how-to/components/containers/HowToContainer.tsx +27 -0
- package/src/features/how-to/components/containers/HowToListContainer.tsx +17 -0
- package/src/features/how-to/components/details/HowToContent.tsx +16 -0
- package/src/features/how-to/components/details/HowToDetails.tsx +52 -0
- package/src/features/how-to/components/forms/HowToDeleter.tsx +31 -0
- package/src/features/how-to/components/forms/HowToEditor.tsx +270 -0
- package/src/features/how-to/components/forms/HowToMultiSelector.tsx +152 -0
- package/src/features/how-to/components/forms/HowToSelector.tsx +164 -0
- package/src/features/how-to/components/index.ts +11 -0
- package/src/features/how-to/components/lists/HowToList.tsx +39 -0
- package/src/features/how-to/contexts/HowToContext.tsx +101 -0
- package/src/features/how-to/data/HowTo.ts +69 -0
- package/src/features/how-to/data/HowToFields.ts +10 -0
- package/src/features/how-to/data/HowToInterface.ts +11 -0
- package/src/features/how-to/data/HowToService.ts +61 -0
- package/src/features/how-to/data/index.ts +4 -0
- package/src/features/how-to/hooks/useHowToTableStructure.tsx +86 -0
- package/src/features/how-to/index.ts +2 -0
- package/src/features/how-to/utils/blocknote.ts +108 -0
- package/src/features/how-to/utils/index.ts +1 -0
- package/dist/BlockNoteEditor-6FDECIS2.mjs.map +0 -1
- package/dist/BlockNoteEditor-DXHROT4C.js.map +0 -1
- package/dist/breadcrumb.item.data.interface-CgB4_1EE.d.mts +0 -6
- package/dist/breadcrumb.item.data.interface-CgB4_1EE.d.ts +0 -6
- package/dist/chunk-37KYO2UD.js.map +0 -1
- package/dist/chunk-ELTHSXBI.js.map +0 -1
- package/dist/chunk-H4ZS3R76.mjs.map +0 -1
- package/dist/chunk-IOMDNRX5.mjs.map +0 -1
- package/dist/content.interface-8T5-G84c.d.mts +0 -21
- package/dist/content.interface-D-xdYxjt.d.ts +0 -21
- /package/dist/{chunk-WVTBEVAL.mjs.map → chunk-WJYWWOTG.mjs.map} +0 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { AttributeElement, ContentTitle } from "../../../../components";
|
|
5
|
+
import { Modules } from "../../../../core";
|
|
6
|
+
import { useSharedContext } from "../../../../contexts/SharedContext";
|
|
7
|
+
import { Link } from "../../../../shadcnui";
|
|
8
|
+
import { HowTo } from "../../data/HowTo";
|
|
9
|
+
import { HowToInterface } from "../../data/HowToInterface";
|
|
10
|
+
import { useHowToContext } from "../../contexts/HowToContext";
|
|
11
|
+
|
|
12
|
+
type HowToDetailsProps = {
|
|
13
|
+
howTo: HowToInterface;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function HowToDetailsInternal({ howTo }: HowToDetailsProps) {
|
|
17
|
+
const t = useTranslations();
|
|
18
|
+
const { title } = useSharedContext();
|
|
19
|
+
const pagesList = HowTo.parsePagesFromString(howTo.pages);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
23
|
+
<ContentTitle type={title.type} element={title.element} functions={title.functions} module={Modules.HowTo} />
|
|
24
|
+
|
|
25
|
+
{pagesList.length > 0 && (
|
|
26
|
+
<div className="border-t pt-4">
|
|
27
|
+
<AttributeElement
|
|
28
|
+
title={t(`howto.fields.pages.label`)}
|
|
29
|
+
value={
|
|
30
|
+
<ul className="flex flex-col gap-y-1">
|
|
31
|
+
{pagesList.map((page, index) => (
|
|
32
|
+
<li key={index}>
|
|
33
|
+
<Link href={page} className="text-primary hover:underline">
|
|
34
|
+
{page}
|
|
35
|
+
</Link>
|
|
36
|
+
</li>
|
|
37
|
+
))}
|
|
38
|
+
</ul>
|
|
39
|
+
}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function HowToDetails() {
|
|
48
|
+
const { howTo } = useHowToContext();
|
|
49
|
+
if (!howTo) return null;
|
|
50
|
+
|
|
51
|
+
return <HowToDetailsInternal howTo={howTo} />;
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { CommonDeleter } from "../../../../components";
|
|
5
|
+
import { Modules } from "../../../../core";
|
|
6
|
+
import { usePageUrlGenerator } from "../../../../hooks";
|
|
7
|
+
import { HowToInterface } from "../../data/HowToInterface";
|
|
8
|
+
import { HowToService } from "../../data/HowToService";
|
|
9
|
+
|
|
10
|
+
type HowToDeleterProps = {
|
|
11
|
+
howTo: HowToInterface;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function HowToDeleterInternal({ howTo }: HowToDeleterProps) {
|
|
15
|
+
const t = useTranslations();
|
|
16
|
+
const generateUrl = usePageUrlGenerator();
|
|
17
|
+
|
|
18
|
+
if (!howTo) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<CommonDeleter
|
|
22
|
+
type={`howtos`}
|
|
23
|
+
deleteFunction={() => HowToService.delete({ howToId: howTo.id })}
|
|
24
|
+
redirectTo={generateUrl({ page: Modules.HowTo })}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default function HowToDeleter(props: HowToDeleterProps) {
|
|
30
|
+
return <HowToDeleterInternal {...props} />;
|
|
31
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { PlusIcon, SearchIcon, XIcon } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import { ReactNode, useCallback, useMemo, useState } from "react";
|
|
7
|
+
import { useForm } from "react-hook-form";
|
|
8
|
+
import { v4 } from "uuid";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
import { EditorSheet, FormInput } from "../../../../components";
|
|
12
|
+
import { BlockNoteEditorContainer } from "../../../../components";
|
|
13
|
+
import { ModuleRegistry, Modules } from "../../../../core";
|
|
14
|
+
import { useI18nRouter } from "../../../../hooks";
|
|
15
|
+
import {
|
|
16
|
+
Button,
|
|
17
|
+
Command,
|
|
18
|
+
CommandEmpty,
|
|
19
|
+
CommandItem,
|
|
20
|
+
CommandList,
|
|
21
|
+
Input,
|
|
22
|
+
Label,
|
|
23
|
+
Popover,
|
|
24
|
+
PopoverContent,
|
|
25
|
+
PopoverTrigger,
|
|
26
|
+
} from "../../../../shadcnui";
|
|
27
|
+
import { HowTo } from "../../data/HowTo";
|
|
28
|
+
import { HowToInput, HowToInterface } from "../../data/HowToInterface";
|
|
29
|
+
import { HowToService } from "../../data/HowToService";
|
|
30
|
+
|
|
31
|
+
function PageSelector({
|
|
32
|
+
value,
|
|
33
|
+
allPageUrls,
|
|
34
|
+
selectedPages,
|
|
35
|
+
placeholder,
|
|
36
|
+
emptyMessage,
|
|
37
|
+
onSelect,
|
|
38
|
+
onRemove,
|
|
39
|
+
}: {
|
|
40
|
+
value: string;
|
|
41
|
+
allPageUrls: { id: string; text: string }[];
|
|
42
|
+
selectedPages: string[];
|
|
43
|
+
placeholder: string;
|
|
44
|
+
emptyMessage: string;
|
|
45
|
+
onSelect: (value: string) => void;
|
|
46
|
+
onRemove: () => void;
|
|
47
|
+
}) {
|
|
48
|
+
const [open, setOpen] = useState(false);
|
|
49
|
+
const [search, setSearch] = useState("");
|
|
50
|
+
|
|
51
|
+
const selectedLabel = value ? allPageUrls.find((opt) => opt.id === value) : undefined;
|
|
52
|
+
|
|
53
|
+
const filteredOptions = useMemo(() => {
|
|
54
|
+
const available = allPageUrls.filter((opt) => opt.id === value || !selectedPages.includes(opt.id));
|
|
55
|
+
if (!search) return available;
|
|
56
|
+
const term = search.toLowerCase();
|
|
57
|
+
return available.filter((opt) => opt.text.toLowerCase().includes(term) || opt.id.toLowerCase().includes(term));
|
|
58
|
+
}, [allPageUrls, selectedPages, value, search]);
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div className="flex gap-2">
|
|
62
|
+
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
|
63
|
+
<PopoverTrigger className="flex-1">
|
|
64
|
+
<div className="bg-input/20 dark:bg-input/30 border-input flex h-9 w-full items-center rounded-md border px-3 text-sm">
|
|
65
|
+
{selectedLabel ? (
|
|
66
|
+
<span>
|
|
67
|
+
{selectedLabel.text} ({selectedLabel.id})
|
|
68
|
+
</span>
|
|
69
|
+
) : (
|
|
70
|
+
<span className="text-muted-foreground">{placeholder}</span>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</PopoverTrigger>
|
|
74
|
+
<PopoverContent align="start" className="w-(--anchor-width) p-0">
|
|
75
|
+
<Command shouldFilter={false}>
|
|
76
|
+
<div className="relative w-full border-b">
|
|
77
|
+
<SearchIcon className="text-muted-foreground absolute top-2.5 left-2.5 h-4 w-4" />
|
|
78
|
+
<Input
|
|
79
|
+
placeholder={placeholder}
|
|
80
|
+
type="text"
|
|
81
|
+
className="rounded-none border-0 pl-8 focus-visible:ring-0"
|
|
82
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
83
|
+
value={search}
|
|
84
|
+
/>
|
|
85
|
+
</div>
|
|
86
|
+
<CommandList className="max-h-48">
|
|
87
|
+
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
|
88
|
+
{filteredOptions.map((opt) => (
|
|
89
|
+
<CommandItem
|
|
90
|
+
key={opt.id}
|
|
91
|
+
className="cursor-pointer"
|
|
92
|
+
onSelect={() => {
|
|
93
|
+
onSelect(opt.id);
|
|
94
|
+
setOpen(false);
|
|
95
|
+
setSearch("");
|
|
96
|
+
}}
|
|
97
|
+
>
|
|
98
|
+
{opt.text} ({opt.id})
|
|
99
|
+
</CommandItem>
|
|
100
|
+
))}
|
|
101
|
+
</CommandList>
|
|
102
|
+
</Command>
|
|
103
|
+
</PopoverContent>
|
|
104
|
+
</Popover>
|
|
105
|
+
<Button type="button" variant="outline" size="icon" onClick={onRemove}>
|
|
106
|
+
<XIcon className="h-4 w-4" />
|
|
107
|
+
</Button>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
type HowToEditorProps = {
|
|
113
|
+
howTo?: HowToInterface;
|
|
114
|
+
propagateChanges?: (howTo: HowToInterface) => void;
|
|
115
|
+
trigger?: ReactNode;
|
|
116
|
+
forceShow?: boolean;
|
|
117
|
+
onClose?: () => void;
|
|
118
|
+
onRevalidate?: (path: string) => Promise<void>;
|
|
119
|
+
dialogOpen?: boolean;
|
|
120
|
+
onDialogOpenChange?: (open: boolean) => void;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
function HowToEditorInternal({
|
|
124
|
+
howTo,
|
|
125
|
+
propagateChanges,
|
|
126
|
+
trigger,
|
|
127
|
+
forceShow,
|
|
128
|
+
onClose,
|
|
129
|
+
onRevalidate,
|
|
130
|
+
dialogOpen,
|
|
131
|
+
onDialogOpenChange,
|
|
132
|
+
}: HowToEditorProps) {
|
|
133
|
+
const router = useI18nRouter();
|
|
134
|
+
const t = useTranslations();
|
|
135
|
+
|
|
136
|
+
const formSchema = useMemo(
|
|
137
|
+
() =>
|
|
138
|
+
z.object({
|
|
139
|
+
id: z.string().uuid(),
|
|
140
|
+
name: z.string().min(1, { message: t(`howto.fields.name.error`) }),
|
|
141
|
+
description: z.any(),
|
|
142
|
+
pages: z.array(z.string()),
|
|
143
|
+
}),
|
|
144
|
+
[t],
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
const getDefaultValues = useCallback(
|
|
148
|
+
() => ({
|
|
149
|
+
id: howTo?.id || v4(),
|
|
150
|
+
name: howTo?.name || "",
|
|
151
|
+
description: howTo?.description || [],
|
|
152
|
+
pages: HowTo.parsePagesFromString(howTo?.pages),
|
|
153
|
+
}),
|
|
154
|
+
[howTo],
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const form = useForm<z.infer<typeof formSchema>>({
|
|
158
|
+
resolver: zodResolver(formSchema),
|
|
159
|
+
defaultValues: getDefaultValues(),
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const handleDescriptionChange = useCallback(
|
|
163
|
+
(content: any) => {
|
|
164
|
+
form.setValue("description", content, { shouldDirty: true });
|
|
165
|
+
},
|
|
166
|
+
[form],
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
const allPageUrls = useMemo(() => ModuleRegistry.getAllPageUrls(), []);
|
|
170
|
+
const pages = form.watch("pages");
|
|
171
|
+
|
|
172
|
+
const addPage = () => {
|
|
173
|
+
form.setValue("pages", [...pages, ""], { shouldDirty: true });
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const removePage = (index: number) => {
|
|
177
|
+
form.setValue(
|
|
178
|
+
"pages",
|
|
179
|
+
pages.filter((_: string, i: number) => i !== index),
|
|
180
|
+
{ shouldDirty: true },
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
<EditorSheet
|
|
186
|
+
form={form}
|
|
187
|
+
entityType={t(`entities.howtos`, { count: 1 })}
|
|
188
|
+
entityName={howTo?.name}
|
|
189
|
+
isEdit={!!howTo}
|
|
190
|
+
module={Modules.HowTo}
|
|
191
|
+
propagateChanges={propagateChanges}
|
|
192
|
+
size="lg"
|
|
193
|
+
onSubmit={async (values) => {
|
|
194
|
+
const payload: HowToInput = {
|
|
195
|
+
id: values.id,
|
|
196
|
+
name: values.name,
|
|
197
|
+
authorId: "",
|
|
198
|
+
description: values.description,
|
|
199
|
+
pages: HowTo.serializePagesToString(values.pages),
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const updatedHowTo = howTo ? await HowToService.update(payload) : await HowToService.create(payload);
|
|
203
|
+
|
|
204
|
+
return updatedHowTo;
|
|
205
|
+
}}
|
|
206
|
+
onReset={() => {
|
|
207
|
+
return getDefaultValues();
|
|
208
|
+
}}
|
|
209
|
+
onRevalidate={onRevalidate}
|
|
210
|
+
onNavigate={(url) => router.push(url)}
|
|
211
|
+
onClose={onClose}
|
|
212
|
+
trigger={trigger}
|
|
213
|
+
forceShow={forceShow}
|
|
214
|
+
dialogOpen={dialogOpen}
|
|
215
|
+
onDialogOpenChange={onDialogOpenChange}
|
|
216
|
+
>
|
|
217
|
+
<div className="flex w-full flex-col gap-y-4">
|
|
218
|
+
<FormInput
|
|
219
|
+
form={form}
|
|
220
|
+
id="name"
|
|
221
|
+
name={t(`howto.fields.name.label`)}
|
|
222
|
+
placeholder={t(`howto.fields.name.placeholder`)}
|
|
223
|
+
isRequired
|
|
224
|
+
/>
|
|
225
|
+
<div className="space-y-2">
|
|
226
|
+
<Label>{t(`howto.fields.description.label`)}</Label>
|
|
227
|
+
<div className="max-h-80 overflow-y-auto rounded-md border">
|
|
228
|
+
<BlockNoteEditorContainer
|
|
229
|
+
id={form.getValues("id")}
|
|
230
|
+
type="howto"
|
|
231
|
+
initialContent={form.getValues("description")}
|
|
232
|
+
onChange={handleDescriptionChange}
|
|
233
|
+
placeholder={t(`howto.fields.description.placeholder`)}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
{/* Pages List */}
|
|
238
|
+
<div className="space-y-2">
|
|
239
|
+
<Label>{t(`howto.fields.pages.label`)}</Label>
|
|
240
|
+
<div className="space-y-2">
|
|
241
|
+
{pages.map((page: string, index: number) => (
|
|
242
|
+
<PageSelector
|
|
243
|
+
key={index}
|
|
244
|
+
value={page}
|
|
245
|
+
allPageUrls={allPageUrls}
|
|
246
|
+
selectedPages={pages}
|
|
247
|
+
placeholder={t(`howto.fields.pages.placeholder`)}
|
|
248
|
+
emptyMessage={t(`howto.command.empty`)}
|
|
249
|
+
onSelect={(value) => {
|
|
250
|
+
const updated = [...pages];
|
|
251
|
+
updated[index] = value;
|
|
252
|
+
form.setValue("pages", updated, { shouldDirty: true });
|
|
253
|
+
}}
|
|
254
|
+
onRemove={() => removePage(index)}
|
|
255
|
+
/>
|
|
256
|
+
))}
|
|
257
|
+
<Button type="button" variant="outline" size="sm" onClick={addPage}>
|
|
258
|
+
<PlusIcon className="mr-2 h-4 w-4" />
|
|
259
|
+
{t(`howto.fields.pages.add`)}
|
|
260
|
+
</Button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</EditorSheet>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export default function HowToEditor(props: HowToEditorProps) {
|
|
269
|
+
return <HowToEditorInternal {...props} />;
|
|
270
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
+
import { useWatch } from "react-hook-form";
|
|
6
|
+
|
|
7
|
+
import { FormFieldWrapper, MultipleSelector, Option } from "../../../../components";
|
|
8
|
+
import { Modules } from "../../../../core";
|
|
9
|
+
import { DataListRetriever, useDataListRetriever, useDebounce } from "../../../../hooks";
|
|
10
|
+
import { HowToInterface } from "../../data/HowToInterface";
|
|
11
|
+
import { HowToService } from "../../data/HowToService";
|
|
12
|
+
|
|
13
|
+
type HowToMultiSelectType = {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type HowToMultiSelectorProps = {
|
|
19
|
+
id: string;
|
|
20
|
+
form: any;
|
|
21
|
+
currentHowTo?: HowToInterface;
|
|
22
|
+
label?: string;
|
|
23
|
+
placeholder?: string;
|
|
24
|
+
onChange?: (howTos?: HowToInterface[]) => void;
|
|
25
|
+
maxCount?: number;
|
|
26
|
+
isRequired?: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type HowToOption = Option & {
|
|
30
|
+
howToData?: HowToInterface;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default function HowToMultiSelector({
|
|
34
|
+
id,
|
|
35
|
+
form,
|
|
36
|
+
currentHowTo,
|
|
37
|
+
label,
|
|
38
|
+
placeholder,
|
|
39
|
+
onChange,
|
|
40
|
+
maxCount = 3,
|
|
41
|
+
isRequired = false,
|
|
42
|
+
}: HowToMultiSelectorProps) {
|
|
43
|
+
const t = useTranslations();
|
|
44
|
+
const [howToOptions, setHowToOptions] = useState<HowToOption[]>([]);
|
|
45
|
+
const [searchTerm, setSearchTerm] = useState<string>("");
|
|
46
|
+
|
|
47
|
+
const selectedHowTos: HowToMultiSelectType[] = useWatch({ control: form.control, name: id }) || [];
|
|
48
|
+
|
|
49
|
+
const data: DataListRetriever<HowToInterface> = useDataListRetriever({
|
|
50
|
+
retriever: (params) => HowToService.findMany(params),
|
|
51
|
+
retrieverParams: {},
|
|
52
|
+
ready: true,
|
|
53
|
+
module: Modules.HowTo,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const updateSearch = useCallback(
|
|
57
|
+
(searchedTerm: string) => {
|
|
58
|
+
if (searchedTerm.trim()) {
|
|
59
|
+
data.addAdditionalParameter("search", searchedTerm.trim());
|
|
60
|
+
} else {
|
|
61
|
+
data.removeAdditionalParameter("search");
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
[data],
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const debouncedUpdateSearch = useDebounce(updateSearch, 500);
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
debouncedUpdateSearch(searchTerm);
|
|
71
|
+
}, [debouncedUpdateSearch, searchTerm]);
|
|
72
|
+
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (data.data && data.data.length > 0) {
|
|
75
|
+
const howTos = data.data as HowToInterface[];
|
|
76
|
+
const filteredHowTos = howTos.filter((howTo) => howTo.id !== currentHowTo?.id);
|
|
77
|
+
|
|
78
|
+
const options: HowToOption[] = filteredHowTos.map((howTo) => ({
|
|
79
|
+
label: howTo.name,
|
|
80
|
+
value: howTo.id,
|
|
81
|
+
howToData: howTo,
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
// Add options for any already selected that aren't in search results
|
|
85
|
+
const existingOptionIds = new Set(options.map((option) => option.value));
|
|
86
|
+
const missingOptions: HowToOption[] = selectedHowTos
|
|
87
|
+
.filter((howTo) => !existingOptionIds.has(howTo.id))
|
|
88
|
+
.map((howTo) => ({
|
|
89
|
+
label: howTo.name,
|
|
90
|
+
value: howTo.id,
|
|
91
|
+
howToData: howTo as unknown as HowToInterface,
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
setHowToOptions([...options, ...missingOptions]);
|
|
95
|
+
}
|
|
96
|
+
}, [data.data, currentHowTo, selectedHowTos]);
|
|
97
|
+
|
|
98
|
+
// Convert selected to Option[] format
|
|
99
|
+
const selectedOptions = useMemo(() => {
|
|
100
|
+
return selectedHowTos.map((howTo) => ({
|
|
101
|
+
value: howTo.id,
|
|
102
|
+
label: howTo.name,
|
|
103
|
+
}));
|
|
104
|
+
}, [selectedHowTos]);
|
|
105
|
+
|
|
106
|
+
const handleChange = (options: Option[]) => {
|
|
107
|
+
// Convert to form format
|
|
108
|
+
const formValues = options.map((option) => ({
|
|
109
|
+
id: option.value,
|
|
110
|
+
name: option.label,
|
|
111
|
+
}));
|
|
112
|
+
|
|
113
|
+
form.setValue(id, formValues, { shouldDirty: true, shouldTouch: true });
|
|
114
|
+
|
|
115
|
+
if (onChange) {
|
|
116
|
+
// Get full data for onChange callback
|
|
117
|
+
const fullData = options
|
|
118
|
+
.map((option) => {
|
|
119
|
+
const howToOption = howToOptions.find((opt) => opt.value === option.value);
|
|
120
|
+
return howToOption?.howToData;
|
|
121
|
+
})
|
|
122
|
+
.filter(Boolean) as HowToInterface[];
|
|
123
|
+
onChange(fullData);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
// Search handler
|
|
128
|
+
const handleSearchSync = (search: string): Option[] => {
|
|
129
|
+
setSearchTerm(search);
|
|
130
|
+
return howToOptions;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<div className="flex w-full flex-col">
|
|
135
|
+
<FormFieldWrapper form={form} name={id} label={label} isRequired={isRequired}>
|
|
136
|
+
{() => (
|
|
137
|
+
<MultipleSelector
|
|
138
|
+
value={selectedOptions}
|
|
139
|
+
onChange={handleChange}
|
|
140
|
+
options={howToOptions}
|
|
141
|
+
placeholder={placeholder}
|
|
142
|
+
maxDisplayCount={maxCount}
|
|
143
|
+
hideClearAllButton
|
|
144
|
+
onSearchSync={handleSearchSync}
|
|
145
|
+
delay={0}
|
|
146
|
+
emptyIndicator={<span className="text-muted-foreground">{t("ui.search.no_results_generic")}</span>}
|
|
147
|
+
/>
|
|
148
|
+
)}
|
|
149
|
+
</FormFieldWrapper>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { CircleX, RefreshCwIcon, SearchIcon, XIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
Command,
|
|
9
|
+
CommandItem,
|
|
10
|
+
CommandList,
|
|
11
|
+
Input,
|
|
12
|
+
Popover,
|
|
13
|
+
PopoverContent,
|
|
14
|
+
PopoverTrigger,
|
|
15
|
+
} from "../../../../shadcnui";
|
|
16
|
+
import { FormFieldWrapper } from "../../../../components";
|
|
17
|
+
import { Modules } from "../../../../core";
|
|
18
|
+
import { DataListRetriever, useDataListRetriever, useDebounce } from "../../../../hooks";
|
|
19
|
+
import { HowToInterface } from "../../data/HowToInterface";
|
|
20
|
+
import { HowToService } from "../../data/HowToService";
|
|
21
|
+
|
|
22
|
+
type HowToSelectorProps = {
|
|
23
|
+
id: string;
|
|
24
|
+
form: any;
|
|
25
|
+
label?: string;
|
|
26
|
+
placeholder?: string;
|
|
27
|
+
onChange?: (howTo?: HowToInterface) => void;
|
|
28
|
+
isRequired?: boolean;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export default function HowToSelector({
|
|
32
|
+
id,
|
|
33
|
+
form,
|
|
34
|
+
label,
|
|
35
|
+
placeholder,
|
|
36
|
+
onChange,
|
|
37
|
+
isRequired = false,
|
|
38
|
+
}: HowToSelectorProps) {
|
|
39
|
+
const t = useTranslations();
|
|
40
|
+
|
|
41
|
+
const [open, setOpen] = useState<boolean>(false);
|
|
42
|
+
|
|
43
|
+
const searchTermRef = useRef<string>("");
|
|
44
|
+
const [searchTerm, setSearchTerm] = useState<string>("");
|
|
45
|
+
|
|
46
|
+
const [isSearching, setIsSearching] = useState<boolean>(false);
|
|
47
|
+
|
|
48
|
+
const data: DataListRetriever<HowToInterface> = useDataListRetriever({
|
|
49
|
+
retriever: (params) => {
|
|
50
|
+
return HowToService.findMany(params);
|
|
51
|
+
},
|
|
52
|
+
retrieverParams: {},
|
|
53
|
+
module: Modules.HowTo,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const search = useCallback(
|
|
57
|
+
async (searchedTerm: string) => {
|
|
58
|
+
try {
|
|
59
|
+
if (searchedTerm === searchTermRef.current) return;
|
|
60
|
+
setIsSearching(true);
|
|
61
|
+
searchTermRef.current = searchedTerm;
|
|
62
|
+
await data.search(searchedTerm);
|
|
63
|
+
} finally {
|
|
64
|
+
setIsSearching(false);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
[searchTermRef, data],
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const updateSearchTerm = useDebounce(search, 500);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
setIsSearching(true);
|
|
74
|
+
updateSearchTerm(searchTerm);
|
|
75
|
+
}, [updateSearchTerm, searchTerm]);
|
|
76
|
+
|
|
77
|
+
const setHowTo = (howTo?: HowToInterface) => {
|
|
78
|
+
if (onChange) onChange(howTo);
|
|
79
|
+
if (!howTo) {
|
|
80
|
+
form.setValue(id, undefined, { shouldDirty: true });
|
|
81
|
+
setOpen(false);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
form.setValue(id, { id: howTo.id, name: howTo.name }, { shouldDirty: true });
|
|
86
|
+
setOpen(false);
|
|
87
|
+
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
setOpen(false);
|
|
90
|
+
}, 0);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="flex w-full flex-col">
|
|
95
|
+
<FormFieldWrapper form={form} name={id} label={label} isRequired={isRequired}>
|
|
96
|
+
{(field: any) => (
|
|
97
|
+
<Popover open={open} onOpenChange={setOpen} modal={true}>
|
|
98
|
+
<div className="flex w-full flex-row items-center justify-between">
|
|
99
|
+
<PopoverTrigger className="w-full">
|
|
100
|
+
<div className="flex w-full flex-row items-center justify-start rounded-md">
|
|
101
|
+
{field.value ? (
|
|
102
|
+
<div className="bg-input/20 dark:bg-input/30 border-input flex h-7 w-full flex-row items-center justify-start rounded-md border px-2 py-0.5 text-sm md:text-xs/relaxed">
|
|
103
|
+
<span>{field.value?.name ?? ""}</span>
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<div className="bg-input/20 dark:bg-input/30 border-input text-muted-foreground flex h-7 w-full flex-row items-center justify-start rounded-md border px-2 py-0.5 text-sm md:text-xs/relaxed">
|
|
107
|
+
{placeholder ?? t(`generic.search.placeholder`, { type: t(`entities.howtos`, { count: 1 }) })}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
</PopoverTrigger>
|
|
112
|
+
{field.value && (
|
|
113
|
+
<CircleX
|
|
114
|
+
className="text-muted hover:text-destructive ml-2 h-4 w-4 shrink-0 cursor-pointer"
|
|
115
|
+
onClick={() => setHowTo()}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
<PopoverContent align="start" className="w-(--anchor-width)">
|
|
120
|
+
<Command shouldFilter={false}>
|
|
121
|
+
<div className="relative mb-2 w-full">
|
|
122
|
+
<SearchIcon className="text-muted-foreground absolute top-2.5 left-2.5 h-4 w-4" />
|
|
123
|
+
<Input
|
|
124
|
+
placeholder={t(`generic.search.placeholder`, { type: t(`entities.howtos`, { count: 1 }) })}
|
|
125
|
+
type="text"
|
|
126
|
+
className="w-full pr-8 pl-8"
|
|
127
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearchTerm(e.target.value)}
|
|
128
|
+
value={searchTerm}
|
|
129
|
+
/>
|
|
130
|
+
{isSearching ? (
|
|
131
|
+
<RefreshCwIcon className="text-muted-foreground absolute top-2.5 right-2.5 h-4 w-4 animate-spin" />
|
|
132
|
+
) : searchTermRef.current ? (
|
|
133
|
+
<XIcon
|
|
134
|
+
className={`absolute top-2.5 right-2.5 h-4 w-4 ${searchTermRef.current ? "cursor-pointer" : "text-muted-foreground"}`}
|
|
135
|
+
onClick={() => {
|
|
136
|
+
setSearchTerm("");
|
|
137
|
+
search("");
|
|
138
|
+
}}
|
|
139
|
+
/>
|
|
140
|
+
) : (
|
|
141
|
+
<></>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
<CommandList>
|
|
145
|
+
{data.data &&
|
|
146
|
+
data.data.length > 0 &&
|
|
147
|
+
(data.data as HowToInterface[]).map((howTo: HowToInterface) => (
|
|
148
|
+
<CommandItem
|
|
149
|
+
className="cursor-pointer hover:bg-muted data-selected:hover:bg-muted bg-transparent data-selected:bg-transparent"
|
|
150
|
+
key={howTo.id}
|
|
151
|
+
onSelect={() => setHowTo(howTo)}
|
|
152
|
+
>
|
|
153
|
+
{howTo.name}
|
|
154
|
+
</CommandItem>
|
|
155
|
+
))}
|
|
156
|
+
</CommandList>
|
|
157
|
+
</Command>
|
|
158
|
+
</PopoverContent>
|
|
159
|
+
</Popover>
|
|
160
|
+
)}
|
|
161
|
+
</FormFieldWrapper>
|
|
162
|
+
</div>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as HowToCommand } from "./containers/HowToCommand";
|
|
2
|
+
export { default as HowToCommandViewer } from "./containers/HowToCommandViewer";
|
|
3
|
+
export { default as HowToContainer } from "./containers/HowToContainer";
|
|
4
|
+
export { default as HowToListContainer } from "./containers/HowToListContainer";
|
|
5
|
+
export { default as HowToContent } from "./details/HowToContent";
|
|
6
|
+
export { default as HowToDetails } from "./details/HowToDetails";
|
|
7
|
+
export { default as HowToDeleter } from "./forms/HowToDeleter";
|
|
8
|
+
export { default as HowToEditor } from "./forms/HowToEditor";
|
|
9
|
+
export { default as HowToMultiSelector } from "./forms/HowToMultiSelector";
|
|
10
|
+
export { default as HowToSelector } from "./forms/HowToSelector";
|
|
11
|
+
export { default as HowToList } from "./lists/HowToList";
|