@vonaffenfels/contentful-teasermanager 1.2.32 → 1.2.41
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/.babelrc +19 -19
- package/dist/TestStory.js +94 -0
- package/dist/TestStory2.js +94 -0
- package/dist/TestStory3.js +94 -0
- package/dist/index.html +12 -0
- package/dist/index.js +120203 -0
- package/jest.config.js +8 -8
- package/package.json +3 -3
- package/postcss.config.js +6 -6
- package/src/components/Contentful/ConfigScreen.js +154 -154
- package/src/components/Contentful/Dialog/LogicEditor.js +602 -602
- package/src/components/Contentful/Dialog/NewestArticles.js +198 -198
- package/src/components/Contentful/Dialog.js +87 -87
- package/src/components/Contentful/EntryEditor.js +196 -196
- package/src/components/Contentful/Page.js +9 -9
- package/src/components/NoAccess.js +12 -12
- package/src/components/Teasermanager/Timeline.js +179 -179
- package/src/components/Teasermanager/Timeline.module.css +89 -89
- package/src/components/Teasermanager.js +269 -269
- package/src/components/Teasermanager.module.css +137 -137
- package/src/dev.js +5 -5
- package/src/hooks/useDebounce.js +20 -20
- package/src/hooks/useOnScreen.js +23 -23
- package/src/icons/remove.svg +7 -7
- package/src/index.html +11 -11
- package/src/index.js +51 -51
- package/src/lib/contentfulClient.js +12 -12
- package/src/lib/runLoaders.js +46 -46
- package/src/queryFromLogic.test.js +148 -148
- package/src/scss/index.scss +11 -11
- package/src/utils/germanTimezoneOffset.js +35 -35
- package/src/utils/germanTimezoneOffset.test.js +51 -51
- package/tailwind.config.js +5 -5
- package/webpack.config.dev.js +25 -25
- package/webpack.config.js +174 -174
|
@@ -1,603 +1,603 @@
|
|
|
1
|
-
import {
|
|
2
|
-
TextInput,
|
|
3
|
-
Pill,
|
|
4
|
-
SectionHeading,
|
|
5
|
-
Checkbox,
|
|
6
|
-
Button,
|
|
7
|
-
DragHandle,
|
|
8
|
-
} from "@contentful/f36-components";
|
|
9
|
-
import {
|
|
10
|
-
DndContext, useDraggable, useDroppable,
|
|
11
|
-
PointerSensor, useSensor, useSensors,
|
|
12
|
-
} from '@dnd-kit/core';
|
|
13
|
-
import contentfulResolveResponse from "contentful-resolve-response";
|
|
14
|
-
import {
|
|
15
|
-
useEffect, useState,
|
|
16
|
-
} from "react";
|
|
17
|
-
import useDebounce from "../../../hooks/useDebounce";
|
|
18
|
-
import {getContentfulClient} from "../../../lib/contentfulClient";
|
|
19
|
-
import {PlusIcon} from "@contentful/f36-icons";
|
|
20
|
-
import {CSS} from "@dnd-kit/utilities";
|
|
21
|
-
import {queryFromLogic} from "../../../queryFromLogic";
|
|
22
|
-
import {useQuery} from "@tanstack/react-query";
|
|
23
|
-
export const LogicEditor = (props) => {
|
|
24
|
-
const [selectedTags, setSelectedTags] = useState([]);
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
setSelectedTags(props?.logicData?.tags || []);
|
|
28
|
-
}, [props?.logicData?.tags]);
|
|
29
|
-
|
|
30
|
-
const handleDragEnd = (event) => {
|
|
31
|
-
const {
|
|
32
|
-
over,
|
|
33
|
-
active,
|
|
34
|
-
} = event;
|
|
35
|
-
|
|
36
|
-
if (over) {
|
|
37
|
-
const draggedTagIndex = selectedTags.findIndex(tag => tag.id === active.id);
|
|
38
|
-
if (draggedTagIndex !== -1) {
|
|
39
|
-
const updatedTags = [...selectedTags];
|
|
40
|
-
updatedTags[draggedTagIndex] = {
|
|
41
|
-
...updatedTags[draggedTagIndex],
|
|
42
|
-
type: over.id, // "and" or "or"
|
|
43
|
-
};
|
|
44
|
-
setSelectedTags(updatedTags);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
return <DndContext onDragEnd={handleDragEnd}>
|
|
50
|
-
<LogicEditorInner {...props} selectedTags={selectedTags} setSelectedTags={setSelectedTags} />
|
|
51
|
-
</DndContext>;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
const LogicEditorInner = ({
|
|
55
|
-
onSave = () => alert("onSave missing"),
|
|
56
|
-
sdk,
|
|
57
|
-
logicData,
|
|
58
|
-
selectedTags,
|
|
59
|
-
setSelectedTags,
|
|
60
|
-
}) => {
|
|
61
|
-
const contentfulClient = getContentfulClient();
|
|
62
|
-
const [loading, setLoading] = useState(false);
|
|
63
|
-
const [tags, setTags] = useState([]);
|
|
64
|
-
const [categories, setCategories] = useState([]);
|
|
65
|
-
const [searchQuery, setSearchQuery] = useState("");
|
|
66
|
-
const [addTimepoint, setAddTimepoint] = useState(7);
|
|
67
|
-
const debouncedSearch = useDebounce(searchQuery, 500);
|
|
68
|
-
const [selectedTimepoints, setSelectedTimepoints] = useState([]);
|
|
69
|
-
const {
|
|
70
|
-
portal,
|
|
71
|
-
locale = "de",
|
|
72
|
-
} = sdk.parameters.invocation;
|
|
73
|
-
|
|
74
|
-
const {data: articles} = useQuery({
|
|
75
|
-
enabled: !!selectedTags.length && !!portal && !!contentfulClient,
|
|
76
|
-
queryKey: ["articles", selectedTags, portal],
|
|
77
|
-
queryFn: async () => {
|
|
78
|
-
const query = queryFromLogic({
|
|
79
|
-
tags: selectedTags,
|
|
80
|
-
project: portal,
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
try {
|
|
84
|
-
const result = await contentfulClient.getEntries({
|
|
85
|
-
content_type: "article",
|
|
86
|
-
locale: "de",
|
|
87
|
-
order: "-fields.date",
|
|
88
|
-
limit: 10,
|
|
89
|
-
...query,
|
|
90
|
-
select: [
|
|
91
|
-
"fields.title",
|
|
92
|
-
"fields.slug",
|
|
93
|
-
"fields.date",
|
|
94
|
-
"sys.id",
|
|
95
|
-
].join(","),
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
return result;
|
|
99
|
-
} catch (e) {
|
|
100
|
-
console.error(e);
|
|
101
|
-
return {items: []};
|
|
102
|
-
}
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
if (logicData?.timepoints) {
|
|
108
|
-
setSelectedTimepoints(logicData?.timepoints);
|
|
109
|
-
} else {
|
|
110
|
-
setSelectedTimepoints([7, 12, 18]);
|
|
111
|
-
}
|
|
112
|
-
}, [logicData]);
|
|
113
|
-
const {
|
|
114
|
-
setNodeRef: setOrNodeRef,
|
|
115
|
-
isOver: isOrOver,
|
|
116
|
-
node: orNode,
|
|
117
|
-
} = useDroppable({id: 'or'});
|
|
118
|
-
const {
|
|
119
|
-
setNodeRef: setAndNodeRef,
|
|
120
|
-
isOver: isAndOver,
|
|
121
|
-
node: andNode,
|
|
122
|
-
} = useDroppable({id: "and"});
|
|
123
|
-
const {
|
|
124
|
-
setNodeRef: setExcludeNodeRef,
|
|
125
|
-
isOver: isExcludeOver,
|
|
126
|
-
node: excludeNode,
|
|
127
|
-
} = useDroppable({id: "exclude"});
|
|
128
|
-
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
setLoading(true);
|
|
131
|
-
const params = {
|
|
132
|
-
limit: 100,
|
|
133
|
-
skip: 0,
|
|
134
|
-
content_type: "tags",
|
|
135
|
-
locale,
|
|
136
|
-
order: "-sys.updatedAt",
|
|
137
|
-
"fields.portal": portal,
|
|
138
|
-
select: [
|
|
139
|
-
"fields.title",
|
|
140
|
-
"fields.isCategory",
|
|
141
|
-
"fields.hidden",
|
|
142
|
-
"sys.publishedAt",
|
|
143
|
-
"sys.id",
|
|
144
|
-
].join(","),
|
|
145
|
-
};
|
|
146
|
-
const paramsNavigation = {
|
|
147
|
-
limit: 1,
|
|
148
|
-
content_type: "navigation",
|
|
149
|
-
include: 2,
|
|
150
|
-
locale,
|
|
151
|
-
"fields.portal": portal,
|
|
152
|
-
"fields.type": "main",
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
if (debouncedSearch) {
|
|
156
|
-
params["fields.title[match]"] = debouncedSearch;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
Promise.all([
|
|
160
|
-
contentfulClient.getEntries(params),
|
|
161
|
-
contentfulClient.getEntries(paramsNavigation).then(async (response) => {
|
|
162
|
-
if (!response?.items?.[0]) {
|
|
163
|
-
return [];
|
|
164
|
-
}
|
|
165
|
-
const allCategories = {};
|
|
166
|
-
|
|
167
|
-
try {
|
|
168
|
-
// Use different include levels based on portal to avoid multilingual content bloat
|
|
169
|
-
const includeLevel = portal === 'YACHT' ? 1 : 2;
|
|
170
|
-
const references = await sdk.cma.entry.references({entryId: response.items[0].sys.id, include: includeLevel});
|
|
171
|
-
const resolved = contentfulResolveResponse(references);
|
|
172
|
-
|
|
173
|
-
for (const category of resolved) {
|
|
174
|
-
if (!category?.fields?.children?.de) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const children = category.fields.children.de || [];
|
|
179
|
-
|
|
180
|
-
for (const child of children) {
|
|
181
|
-
await getAllTagsFromCategory(child, allCategories, null, portal, sdk);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
} catch (e) {
|
|
185
|
-
console.error("Error getting categories and tags from references: ", e);
|
|
186
|
-
return [];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return Object.values(allCategories).filter((c) => c.tags.length > 0);
|
|
190
|
-
}),
|
|
191
|
-
]).then(([tags, categories]) => {
|
|
192
|
-
setLoading(false);
|
|
193
|
-
setTags(tags.items);
|
|
194
|
-
setCategories(categories);
|
|
195
|
-
});
|
|
196
|
-
}, [debouncedSearch, portal, locale]);
|
|
197
|
-
|
|
198
|
-
const andTags = selectedTags.filter((t) => t.type === "and");
|
|
199
|
-
const orTags = selectedTags.filter((t) => t.type === "or");
|
|
200
|
-
const excludeTags = selectedTags.filter((t) => t.type === "exclude");
|
|
201
|
-
|
|
202
|
-
return <div className="flex flex-col gap-2 p-4">
|
|
203
|
-
<div className="z-10 flex w-full flex-col gap-4 border-b border-gray-200 bg-white py-4">
|
|
204
|
-
<div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
|
|
205
|
-
<SectionHeading style={{marginBottom: 0}}>Gewählte Zeitpunkte:</SectionHeading>
|
|
206
|
-
<div className="flex flex-row items-center gap-2">
|
|
207
|
-
{selectedTimepoints.map((timepoint) => {
|
|
208
|
-
return <Pill
|
|
209
|
-
key={timepoint}
|
|
210
|
-
draggable={false}
|
|
211
|
-
testId="pill-item"
|
|
212
|
-
variant="active"
|
|
213
|
-
label={`${timepoint} Uhr`}
|
|
214
|
-
onClose={() => {
|
|
215
|
-
setSelectedTimepoints(selectedTimepoints.filter((t) => t !== timepoint));
|
|
216
|
-
}}
|
|
217
|
-
/>;
|
|
218
|
-
})}
|
|
219
|
-
<TextInput
|
|
220
|
-
type="number"
|
|
221
|
-
min={1}
|
|
222
|
-
max={24}
|
|
223
|
-
value={addTimepoint}
|
|
224
|
-
onChange={(e) => {
|
|
225
|
-
setAddTimepoint(e.target.value);
|
|
226
|
-
}}
|
|
227
|
-
/>
|
|
228
|
-
<SectionHeading style={{marginBottom: 0}}>Uhr</SectionHeading>
|
|
229
|
-
<Button
|
|
230
|
-
variant="secondary"
|
|
231
|
-
onClick={() => {
|
|
232
|
-
if (selectedTimepoints.includes(addTimepoint)) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
setSelectedTimepoints([...selectedTimepoints, addTimepoint]);
|
|
237
|
-
}}
|
|
238
|
-
>
|
|
239
|
-
<PlusIcon />
|
|
240
|
-
</Button>
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
<div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
|
|
244
|
-
<SectionHeading style={{marginBottom: 0}}>Gewählte Kategorien & Tags:</SectionHeading>
|
|
245
|
-
<div className="grid w-full grid-cols-5 gap-4">
|
|
246
|
-
<div className="flex w-full flex-col gap-2">
|
|
247
|
-
<div>
|
|
248
|
-
<SectionHeading style={{marginBottom: 0}}>Oder</SectionHeading>
|
|
249
|
-
</div>
|
|
250
|
-
<div
|
|
251
|
-
ref={setOrNodeRef}
|
|
252
|
-
style={{
|
|
253
|
-
minHeight: 36,
|
|
254
|
-
backgroundColor: isOrOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
|
|
255
|
-
}}
|
|
256
|
-
className="flex h-full w-full flex-row flex-wrap gap-2"
|
|
257
|
-
>
|
|
258
|
-
{orTags.map((tag) => {
|
|
259
|
-
return <DraggablePill
|
|
260
|
-
key={tag.id}
|
|
261
|
-
id={tag.id}
|
|
262
|
-
variant="active"
|
|
263
|
-
label={(`${tag.label}`)}
|
|
264
|
-
onClose={() => {
|
|
265
|
-
setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
|
|
266
|
-
}}
|
|
267
|
-
/>;
|
|
268
|
-
})}
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
271
|
-
<div className="flex w-full flex-col gap-2">
|
|
272
|
-
<SectionHeading style={{marginBottom: 0}}>Und</SectionHeading>
|
|
273
|
-
<div
|
|
274
|
-
ref={setAndNodeRef}
|
|
275
|
-
style={{
|
|
276
|
-
minHeight: 36,
|
|
277
|
-
backgroundColor: isAndOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
|
|
278
|
-
}}
|
|
279
|
-
className="flex h-full w-full flex-row flex-wrap gap-2"
|
|
280
|
-
>
|
|
281
|
-
{andTags.map((tag) => {
|
|
282
|
-
return <DraggablePill
|
|
283
|
-
key={tag.id}
|
|
284
|
-
id={tag.id}
|
|
285
|
-
testId={tag.id}
|
|
286
|
-
variant="active"
|
|
287
|
-
label={(`${tag.label}`)}
|
|
288
|
-
onClose={() => {
|
|
289
|
-
setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
|
|
290
|
-
}}
|
|
291
|
-
/>;
|
|
292
|
-
})}
|
|
293
|
-
</div>
|
|
294
|
-
</div>
|
|
295
|
-
<div className="flex w-full flex-col gap-2">
|
|
296
|
-
<SectionHeading style={{marginBottom: 0}}>Ausschließen</SectionHeading>
|
|
297
|
-
<div
|
|
298
|
-
ref={setExcludeNodeRef}
|
|
299
|
-
style={{
|
|
300
|
-
minHeight: 36,
|
|
301
|
-
backgroundColor: isExcludeOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
|
|
302
|
-
}}
|
|
303
|
-
className="flex h-full w-full flex-row flex-wrap gap-2"
|
|
304
|
-
>
|
|
305
|
-
{excludeTags.map((tag) => {
|
|
306
|
-
return <DraggablePill
|
|
307
|
-
key={tag.id}
|
|
308
|
-
id={tag.id}
|
|
309
|
-
testId={tag.id}
|
|
310
|
-
variant="active"
|
|
311
|
-
label={(`${tag.label}`)}
|
|
312
|
-
onClose={() => {
|
|
313
|
-
setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
|
|
314
|
-
}}
|
|
315
|
-
/>;
|
|
316
|
-
})}
|
|
317
|
-
</div>
|
|
318
|
-
</div>
|
|
319
|
-
<div className="col-span-2 flex w-full flex-col gap-4">
|
|
320
|
-
<div>
|
|
321
|
-
<SectionHeading style={{marginBottom: 0}}>Artikel</SectionHeading>
|
|
322
|
-
</div>
|
|
323
|
-
<div className="flex flex-col gap-4">
|
|
324
|
-
<table className="w-full max-w-full table-auto gap-4 text-left">
|
|
325
|
-
<thead>
|
|
326
|
-
<tr>
|
|
327
|
-
<th>Datum</th>
|
|
328
|
-
<th className="pl-4">Titel</th>
|
|
329
|
-
</tr>
|
|
330
|
-
</thead>
|
|
331
|
-
<tbody>
|
|
332
|
-
{articles?.items?.map((article) => {
|
|
333
|
-
return <tr key={article.sys.id}>
|
|
334
|
-
<td className="whitespace-nowrap">
|
|
335
|
-
{new Date(article.fields.date.de).toLocaleString("de-DE", {
|
|
336
|
-
hour: "2-digit",
|
|
337
|
-
minute: "2-digit",
|
|
338
|
-
day: "2-digit",
|
|
339
|
-
month: "2-digit",
|
|
340
|
-
year: "numeric",
|
|
341
|
-
})}
|
|
342
|
-
</td>
|
|
343
|
-
<td className="truncate whitespace-nowrap pl-4">
|
|
344
|
-
{article.fields.title.de}
|
|
345
|
-
</td>
|
|
346
|
-
</tr>;
|
|
347
|
-
})}
|
|
348
|
-
</tbody>
|
|
349
|
-
</table>
|
|
350
|
-
</div>
|
|
351
|
-
</div>
|
|
352
|
-
</div>
|
|
353
|
-
</div>
|
|
354
|
-
<div className="flex w-full flex-row gap-2">
|
|
355
|
-
<TextInput
|
|
356
|
-
placeholder="Suche nach Kategorie oder Tag"
|
|
357
|
-
value={searchQuery}
|
|
358
|
-
onChange={(e) => setSearchQuery(e.target.value)}
|
|
359
|
-
/>
|
|
360
|
-
</div>
|
|
361
|
-
</div>
|
|
362
|
-
<div className="grid grid-cols-2 gap-4 overflow-y-auto">
|
|
363
|
-
<div className="flex flex-col gap-4">
|
|
364
|
-
<div>
|
|
365
|
-
<SectionHeading style={{marginBottom: 0}}>Kategorien</SectionHeading>
|
|
366
|
-
</div>
|
|
367
|
-
<div className="grid grid-cols-3 gap-4">
|
|
368
|
-
{categories?.map((category) => {
|
|
369
|
-
return (
|
|
370
|
-
<Checkbox
|
|
371
|
-
key={category.id}
|
|
372
|
-
isChecked={selectedTags.some((t) => category.tags.some((c) => c.id === t.id))}
|
|
373
|
-
onChange={(e) => {
|
|
374
|
-
let newTags = [...selectedTags];
|
|
375
|
-
|
|
376
|
-
if (e.target.checked) {
|
|
377
|
-
for (const tag of category.tags) {
|
|
378
|
-
if (!selectedTags.find((t) => t.id === tag.id)) {
|
|
379
|
-
newTags.push({
|
|
380
|
-
label: tag.label,
|
|
381
|
-
id: tag.id,
|
|
382
|
-
type: "or",
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
for (const tag of category.tags) {
|
|
388
|
-
newTags = newTags.filter((t) => t.id !== tag.id);
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
setSelectedTags(newTags);
|
|
393
|
-
}}
|
|
394
|
-
>
|
|
395
|
-
{category.title} ({selectedTags.filter((t) =>
|
|
396
|
-
category.tags.some((c) => c.id === t.id)).length}/{category.tags.length})
|
|
397
|
-
</Checkbox>
|
|
398
|
-
);
|
|
399
|
-
})}
|
|
400
|
-
</div>
|
|
401
|
-
</div>
|
|
402
|
-
<div className="flex flex-col gap-4">
|
|
403
|
-
<div>
|
|
404
|
-
<SectionHeading style={{marginBottom: 0}}>Tags</SectionHeading>
|
|
405
|
-
</div>
|
|
406
|
-
<div className="grid grid-cols-3 gap-4">
|
|
407
|
-
{tags?.map((tag) => {
|
|
408
|
-
return (
|
|
409
|
-
<Checkbox
|
|
410
|
-
key={tag.sys.id}
|
|
411
|
-
isChecked={!!selectedTags.find((t) => t.id === tag.sys.id)}
|
|
412
|
-
onChange={(e) => {
|
|
413
|
-
if (e.target.checked) {
|
|
414
|
-
setSelectedTags([...selectedTags, {
|
|
415
|
-
label: tag.fields.title[locale],
|
|
416
|
-
id: tag.sys.id,
|
|
417
|
-
type: "or",
|
|
418
|
-
}]);
|
|
419
|
-
} else {
|
|
420
|
-
setSelectedTags(selectedTags.filter((t) => t.id !== tag.sys.id));
|
|
421
|
-
}
|
|
422
|
-
}}
|
|
423
|
-
>
|
|
424
|
-
{tag.fields.title[locale]}
|
|
425
|
-
</Checkbox>
|
|
426
|
-
);
|
|
427
|
-
})}
|
|
428
|
-
</div>
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
<div className="sticky bottom-0 flex w-full flex-row justify-between gap-2 border-t border-gray-200 bg-white py-4">
|
|
432
|
-
<Button
|
|
433
|
-
variant="positive"
|
|
434
|
-
type="button"
|
|
435
|
-
onClick={() => onSave({
|
|
436
|
-
tags: selectedTags,
|
|
437
|
-
timepoints: selectedTimepoints,
|
|
438
|
-
})}>Speichern</Button>
|
|
439
|
-
<Button
|
|
440
|
-
variant="negative"
|
|
441
|
-
type="button"
|
|
442
|
-
onClick={() => onSave({
|
|
443
|
-
tags: [],
|
|
444
|
-
timepoints: [],
|
|
445
|
-
})}>Logik löschen</Button>
|
|
446
|
-
</div>
|
|
447
|
-
</div>;
|
|
448
|
-
};
|
|
449
|
-
|
|
450
|
-
async function getAllTagsFromCategory(categoryNode, allCategories = {}, parentCategoryTitle = null, portal = null, sdk = null) {
|
|
451
|
-
if (!categoryNode || !categoryNode.fields?.children) {
|
|
452
|
-
return allCategories;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const children = categoryNode.fields.children.de || [];
|
|
456
|
-
const title = parentCategoryTitle ? `${parentCategoryTitle} > ${categoryNode.fields.title.de}` : categoryNode.fields.title.de;
|
|
457
|
-
|
|
458
|
-
allCategories[categoryNode.sys.id] = {
|
|
459
|
-
title,
|
|
460
|
-
tags: [],
|
|
461
|
-
id: categoryNode.sys.id,
|
|
462
|
-
};
|
|
463
|
-
|
|
464
|
-
if (categoryNode?.fields?.tags?.de) {
|
|
465
|
-
const tagRefs = categoryNode.fields.tags.de;
|
|
466
|
-
|
|
467
|
-
if (portal === 'YACHT') {
|
|
468
|
-
// For YACHT portal, fetch all tag titles at once for better performance
|
|
469
|
-
const tagIds = tagRefs.map(tag => tag.sys.id).filter(id => id);
|
|
470
|
-
|
|
471
|
-
if (tagIds.length > 0) {
|
|
472
|
-
try {
|
|
473
|
-
const tagPromises = tagIds.map(tagId =>
|
|
474
|
-
sdk.cma.entry.get({entryId: tagId}).catch(() => null),
|
|
475
|
-
);
|
|
476
|
-
const fetchedTags = await Promise.all(tagPromises);
|
|
477
|
-
|
|
478
|
-
fetchedTags.forEach((fetchedTag, index) => {
|
|
479
|
-
if (fetchedTag?.fields?.title?.de) {
|
|
480
|
-
const tagId = tagIds[index];
|
|
481
|
-
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tagId)) {
|
|
482
|
-
allCategories[categoryNode.sys.id].tags.push({
|
|
483
|
-
label: fetchedTag.fields.title.de,
|
|
484
|
-
id: tagId,
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
} catch (e) {
|
|
490
|
-
console.error("Error fetching tags for YACHT portal:", e);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
} else {
|
|
494
|
-
// For non-YACHT portals, tags should have fields already included
|
|
495
|
-
for (const tag of tagRefs) {
|
|
496
|
-
if (!tag?.fields?.title) {
|
|
497
|
-
continue;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
|
|
501
|
-
allCategories[categoryNode.sys.id].tags.push({
|
|
502
|
-
label: tag.fields.title.de,
|
|
503
|
-
id: tag.sys.id,
|
|
504
|
-
});
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
for (const child of children) {
|
|
511
|
-
if (!child || !child.fields) {
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (child?.fields?.tags?.de) {
|
|
516
|
-
const childTagRefs = child.fields.tags.de;
|
|
517
|
-
|
|
518
|
-
if (portal === 'YACHT') {
|
|
519
|
-
// For YACHT portal, fetch child tag titles
|
|
520
|
-
const childTagIds = childTagRefs.map(tag => tag.sys.id).filter(id => id);
|
|
521
|
-
|
|
522
|
-
if (childTagIds.length > 0) {
|
|
523
|
-
try {
|
|
524
|
-
const childTagPromises = childTagIds.map(tagId =>
|
|
525
|
-
sdk.cma.entry.get({entryId: tagId}).catch(() => null),
|
|
526
|
-
);
|
|
527
|
-
const fetchedChildTags = await Promise.all(childTagPromises);
|
|
528
|
-
|
|
529
|
-
fetchedChildTags.forEach((fetchedTag, index) => {
|
|
530
|
-
if (fetchedTag?.fields?.title?.de) {
|
|
531
|
-
const tagId = childTagIds[index];
|
|
532
|
-
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tagId)) {
|
|
533
|
-
allCategories[categoryNode.sys.id].tags.push({
|
|
534
|
-
label: fetchedTag.fields.title.de,
|
|
535
|
-
id: tagId,
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
} catch (e) {
|
|
541
|
-
console.error("Error fetching child tags for YACHT portal:", e);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
} else {
|
|
545
|
-
// For non-YACHT portals, child tags should have fields already included
|
|
546
|
-
for (const tag of childTagRefs) {
|
|
547
|
-
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
|
|
548
|
-
if (!tag?.fields?.title) {
|
|
549
|
-
continue;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
allCategories[categoryNode.sys.id].tags.push({
|
|
553
|
-
label: tag.fields.title.de,
|
|
554
|
-
id: tag.sys.id,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
await getAllTagsFromCategory(child, allCategories, title, portal, sdk);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
allCategories[categoryNode.sys.id].tags = [...new Set(allCategories[categoryNode.sys.id].tags)];
|
|
565
|
-
|
|
566
|
-
return allCategories;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
function DraggablePill({
|
|
571
|
-
id, ...props
|
|
572
|
-
}) {
|
|
573
|
-
const {
|
|
574
|
-
attributes,
|
|
575
|
-
listeners,
|
|
576
|
-
setNodeRef,
|
|
577
|
-
transform,
|
|
578
|
-
transition,
|
|
579
|
-
} = useDraggable({id});
|
|
580
|
-
const style = {
|
|
581
|
-
transform: CSS.Translate.toString(transform),
|
|
582
|
-
transition,
|
|
583
|
-
};
|
|
584
|
-
|
|
585
|
-
return (
|
|
586
|
-
<div>
|
|
587
|
-
<Pill
|
|
588
|
-
dragHandleComponent={
|
|
589
|
-
<DragHandle
|
|
590
|
-
label="Reorder item"
|
|
591
|
-
variant="transparent"
|
|
592
|
-
{...attributes}
|
|
593
|
-
{...listeners}
|
|
594
|
-
/>
|
|
595
|
-
}
|
|
596
|
-
isDraggable
|
|
597
|
-
ref={setNodeRef}
|
|
598
|
-
style={style}
|
|
599
|
-
{...props}
|
|
600
|
-
/>
|
|
601
|
-
</div>
|
|
602
|
-
);
|
|
1
|
+
import {
|
|
2
|
+
TextInput,
|
|
3
|
+
Pill,
|
|
4
|
+
SectionHeading,
|
|
5
|
+
Checkbox,
|
|
6
|
+
Button,
|
|
7
|
+
DragHandle,
|
|
8
|
+
} from "@contentful/f36-components";
|
|
9
|
+
import {
|
|
10
|
+
DndContext, useDraggable, useDroppable,
|
|
11
|
+
PointerSensor, useSensor, useSensors,
|
|
12
|
+
} from '@dnd-kit/core';
|
|
13
|
+
import contentfulResolveResponse from "contentful-resolve-response";
|
|
14
|
+
import {
|
|
15
|
+
useEffect, useState,
|
|
16
|
+
} from "react";
|
|
17
|
+
import useDebounce from "../../../hooks/useDebounce";
|
|
18
|
+
import {getContentfulClient} from "../../../lib/contentfulClient";
|
|
19
|
+
import {PlusIcon} from "@contentful/f36-icons";
|
|
20
|
+
import {CSS} from "@dnd-kit/utilities";
|
|
21
|
+
import {queryFromLogic} from "../../../queryFromLogic";
|
|
22
|
+
import {useQuery} from "@tanstack/react-query";
|
|
23
|
+
export const LogicEditor = (props) => {
|
|
24
|
+
const [selectedTags, setSelectedTags] = useState([]);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
setSelectedTags(props?.logicData?.tags || []);
|
|
28
|
+
}, [props?.logicData?.tags]);
|
|
29
|
+
|
|
30
|
+
const handleDragEnd = (event) => {
|
|
31
|
+
const {
|
|
32
|
+
over,
|
|
33
|
+
active,
|
|
34
|
+
} = event;
|
|
35
|
+
|
|
36
|
+
if (over) {
|
|
37
|
+
const draggedTagIndex = selectedTags.findIndex(tag => tag.id === active.id);
|
|
38
|
+
if (draggedTagIndex !== -1) {
|
|
39
|
+
const updatedTags = [...selectedTags];
|
|
40
|
+
updatedTags[draggedTagIndex] = {
|
|
41
|
+
...updatedTags[draggedTagIndex],
|
|
42
|
+
type: over.id, // "and" or "or"
|
|
43
|
+
};
|
|
44
|
+
setSelectedTags(updatedTags);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
return <DndContext onDragEnd={handleDragEnd}>
|
|
50
|
+
<LogicEditorInner {...props} selectedTags={selectedTags} setSelectedTags={setSelectedTags} />
|
|
51
|
+
</DndContext>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const LogicEditorInner = ({
|
|
55
|
+
onSave = () => alert("onSave missing"),
|
|
56
|
+
sdk,
|
|
57
|
+
logicData,
|
|
58
|
+
selectedTags,
|
|
59
|
+
setSelectedTags,
|
|
60
|
+
}) => {
|
|
61
|
+
const contentfulClient = getContentfulClient();
|
|
62
|
+
const [loading, setLoading] = useState(false);
|
|
63
|
+
const [tags, setTags] = useState([]);
|
|
64
|
+
const [categories, setCategories] = useState([]);
|
|
65
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
66
|
+
const [addTimepoint, setAddTimepoint] = useState(7);
|
|
67
|
+
const debouncedSearch = useDebounce(searchQuery, 500);
|
|
68
|
+
const [selectedTimepoints, setSelectedTimepoints] = useState([]);
|
|
69
|
+
const {
|
|
70
|
+
portal,
|
|
71
|
+
locale = "de",
|
|
72
|
+
} = sdk.parameters.invocation;
|
|
73
|
+
|
|
74
|
+
const {data: articles} = useQuery({
|
|
75
|
+
enabled: !!selectedTags.length && !!portal && !!contentfulClient,
|
|
76
|
+
queryKey: ["articles", selectedTags, portal],
|
|
77
|
+
queryFn: async () => {
|
|
78
|
+
const query = queryFromLogic({
|
|
79
|
+
tags: selectedTags,
|
|
80
|
+
project: portal,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
const result = await contentfulClient.getEntries({
|
|
85
|
+
content_type: "article",
|
|
86
|
+
locale: "de",
|
|
87
|
+
order: "-fields.date",
|
|
88
|
+
limit: 10,
|
|
89
|
+
...query,
|
|
90
|
+
select: [
|
|
91
|
+
"fields.title",
|
|
92
|
+
"fields.slug",
|
|
93
|
+
"fields.date",
|
|
94
|
+
"sys.id",
|
|
95
|
+
].join(","),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(e);
|
|
101
|
+
return {items: []};
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (logicData?.timepoints) {
|
|
108
|
+
setSelectedTimepoints(logicData?.timepoints);
|
|
109
|
+
} else {
|
|
110
|
+
setSelectedTimepoints([7, 12, 18]);
|
|
111
|
+
}
|
|
112
|
+
}, [logicData]);
|
|
113
|
+
const {
|
|
114
|
+
setNodeRef: setOrNodeRef,
|
|
115
|
+
isOver: isOrOver,
|
|
116
|
+
node: orNode,
|
|
117
|
+
} = useDroppable({id: 'or'});
|
|
118
|
+
const {
|
|
119
|
+
setNodeRef: setAndNodeRef,
|
|
120
|
+
isOver: isAndOver,
|
|
121
|
+
node: andNode,
|
|
122
|
+
} = useDroppable({id: "and"});
|
|
123
|
+
const {
|
|
124
|
+
setNodeRef: setExcludeNodeRef,
|
|
125
|
+
isOver: isExcludeOver,
|
|
126
|
+
node: excludeNode,
|
|
127
|
+
} = useDroppable({id: "exclude"});
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
setLoading(true);
|
|
131
|
+
const params = {
|
|
132
|
+
limit: 100,
|
|
133
|
+
skip: 0,
|
|
134
|
+
content_type: "tags",
|
|
135
|
+
locale,
|
|
136
|
+
order: "-sys.updatedAt",
|
|
137
|
+
"fields.portal": portal,
|
|
138
|
+
select: [
|
|
139
|
+
"fields.title",
|
|
140
|
+
"fields.isCategory",
|
|
141
|
+
"fields.hidden",
|
|
142
|
+
"sys.publishedAt",
|
|
143
|
+
"sys.id",
|
|
144
|
+
].join(","),
|
|
145
|
+
};
|
|
146
|
+
const paramsNavigation = {
|
|
147
|
+
limit: 1,
|
|
148
|
+
content_type: "navigation",
|
|
149
|
+
include: 2,
|
|
150
|
+
locale,
|
|
151
|
+
"fields.portal": portal,
|
|
152
|
+
"fields.type": "main",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
if (debouncedSearch) {
|
|
156
|
+
params["fields.title[match]"] = debouncedSearch;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
Promise.all([
|
|
160
|
+
contentfulClient.getEntries(params),
|
|
161
|
+
contentfulClient.getEntries(paramsNavigation).then(async (response) => {
|
|
162
|
+
if (!response?.items?.[0]) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const allCategories = {};
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Use different include levels based on portal to avoid multilingual content bloat
|
|
169
|
+
const includeLevel = portal === 'YACHT' ? 1 : 2;
|
|
170
|
+
const references = await sdk.cma.entry.references({entryId: response.items[0].sys.id, include: includeLevel});
|
|
171
|
+
const resolved = contentfulResolveResponse(references);
|
|
172
|
+
|
|
173
|
+
for (const category of resolved) {
|
|
174
|
+
if (!category?.fields?.children?.de) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const children = category.fields.children.de || [];
|
|
179
|
+
|
|
180
|
+
for (const child of children) {
|
|
181
|
+
await getAllTagsFromCategory(child, allCategories, null, portal, sdk);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.error("Error getting categories and tags from references: ", e);
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return Object.values(allCategories).filter((c) => c.tags.length > 0);
|
|
190
|
+
}),
|
|
191
|
+
]).then(([tags, categories]) => {
|
|
192
|
+
setLoading(false);
|
|
193
|
+
setTags(tags.items);
|
|
194
|
+
setCategories(categories);
|
|
195
|
+
});
|
|
196
|
+
}, [debouncedSearch, portal, locale]);
|
|
197
|
+
|
|
198
|
+
const andTags = selectedTags.filter((t) => t.type === "and");
|
|
199
|
+
const orTags = selectedTags.filter((t) => t.type === "or");
|
|
200
|
+
const excludeTags = selectedTags.filter((t) => t.type === "exclude");
|
|
201
|
+
|
|
202
|
+
return <div className="flex flex-col gap-2 p-4">
|
|
203
|
+
<div className="z-10 flex w-full flex-col gap-4 border-b border-gray-200 bg-white py-4">
|
|
204
|
+
<div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
|
|
205
|
+
<SectionHeading style={{marginBottom: 0}}>Gewählte Zeitpunkte:</SectionHeading>
|
|
206
|
+
<div className="flex flex-row items-center gap-2">
|
|
207
|
+
{selectedTimepoints.map((timepoint) => {
|
|
208
|
+
return <Pill
|
|
209
|
+
key={timepoint}
|
|
210
|
+
draggable={false}
|
|
211
|
+
testId="pill-item"
|
|
212
|
+
variant="active"
|
|
213
|
+
label={`${timepoint} Uhr`}
|
|
214
|
+
onClose={() => {
|
|
215
|
+
setSelectedTimepoints(selectedTimepoints.filter((t) => t !== timepoint));
|
|
216
|
+
}}
|
|
217
|
+
/>;
|
|
218
|
+
})}
|
|
219
|
+
<TextInput
|
|
220
|
+
type="number"
|
|
221
|
+
min={1}
|
|
222
|
+
max={24}
|
|
223
|
+
value={addTimepoint}
|
|
224
|
+
onChange={(e) => {
|
|
225
|
+
setAddTimepoint(e.target.value);
|
|
226
|
+
}}
|
|
227
|
+
/>
|
|
228
|
+
<SectionHeading style={{marginBottom: 0}}>Uhr</SectionHeading>
|
|
229
|
+
<Button
|
|
230
|
+
variant="secondary"
|
|
231
|
+
onClick={() => {
|
|
232
|
+
if (selectedTimepoints.includes(addTimepoint)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setSelectedTimepoints([...selectedTimepoints, addTimepoint]);
|
|
237
|
+
}}
|
|
238
|
+
>
|
|
239
|
+
<PlusIcon />
|
|
240
|
+
</Button>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
<div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
|
|
244
|
+
<SectionHeading style={{marginBottom: 0}}>Gewählte Kategorien & Tags:</SectionHeading>
|
|
245
|
+
<div className="grid w-full grid-cols-5 gap-4">
|
|
246
|
+
<div className="flex w-full flex-col gap-2">
|
|
247
|
+
<div>
|
|
248
|
+
<SectionHeading style={{marginBottom: 0}}>Oder</SectionHeading>
|
|
249
|
+
</div>
|
|
250
|
+
<div
|
|
251
|
+
ref={setOrNodeRef}
|
|
252
|
+
style={{
|
|
253
|
+
minHeight: 36,
|
|
254
|
+
backgroundColor: isOrOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
|
|
255
|
+
}}
|
|
256
|
+
className="flex h-full w-full flex-row flex-wrap gap-2"
|
|
257
|
+
>
|
|
258
|
+
{orTags.map((tag) => {
|
|
259
|
+
return <DraggablePill
|
|
260
|
+
key={tag.id}
|
|
261
|
+
id={tag.id}
|
|
262
|
+
variant="active"
|
|
263
|
+
label={(`${tag.label}`)}
|
|
264
|
+
onClose={() => {
|
|
265
|
+
setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
|
|
266
|
+
}}
|
|
267
|
+
/>;
|
|
268
|
+
})}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
<div className="flex w-full flex-col gap-2">
|
|
272
|
+
<SectionHeading style={{marginBottom: 0}}>Und</SectionHeading>
|
|
273
|
+
<div
|
|
274
|
+
ref={setAndNodeRef}
|
|
275
|
+
style={{
|
|
276
|
+
minHeight: 36,
|
|
277
|
+
backgroundColor: isAndOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
|
|
278
|
+
}}
|
|
279
|
+
className="flex h-full w-full flex-row flex-wrap gap-2"
|
|
280
|
+
>
|
|
281
|
+
{andTags.map((tag) => {
|
|
282
|
+
return <DraggablePill
|
|
283
|
+
key={tag.id}
|
|
284
|
+
id={tag.id}
|
|
285
|
+
testId={tag.id}
|
|
286
|
+
variant="active"
|
|
287
|
+
label={(`${tag.label}`)}
|
|
288
|
+
onClose={() => {
|
|
289
|
+
setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
|
|
290
|
+
}}
|
|
291
|
+
/>;
|
|
292
|
+
})}
|
|
293
|
+
</div>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="flex w-full flex-col gap-2">
|
|
296
|
+
<SectionHeading style={{marginBottom: 0}}>Ausschließen</SectionHeading>
|
|
297
|
+
<div
|
|
298
|
+
ref={setExcludeNodeRef}
|
|
299
|
+
style={{
|
|
300
|
+
minHeight: 36,
|
|
301
|
+
backgroundColor: isExcludeOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
|
|
302
|
+
}}
|
|
303
|
+
className="flex h-full w-full flex-row flex-wrap gap-2"
|
|
304
|
+
>
|
|
305
|
+
{excludeTags.map((tag) => {
|
|
306
|
+
return <DraggablePill
|
|
307
|
+
key={tag.id}
|
|
308
|
+
id={tag.id}
|
|
309
|
+
testId={tag.id}
|
|
310
|
+
variant="active"
|
|
311
|
+
label={(`${tag.label}`)}
|
|
312
|
+
onClose={() => {
|
|
313
|
+
setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
|
|
314
|
+
}}
|
|
315
|
+
/>;
|
|
316
|
+
})}
|
|
317
|
+
</div>
|
|
318
|
+
</div>
|
|
319
|
+
<div className="col-span-2 flex w-full flex-col gap-4">
|
|
320
|
+
<div>
|
|
321
|
+
<SectionHeading style={{marginBottom: 0}}>Artikel</SectionHeading>
|
|
322
|
+
</div>
|
|
323
|
+
<div className="flex flex-col gap-4">
|
|
324
|
+
<table className="w-full max-w-full table-auto gap-4 text-left">
|
|
325
|
+
<thead>
|
|
326
|
+
<tr>
|
|
327
|
+
<th>Datum</th>
|
|
328
|
+
<th className="pl-4">Titel</th>
|
|
329
|
+
</tr>
|
|
330
|
+
</thead>
|
|
331
|
+
<tbody>
|
|
332
|
+
{articles?.items?.map((article) => {
|
|
333
|
+
return <tr key={article.sys.id}>
|
|
334
|
+
<td className="whitespace-nowrap">
|
|
335
|
+
{new Date(article.fields.date.de).toLocaleString("de-DE", {
|
|
336
|
+
hour: "2-digit",
|
|
337
|
+
minute: "2-digit",
|
|
338
|
+
day: "2-digit",
|
|
339
|
+
month: "2-digit",
|
|
340
|
+
year: "numeric",
|
|
341
|
+
})}
|
|
342
|
+
</td>
|
|
343
|
+
<td className="truncate whitespace-nowrap pl-4">
|
|
344
|
+
{article.fields.title.de}
|
|
345
|
+
</td>
|
|
346
|
+
</tr>;
|
|
347
|
+
})}
|
|
348
|
+
</tbody>
|
|
349
|
+
</table>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
<div className="flex w-full flex-row gap-2">
|
|
355
|
+
<TextInput
|
|
356
|
+
placeholder="Suche nach Kategorie oder Tag"
|
|
357
|
+
value={searchQuery}
|
|
358
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
359
|
+
/>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
<div className="grid grid-cols-2 gap-4 overflow-y-auto">
|
|
363
|
+
<div className="flex flex-col gap-4">
|
|
364
|
+
<div>
|
|
365
|
+
<SectionHeading style={{marginBottom: 0}}>Kategorien</SectionHeading>
|
|
366
|
+
</div>
|
|
367
|
+
<div className="grid grid-cols-3 gap-4">
|
|
368
|
+
{categories?.map((category) => {
|
|
369
|
+
return (
|
|
370
|
+
<Checkbox
|
|
371
|
+
key={category.id}
|
|
372
|
+
isChecked={selectedTags.some((t) => category.tags.some((c) => c.id === t.id))}
|
|
373
|
+
onChange={(e) => {
|
|
374
|
+
let newTags = [...selectedTags];
|
|
375
|
+
|
|
376
|
+
if (e.target.checked) {
|
|
377
|
+
for (const tag of category.tags) {
|
|
378
|
+
if (!selectedTags.find((t) => t.id === tag.id)) {
|
|
379
|
+
newTags.push({
|
|
380
|
+
label: tag.label,
|
|
381
|
+
id: tag.id,
|
|
382
|
+
type: "or",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
} else {
|
|
387
|
+
for (const tag of category.tags) {
|
|
388
|
+
newTags = newTags.filter((t) => t.id !== tag.id);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
setSelectedTags(newTags);
|
|
393
|
+
}}
|
|
394
|
+
>
|
|
395
|
+
{category.title} ({selectedTags.filter((t) =>
|
|
396
|
+
category.tags.some((c) => c.id === t.id)).length}/{category.tags.length})
|
|
397
|
+
</Checkbox>
|
|
398
|
+
);
|
|
399
|
+
})}
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
<div className="flex flex-col gap-4">
|
|
403
|
+
<div>
|
|
404
|
+
<SectionHeading style={{marginBottom: 0}}>Tags</SectionHeading>
|
|
405
|
+
</div>
|
|
406
|
+
<div className="grid grid-cols-3 gap-4">
|
|
407
|
+
{tags?.map((tag) => {
|
|
408
|
+
return (
|
|
409
|
+
<Checkbox
|
|
410
|
+
key={tag.sys.id}
|
|
411
|
+
isChecked={!!selectedTags.find((t) => t.id === tag.sys.id)}
|
|
412
|
+
onChange={(e) => {
|
|
413
|
+
if (e.target.checked) {
|
|
414
|
+
setSelectedTags([...selectedTags, {
|
|
415
|
+
label: tag.fields.title[locale],
|
|
416
|
+
id: tag.sys.id,
|
|
417
|
+
type: "or",
|
|
418
|
+
}]);
|
|
419
|
+
} else {
|
|
420
|
+
setSelectedTags(selectedTags.filter((t) => t.id !== tag.sys.id));
|
|
421
|
+
}
|
|
422
|
+
}}
|
|
423
|
+
>
|
|
424
|
+
{tag.fields.title[locale]}
|
|
425
|
+
</Checkbox>
|
|
426
|
+
);
|
|
427
|
+
})}
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
<div className="sticky bottom-0 flex w-full flex-row justify-between gap-2 border-t border-gray-200 bg-white py-4">
|
|
432
|
+
<Button
|
|
433
|
+
variant="positive"
|
|
434
|
+
type="button"
|
|
435
|
+
onClick={() => onSave({
|
|
436
|
+
tags: selectedTags,
|
|
437
|
+
timepoints: selectedTimepoints,
|
|
438
|
+
})}>Speichern</Button>
|
|
439
|
+
<Button
|
|
440
|
+
variant="negative"
|
|
441
|
+
type="button"
|
|
442
|
+
onClick={() => onSave({
|
|
443
|
+
tags: [],
|
|
444
|
+
timepoints: [],
|
|
445
|
+
})}>Logik löschen</Button>
|
|
446
|
+
</div>
|
|
447
|
+
</div>;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
async function getAllTagsFromCategory(categoryNode, allCategories = {}, parentCategoryTitle = null, portal = null, sdk = null) {
|
|
451
|
+
if (!categoryNode || !categoryNode.fields?.children) {
|
|
452
|
+
return allCategories;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const children = categoryNode.fields.children.de || [];
|
|
456
|
+
const title = parentCategoryTitle ? `${parentCategoryTitle} > ${categoryNode.fields.title.de}` : categoryNode.fields.title.de;
|
|
457
|
+
|
|
458
|
+
allCategories[categoryNode.sys.id] = {
|
|
459
|
+
title,
|
|
460
|
+
tags: [],
|
|
461
|
+
id: categoryNode.sys.id,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
if (categoryNode?.fields?.tags?.de) {
|
|
465
|
+
const tagRefs = categoryNode.fields.tags.de;
|
|
466
|
+
|
|
467
|
+
if (portal === 'YACHT') {
|
|
468
|
+
// For YACHT portal, fetch all tag titles at once for better performance
|
|
469
|
+
const tagIds = tagRefs.map(tag => tag.sys.id).filter(id => id);
|
|
470
|
+
|
|
471
|
+
if (tagIds.length > 0) {
|
|
472
|
+
try {
|
|
473
|
+
const tagPromises = tagIds.map(tagId =>
|
|
474
|
+
sdk.cma.entry.get({entryId: tagId}).catch(() => null),
|
|
475
|
+
);
|
|
476
|
+
const fetchedTags = await Promise.all(tagPromises);
|
|
477
|
+
|
|
478
|
+
fetchedTags.forEach((fetchedTag, index) => {
|
|
479
|
+
if (fetchedTag?.fields?.title?.de) {
|
|
480
|
+
const tagId = tagIds[index];
|
|
481
|
+
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tagId)) {
|
|
482
|
+
allCategories[categoryNode.sys.id].tags.push({
|
|
483
|
+
label: fetchedTag.fields.title.de,
|
|
484
|
+
id: tagId,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
} catch (e) {
|
|
490
|
+
console.error("Error fetching tags for YACHT portal:", e);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
} else {
|
|
494
|
+
// For non-YACHT portals, tags should have fields already included
|
|
495
|
+
for (const tag of tagRefs) {
|
|
496
|
+
if (!tag?.fields?.title) {
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
|
|
501
|
+
allCategories[categoryNode.sys.id].tags.push({
|
|
502
|
+
label: tag.fields.title.de,
|
|
503
|
+
id: tag.sys.id,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
for (const child of children) {
|
|
511
|
+
if (!child || !child.fields) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (child?.fields?.tags?.de) {
|
|
516
|
+
const childTagRefs = child.fields.tags.de;
|
|
517
|
+
|
|
518
|
+
if (portal === 'YACHT') {
|
|
519
|
+
// For YACHT portal, fetch child tag titles
|
|
520
|
+
const childTagIds = childTagRefs.map(tag => tag.sys.id).filter(id => id);
|
|
521
|
+
|
|
522
|
+
if (childTagIds.length > 0) {
|
|
523
|
+
try {
|
|
524
|
+
const childTagPromises = childTagIds.map(tagId =>
|
|
525
|
+
sdk.cma.entry.get({entryId: tagId}).catch(() => null),
|
|
526
|
+
);
|
|
527
|
+
const fetchedChildTags = await Promise.all(childTagPromises);
|
|
528
|
+
|
|
529
|
+
fetchedChildTags.forEach((fetchedTag, index) => {
|
|
530
|
+
if (fetchedTag?.fields?.title?.de) {
|
|
531
|
+
const tagId = childTagIds[index];
|
|
532
|
+
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tagId)) {
|
|
533
|
+
allCategories[categoryNode.sys.id].tags.push({
|
|
534
|
+
label: fetchedTag.fields.title.de,
|
|
535
|
+
id: tagId,
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
} catch (e) {
|
|
541
|
+
console.error("Error fetching child tags for YACHT portal:", e);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
} else {
|
|
545
|
+
// For non-YACHT portals, child tags should have fields already included
|
|
546
|
+
for (const tag of childTagRefs) {
|
|
547
|
+
if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
|
|
548
|
+
if (!tag?.fields?.title) {
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
allCategories[categoryNode.sys.id].tags.push({
|
|
553
|
+
label: tag.fields.title.de,
|
|
554
|
+
id: tag.sys.id,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
await getAllTagsFromCategory(child, allCategories, title, portal, sdk);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
allCategories[categoryNode.sys.id].tags = [...new Set(allCategories[categoryNode.sys.id].tags)];
|
|
565
|
+
|
|
566
|
+
return allCategories;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
function DraggablePill({
|
|
571
|
+
id, ...props
|
|
572
|
+
}) {
|
|
573
|
+
const {
|
|
574
|
+
attributes,
|
|
575
|
+
listeners,
|
|
576
|
+
setNodeRef,
|
|
577
|
+
transform,
|
|
578
|
+
transition,
|
|
579
|
+
} = useDraggable({id});
|
|
580
|
+
const style = {
|
|
581
|
+
transform: CSS.Translate.toString(transform),
|
|
582
|
+
transition,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
return (
|
|
586
|
+
<div>
|
|
587
|
+
<Pill
|
|
588
|
+
dragHandleComponent={
|
|
589
|
+
<DragHandle
|
|
590
|
+
label="Reorder item"
|
|
591
|
+
variant="transparent"
|
|
592
|
+
{...attributes}
|
|
593
|
+
{...listeners}
|
|
594
|
+
/>
|
|
595
|
+
}
|
|
596
|
+
isDraggable
|
|
597
|
+
ref={setNodeRef}
|
|
598
|
+
style={style}
|
|
599
|
+
{...props}
|
|
600
|
+
/>
|
|
601
|
+
</div>
|
|
602
|
+
);
|
|
603
603
|
}
|