@vonaffenfels/contentful-teasermanager 1.2.9 → 1.2.10

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.
@@ -1,504 +1,504 @@
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
-
124
- useEffect(() => {
125
- setLoading(true);
126
- const params = {
127
- limit: 100,
128
- skip: 0,
129
- content_type: "tags",
130
- locale,
131
- order: "-sys.updatedAt",
132
- "fields.portal": portal,
133
- select: [
134
- "fields.title",
135
- "fields.isCategory",
136
- "fields.hidden",
137
- "sys.publishedAt",
138
- "sys.id",
139
- ].join(","),
140
- };
141
- const paramsNavigation = {
142
- limit: 1,
143
- content_type: "navigation",
144
- include: 2,
145
- locale,
146
- "fields.portal": portal,
147
- "fields.type": "main",
148
- };
149
-
150
- if (debouncedSearch) {
151
- params["fields.title[match]"] = debouncedSearch;
152
- }
153
-
154
- Promise.all([
155
- contentfulClient.getEntries(params),
156
- contentfulClient.getEntries(paramsNavigation).then(async (response) => {
157
- if (!response?.items?.[0]) {
158
- return [];
159
- }
160
-
161
- const references = await sdk.cma.entry.references({entryId: response.items[0].sys.id}, {include: 10});
162
- const resolved = contentfulResolveResponse(references);
163
- const allCategories = {};
164
-
165
- for (const category of resolved) {
166
- if (!category?.fields?.children?.de) {
167
- continue;
168
- }
169
-
170
- const children = category.fields.children.de || [];
171
-
172
- for (const child of children) {
173
- getAllTagsFromCategory(child, allCategories);
174
- }
175
- }
176
-
177
- return Object.values(allCategories).filter((c) => c.tags.length > 0);
178
- }),
179
- ]).then(([tags, categories]) => {
180
- setLoading(false);
181
- setTags(tags.items);
182
- setCategories(categories);
183
- });
184
- }, [debouncedSearch, portal, locale]);
185
-
186
- const andTags = selectedTags.filter((t) => t.type === "and");
187
- const orTags = selectedTags.filter((t) => t.type === "or");
188
-
189
- return <div className="flex flex-col gap-2 p-4">
190
- <div className="z-10 flex w-full flex-col gap-4 border-b border-gray-200 bg-white py-4">
191
- <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
192
- <SectionHeading style={{marginBottom: 0}}>Gewählte Zeitpunkte:</SectionHeading>
193
- <div className="flex flex-row items-center gap-2">
194
- {selectedTimepoints.map((timepoint) => {
195
- return <Pill
196
- key={timepoint}
197
- draggable={false}
198
- testId="pill-item"
199
- variant="active"
200
- label={`${timepoint} Uhr`}
201
- onClose={() => {
202
- setSelectedTimepoints(selectedTimepoints.filter((t) => t !== timepoint));
203
- }}
204
- />;
205
- })}
206
- <TextInput
207
- type="number"
208
- min={1}
209
- max={24}
210
- value={addTimepoint}
211
- onChange={(e) => {
212
- setAddTimepoint(e.target.value);
213
- }}
214
- />
215
- <SectionHeading style={{marginBottom: 0}}>Uhr</SectionHeading>
216
- <Button
217
- variant="secondary"
218
- onClick={() => {
219
- if (selectedTimepoints.includes(addTimepoint)) {
220
- return;
221
- }
222
-
223
- setSelectedTimepoints([...selectedTimepoints, addTimepoint]);
224
- }}
225
- >
226
- <PlusIcon />
227
- </Button>
228
- </div>
229
- </div>
230
- <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
231
- <SectionHeading style={{marginBottom: 0}}>Gewählte Kategorien & Tags:</SectionHeading>
232
- <div className="grid w-full grid-cols-4 gap-4">
233
- <div className="flex w-full flex-col gap-2">
234
- <div>
235
- <SectionHeading style={{marginBottom: 0}}>Oder</SectionHeading>
236
- </div>
237
- <div
238
- ref={setOrNodeRef}
239
- style={{
240
- minHeight: 36,
241
- backgroundColor: isOrOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
242
- }}
243
- className="flex h-full w-full flex-row flex-wrap gap-2"
244
- >
245
- {orTags.map((tag) => {
246
- return <DraggablePill
247
- key={tag.id}
248
- id={tag.id}
249
- variant="active"
250
- label={(`${tag.label}`)}
251
- onClose={() => {
252
- setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
253
- }}
254
- />;
255
- })}
256
- </div>
257
- </div>
258
- <div className="flex w-full flex-col gap-2">
259
- <SectionHeading style={{marginBottom: 0}}>Und</SectionHeading>
260
- <div
261
- ref={setAndNodeRef}
262
- style={{
263
- minHeight: 36,
264
- backgroundColor: isAndOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
265
- }}
266
- className="flex h-full w-full flex-row flex-wrap gap-2"
267
- >
268
- {andTags.map((tag) => {
269
- return <DraggablePill
270
- key={tag.id}
271
- id={tag.id}
272
- testId={tag.id}
273
- variant="active"
274
- label={(`${tag.label}`)}
275
- onClose={() => {
276
- setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
277
- }}
278
- />;
279
- })}
280
- </div>
281
- </div>
282
- <div className="col-span-2 flex w-full flex-col gap-4">
283
- <div>
284
- <SectionHeading style={{marginBottom: 0}}>Artikel</SectionHeading>
285
- </div>
286
- <div className="flex flex-col gap-4">
287
- <table className="w-full max-w-full table-auto gap-4 text-left">
288
- <thead>
289
- <tr>
290
- <th>Datum</th>
291
- <th className="pl-4">Titel</th>
292
- </tr>
293
- </thead>
294
- <tbody>
295
- {articles?.items?.map((article) => {
296
- return <tr key={article.sys.id}>
297
- <td className="whitespace-nowrap">
298
- {new Date(article.fields.date.de).toLocaleString("de-DE", {
299
- hour: "2-digit",
300
- minute: "2-digit",
301
- day: "2-digit",
302
- month: "2-digit",
303
- year: "numeric",
304
- })}
305
- </td>
306
- <td className="truncate whitespace-nowrap pl-4">
307
- {article.fields.title.de}
308
- </td>
309
- </tr>;
310
- })}
311
- </tbody>
312
- </table>
313
- </div>
314
- </div>
315
- </div>
316
- </div>
317
- <div className="flex w-full flex-row gap-2">
318
- <TextInput
319
- placeholder="Suche nach Kategorie oder Tag"
320
- value={searchQuery}
321
- onChange={(e) => setSearchQuery(e.target.value)}
322
- />
323
- </div>
324
- </div>
325
- <div className="grid grid-cols-2 gap-4 overflow-y-auto">
326
- <div className="flex flex-col gap-4">
327
- <div>
328
- <SectionHeading style={{marginBottom: 0}}>Kategorien</SectionHeading>
329
- </div>
330
- <div className="grid grid-cols-3 gap-4">
331
- {categories?.map((category) => {
332
- return (
333
- <Checkbox
334
- key={category.id}
335
- isChecked={selectedTags.some((t) => category.tags.some((c) => c.id === t.id))}
336
- onChange={(e) => {
337
- let newTags = [...selectedTags];
338
-
339
- if (e.target.checked) {
340
- for (const tag of category.tags) {
341
- if (!selectedTags.find((t) => t.id === tag.id)) {
342
- newTags.push({
343
- label: tag.label,
344
- id: tag.id,
345
- type: "or",
346
- });
347
- }
348
- }
349
- } else {
350
- for (const tag of category.tags) {
351
- newTags = newTags.filter((t) => t.id !== tag.id);
352
- }
353
- }
354
-
355
- setSelectedTags(newTags);
356
- }}
357
- >
358
- {category.title} ({selectedTags.filter((t) =>
359
- category.tags.some((c) => c.id === t.id)).length}/{category.tags.length})
360
- </Checkbox>
361
- );
362
- })}
363
- </div>
364
- </div>
365
- <div className="flex flex-col gap-4">
366
- <div>
367
- <SectionHeading style={{marginBottom: 0}}>Tags</SectionHeading>
368
- </div>
369
- <div className="grid grid-cols-3 gap-4">
370
- {tags?.map((tag) => {
371
- return (
372
- <Checkbox
373
- key={tag.sys.id}
374
- isChecked={!!selectedTags.find((t) => t.id === tag.sys.id)}
375
- onChange={(e) => {
376
- if (e.target.checked) {
377
- setSelectedTags([...selectedTags, {
378
- label: tag.fields.title[locale],
379
- id: tag.sys.id,
380
- type: "or",
381
- }]);
382
- } else {
383
- setSelectedTags(selectedTags.filter((t) => t.id !== tag.sys.id));
384
- }
385
- }}
386
- >
387
- {tag.fields.title[locale]}
388
- </Checkbox>
389
- );
390
- })}
391
- </div>
392
- </div>
393
- </div>
394
- <div className="sticky bottom-0 flex w-full flex-row justify-between gap-2 border-t border-gray-200 bg-white py-4">
395
- <Button
396
- variant="positive"
397
- type="button"
398
- onClick={() => onSave({
399
- tags: selectedTags,
400
- timepoints: selectedTimepoints,
401
- })}>Speichern</Button>
402
- <Button
403
- variant="negative"
404
- type="button"
405
- onClick={() => onSave({
406
- tags: [],
407
- timepoints: [],
408
- })}>Logik löschen</Button>
409
- </div>
410
- </div>;
411
- };
412
-
413
- function getAllTagsFromCategory(categoryNode, allCategories = {}, parentCategoryTitle = null) {
414
- if (!categoryNode || !categoryNode.fields?.children) {
415
- return allCategories;
416
- }
417
-
418
- const children = categoryNode.fields.children.de || [];
419
- const title = parentCategoryTitle ? `${parentCategoryTitle} > ${categoryNode.fields.title.de}` : categoryNode.fields.title.de;
420
-
421
- allCategories[categoryNode.sys.id] = {
422
- title,
423
- tags: [],
424
- id: categoryNode.sys.id,
425
- };
426
-
427
- if (categoryNode?.fields?.tags?.de) {
428
- for (const tag of categoryNode.fields.tags.de) {
429
- if (!tag?.fields?.title) {
430
- continue;
431
- }
432
-
433
- if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
434
- allCategories[categoryNode.sys.id].tags.push({
435
- label: tag.fields.title.de,
436
- id: tag.sys.id,
437
- });
438
- }
439
- }
440
- }
441
-
442
- for (const child of children) {
443
- if (!child || !child.fields) {
444
- continue;
445
- }
446
-
447
- if (child?.fields?.tags?.de) {
448
- for (const tag of child.fields.tags.de) {
449
- if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
450
- if (!tag?.fields?.title) {
451
- continue;
452
- }
453
-
454
- allCategories[categoryNode.sys.id].tags.push({
455
- label: tag.fields.title.de,
456
- id: tag.sys.id,
457
- });
458
- }
459
- }
460
- }
461
-
462
- getAllTagsFromCategory(child, allCategories, title);
463
- }
464
-
465
- allCategories[categoryNode.sys.id].tags = [...new Set(allCategories[categoryNode.sys.id].tags)];
466
-
467
- return allCategories;
468
- }
469
-
470
-
471
- function DraggablePill({
472
- id, ...props
473
- }) {
474
- const {
475
- attributes,
476
- listeners,
477
- setNodeRef,
478
- transform,
479
- transition,
480
- } = useDraggable({id});
481
- const style = {
482
- transform: CSS.Translate.toString(transform),
483
- transition,
484
- };
485
-
486
- return (
487
- <div>
488
- <Pill
489
- dragHandleComponent={
490
- <DragHandle
491
- label="Reorder item"
492
- variant="transparent"
493
- {...attributes}
494
- {...listeners}
495
- />
496
- }
497
- isDraggable
498
- ref={setNodeRef}
499
- style={style}
500
- {...props}
501
- />
502
- </div>
503
- );
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
+
124
+ useEffect(() => {
125
+ setLoading(true);
126
+ const params = {
127
+ limit: 100,
128
+ skip: 0,
129
+ content_type: "tags",
130
+ locale,
131
+ order: "-sys.updatedAt",
132
+ "fields.portal": portal,
133
+ select: [
134
+ "fields.title",
135
+ "fields.isCategory",
136
+ "fields.hidden",
137
+ "sys.publishedAt",
138
+ "sys.id",
139
+ ].join(","),
140
+ };
141
+ const paramsNavigation = {
142
+ limit: 1,
143
+ content_type: "navigation",
144
+ include: 2,
145
+ locale,
146
+ "fields.portal": portal,
147
+ "fields.type": "main",
148
+ };
149
+
150
+ if (debouncedSearch) {
151
+ params["fields.title[match]"] = debouncedSearch;
152
+ }
153
+
154
+ Promise.all([
155
+ contentfulClient.getEntries(params),
156
+ contentfulClient.getEntries(paramsNavigation).then(async (response) => {
157
+ if (!response?.items?.[0]) {
158
+ return [];
159
+ }
160
+
161
+ const references = await sdk.cma.entry.references({entryId: response.items[0].sys.id}, {include: 10});
162
+ const resolved = contentfulResolveResponse(references);
163
+ const allCategories = {};
164
+
165
+ for (const category of resolved) {
166
+ if (!category?.fields?.children?.de) {
167
+ continue;
168
+ }
169
+
170
+ const children = category.fields.children.de || [];
171
+
172
+ for (const child of children) {
173
+ getAllTagsFromCategory(child, allCategories);
174
+ }
175
+ }
176
+
177
+ return Object.values(allCategories).filter((c) => c.tags.length > 0);
178
+ }),
179
+ ]).then(([tags, categories]) => {
180
+ setLoading(false);
181
+ setTags(tags.items);
182
+ setCategories(categories);
183
+ });
184
+ }, [debouncedSearch, portal, locale]);
185
+
186
+ const andTags = selectedTags.filter((t) => t.type === "and");
187
+ const orTags = selectedTags.filter((t) => t.type === "or");
188
+
189
+ return <div className="flex flex-col gap-2 p-4">
190
+ <div className="z-10 flex w-full flex-col gap-4 border-b border-gray-200 bg-white py-4">
191
+ <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
192
+ <SectionHeading style={{marginBottom: 0}}>Gewählte Zeitpunkte:</SectionHeading>
193
+ <div className="flex flex-row items-center gap-2">
194
+ {selectedTimepoints.map((timepoint) => {
195
+ return <Pill
196
+ key={timepoint}
197
+ draggable={false}
198
+ testId="pill-item"
199
+ variant="active"
200
+ label={`${timepoint} Uhr`}
201
+ onClose={() => {
202
+ setSelectedTimepoints(selectedTimepoints.filter((t) => t !== timepoint));
203
+ }}
204
+ />;
205
+ })}
206
+ <TextInput
207
+ type="number"
208
+ min={1}
209
+ max={24}
210
+ value={addTimepoint}
211
+ onChange={(e) => {
212
+ setAddTimepoint(e.target.value);
213
+ }}
214
+ />
215
+ <SectionHeading style={{marginBottom: 0}}>Uhr</SectionHeading>
216
+ <Button
217
+ variant="secondary"
218
+ onClick={() => {
219
+ if (selectedTimepoints.includes(addTimepoint)) {
220
+ return;
221
+ }
222
+
223
+ setSelectedTimepoints([...selectedTimepoints, addTimepoint]);
224
+ }}
225
+ >
226
+ <PlusIcon />
227
+ </Button>
228
+ </div>
229
+ </div>
230
+ <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
231
+ <SectionHeading style={{marginBottom: 0}}>Gewählte Kategorien & Tags:</SectionHeading>
232
+ <div className="grid w-full grid-cols-4 gap-4">
233
+ <div className="flex w-full flex-col gap-2">
234
+ <div>
235
+ <SectionHeading style={{marginBottom: 0}}>Oder</SectionHeading>
236
+ </div>
237
+ <div
238
+ ref={setOrNodeRef}
239
+ style={{
240
+ minHeight: 36,
241
+ backgroundColor: isOrOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
242
+ }}
243
+ className="flex h-full w-full flex-row flex-wrap gap-2"
244
+ >
245
+ {orTags.map((tag) => {
246
+ return <DraggablePill
247
+ key={tag.id}
248
+ id={tag.id}
249
+ variant="active"
250
+ label={(`${tag.label}`)}
251
+ onClose={() => {
252
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
253
+ }}
254
+ />;
255
+ })}
256
+ </div>
257
+ </div>
258
+ <div className="flex w-full flex-col gap-2">
259
+ <SectionHeading style={{marginBottom: 0}}>Und</SectionHeading>
260
+ <div
261
+ ref={setAndNodeRef}
262
+ style={{
263
+ minHeight: 36,
264
+ backgroundColor: isAndOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
265
+ }}
266
+ className="flex h-full w-full flex-row flex-wrap gap-2"
267
+ >
268
+ {andTags.map((tag) => {
269
+ return <DraggablePill
270
+ key={tag.id}
271
+ id={tag.id}
272
+ testId={tag.id}
273
+ variant="active"
274
+ label={(`${tag.label}`)}
275
+ onClose={() => {
276
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
277
+ }}
278
+ />;
279
+ })}
280
+ </div>
281
+ </div>
282
+ <div className="col-span-2 flex w-full flex-col gap-4">
283
+ <div>
284
+ <SectionHeading style={{marginBottom: 0}}>Artikel</SectionHeading>
285
+ </div>
286
+ <div className="flex flex-col gap-4">
287
+ <table className="w-full max-w-full table-auto gap-4 text-left">
288
+ <thead>
289
+ <tr>
290
+ <th>Datum</th>
291
+ <th className="pl-4">Titel</th>
292
+ </tr>
293
+ </thead>
294
+ <tbody>
295
+ {articles?.items?.map((article) => {
296
+ return <tr key={article.sys.id}>
297
+ <td className="whitespace-nowrap">
298
+ {new Date(article.fields.date.de).toLocaleString("de-DE", {
299
+ hour: "2-digit",
300
+ minute: "2-digit",
301
+ day: "2-digit",
302
+ month: "2-digit",
303
+ year: "numeric",
304
+ })}
305
+ </td>
306
+ <td className="truncate whitespace-nowrap pl-4">
307
+ {article.fields.title.de}
308
+ </td>
309
+ </tr>;
310
+ })}
311
+ </tbody>
312
+ </table>
313
+ </div>
314
+ </div>
315
+ </div>
316
+ </div>
317
+ <div className="flex w-full flex-row gap-2">
318
+ <TextInput
319
+ placeholder="Suche nach Kategorie oder Tag"
320
+ value={searchQuery}
321
+ onChange={(e) => setSearchQuery(e.target.value)}
322
+ />
323
+ </div>
324
+ </div>
325
+ <div className="grid grid-cols-2 gap-4 overflow-y-auto">
326
+ <div className="flex flex-col gap-4">
327
+ <div>
328
+ <SectionHeading style={{marginBottom: 0}}>Kategorien</SectionHeading>
329
+ </div>
330
+ <div className="grid grid-cols-3 gap-4">
331
+ {categories?.map((category) => {
332
+ return (
333
+ <Checkbox
334
+ key={category.id}
335
+ isChecked={selectedTags.some((t) => category.tags.some((c) => c.id === t.id))}
336
+ onChange={(e) => {
337
+ let newTags = [...selectedTags];
338
+
339
+ if (e.target.checked) {
340
+ for (const tag of category.tags) {
341
+ if (!selectedTags.find((t) => t.id === tag.id)) {
342
+ newTags.push({
343
+ label: tag.label,
344
+ id: tag.id,
345
+ type: "or",
346
+ });
347
+ }
348
+ }
349
+ } else {
350
+ for (const tag of category.tags) {
351
+ newTags = newTags.filter((t) => t.id !== tag.id);
352
+ }
353
+ }
354
+
355
+ setSelectedTags(newTags);
356
+ }}
357
+ >
358
+ {category.title} ({selectedTags.filter((t) =>
359
+ category.tags.some((c) => c.id === t.id)).length}/{category.tags.length})
360
+ </Checkbox>
361
+ );
362
+ })}
363
+ </div>
364
+ </div>
365
+ <div className="flex flex-col gap-4">
366
+ <div>
367
+ <SectionHeading style={{marginBottom: 0}}>Tags</SectionHeading>
368
+ </div>
369
+ <div className="grid grid-cols-3 gap-4">
370
+ {tags?.map((tag) => {
371
+ return (
372
+ <Checkbox
373
+ key={tag.sys.id}
374
+ isChecked={!!selectedTags.find((t) => t.id === tag.sys.id)}
375
+ onChange={(e) => {
376
+ if (e.target.checked) {
377
+ setSelectedTags([...selectedTags, {
378
+ label: tag.fields.title[locale],
379
+ id: tag.sys.id,
380
+ type: "or",
381
+ }]);
382
+ } else {
383
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.sys.id));
384
+ }
385
+ }}
386
+ >
387
+ {tag.fields.title[locale]}
388
+ </Checkbox>
389
+ );
390
+ })}
391
+ </div>
392
+ </div>
393
+ </div>
394
+ <div className="sticky bottom-0 flex w-full flex-row justify-between gap-2 border-t border-gray-200 bg-white py-4">
395
+ <Button
396
+ variant="positive"
397
+ type="button"
398
+ onClick={() => onSave({
399
+ tags: selectedTags,
400
+ timepoints: selectedTimepoints,
401
+ })}>Speichern</Button>
402
+ <Button
403
+ variant="negative"
404
+ type="button"
405
+ onClick={() => onSave({
406
+ tags: [],
407
+ timepoints: [],
408
+ })}>Logik löschen</Button>
409
+ </div>
410
+ </div>;
411
+ };
412
+
413
+ function getAllTagsFromCategory(categoryNode, allCategories = {}, parentCategoryTitle = null) {
414
+ if (!categoryNode || !categoryNode.fields?.children) {
415
+ return allCategories;
416
+ }
417
+
418
+ const children = categoryNode.fields.children.de || [];
419
+ const title = parentCategoryTitle ? `${parentCategoryTitle} > ${categoryNode.fields.title.de}` : categoryNode.fields.title.de;
420
+
421
+ allCategories[categoryNode.sys.id] = {
422
+ title,
423
+ tags: [],
424
+ id: categoryNode.sys.id,
425
+ };
426
+
427
+ if (categoryNode?.fields?.tags?.de) {
428
+ for (const tag of categoryNode.fields.tags.de) {
429
+ if (!tag?.fields?.title) {
430
+ continue;
431
+ }
432
+
433
+ if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
434
+ allCategories[categoryNode.sys.id].tags.push({
435
+ label: tag.fields.title.de,
436
+ id: tag.sys.id,
437
+ });
438
+ }
439
+ }
440
+ }
441
+
442
+ for (const child of children) {
443
+ if (!child || !child.fields) {
444
+ continue;
445
+ }
446
+
447
+ if (child?.fields?.tags?.de) {
448
+ for (const tag of child.fields.tags.de) {
449
+ if (!allCategories[categoryNode.sys.id].tags.find((c) => c.id === tag.sys.id)) {
450
+ if (!tag?.fields?.title) {
451
+ continue;
452
+ }
453
+
454
+ allCategories[categoryNode.sys.id].tags.push({
455
+ label: tag.fields.title.de,
456
+ id: tag.sys.id,
457
+ });
458
+ }
459
+ }
460
+ }
461
+
462
+ getAllTagsFromCategory(child, allCategories, title);
463
+ }
464
+
465
+ allCategories[categoryNode.sys.id].tags = [...new Set(allCategories[categoryNode.sys.id].tags)];
466
+
467
+ return allCategories;
468
+ }
469
+
470
+
471
+ function DraggablePill({
472
+ id, ...props
473
+ }) {
474
+ const {
475
+ attributes,
476
+ listeners,
477
+ setNodeRef,
478
+ transform,
479
+ transition,
480
+ } = useDraggable({id});
481
+ const style = {
482
+ transform: CSS.Translate.toString(transform),
483
+ transition,
484
+ };
485
+
486
+ return (
487
+ <div>
488
+ <Pill
489
+ dragHandleComponent={
490
+ <DragHandle
491
+ label="Reorder item"
492
+ variant="transparent"
493
+ {...attributes}
494
+ {...listeners}
495
+ />
496
+ }
497
+ isDraggable
498
+ ref={setNodeRef}
499
+ style={style}
500
+ {...props}
501
+ />
502
+ </div>
503
+ );
504
504
  }