@vonaffenfels/contentful-teasermanager 1.1.70 → 1.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vonaffenfels/contentful-teasermanager",
3
- "version": "1.1.70",
3
+ "version": "1.2.0",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "prepublish": "yarn run build",
@@ -16,7 +16,7 @@
16
16
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
17
17
  "@babel/preset-env": "^7.13.15",
18
18
  "@babel/preset-react": "^7.13.13",
19
- "@contentful/app-sdk": "^3.33.0",
19
+ "@contentful/app-sdk": "^4.29.5",
20
20
  "@contentful/f36-components": "^4.50.2",
21
21
  "@contentful/field-editor-single-line": "^0.14.1",
22
22
  "@contentful/field-editor-test-utils": "^0.11.1",
@@ -57,13 +57,14 @@
57
57
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
58
58
  "@babel/preset-env": "^7.13.15",
59
59
  "@babel/preset-react": "^7.13.13",
60
- "@contentful/app-sdk": "^3.33.0",
60
+ "@contentful/app-sdk": "^4.29.5",
61
61
  "@contentful/f36-components": "^4.50.2",
62
62
  "@contentful/field-editor-single-line": "^0.14.1",
63
63
  "@contentful/field-editor-test-utils": "^0.11.1",
64
64
  "@contentful/forma-36-fcss": "^0.3.1",
65
65
  "@contentful/forma-36-react-components": "^3.88.3",
66
66
  "@contentful/forma-36-tokens": "^0.10.1",
67
+ "@dnd-kit/core": "^6.3.1",
67
68
  "@svgr/webpack": "^5.5.0",
68
69
  "babel-loader": "^8.2.2",
69
70
  "classnames": "^2.3.2",
@@ -96,10 +97,10 @@
96
97
  "webpack-dev-server": "^4.0.0-beta.2"
97
98
  },
98
99
  "dependencies": {
99
- "@vonaffenfels/slate-editor": "^1.1.70",
100
+ "@vonaffenfels/slate-editor": "^1.2.0",
100
101
  "webpack": "5.88.2"
101
102
  },
102
- "gitHead": "6aab8bfc8222e44d13e0fa103982fc0daf0104cf",
103
+ "gitHead": "fe9b4ce59e9b4dfae924c580f3a1247c180f4d9b",
103
104
  "publishConfig": {
104
105
  "access": "public"
105
106
  }
@@ -0,0 +1,294 @@
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 {
14
+ useEffect, useState,
15
+ } from "react";
16
+ import useDebounce from "../../../hooks/useDebounce";
17
+ import {getContentfulClient} from "../../../lib/contentfulClient";
18
+ import {PlusIcon} from "@contentful/f36-icons";
19
+ import {CSS} from "@dnd-kit/utilities";
20
+
21
+ export const LogicEditor = (props) => {
22
+ const [selectedTags, setSelectedTags] = useState([]);
23
+
24
+ useEffect(() => {
25
+ setSelectedTags(props?.logicData?.tags || []);
26
+ }, [props?.logicData?.tags]);
27
+
28
+ const handleDragEnd = (event) => {
29
+ const {
30
+ over,
31
+ active,
32
+ } = event;
33
+
34
+ if (over) {
35
+ const draggedTagIndex = selectedTags.findIndex(tag => tag.id === active.id);
36
+ if (draggedTagIndex !== -1) {
37
+ const updatedTags = [...selectedTags];
38
+ updatedTags[draggedTagIndex] = {
39
+ ...updatedTags[draggedTagIndex],
40
+ type: over.id, // "and" or "or"
41
+ };
42
+ setSelectedTags(updatedTags);
43
+ }
44
+ }
45
+ };
46
+
47
+ return <DndContext onDragEnd={handleDragEnd}>
48
+ <LogicEditorInner {...props} selectedTags={selectedTags} setSelectedTags={setSelectedTags} />
49
+ </DndContext>;
50
+ };
51
+
52
+ const LogicEditorInner = ({
53
+ onSave = () => alert("onSave missing"),
54
+ sdk,
55
+ logicData = {
56
+ timepoints: [7, 12, 18],
57
+ tags: [],
58
+ },
59
+ selectedTags,
60
+ setSelectedTags,
61
+ }) => {
62
+ const contentfulClient = getContentfulClient();
63
+ const [loading, setLoading] = useState(false);
64
+ const [data, setData] = useState([]);
65
+ const [searchQuery, setSearchQuery] = useState("");
66
+ const [addTimepoint, setAddTimepoint] = useState(7);
67
+ const debouncedSearch = useDebounce(searchQuery, 500);
68
+ const [selectedTimepoints, setSelectedTimepoints] = useState([]);
69
+ useEffect(() => {
70
+ setSelectedTimepoints(logicData?.timepoints || []);
71
+ }, [logicData?.timepoints]);
72
+ const {
73
+ setNodeRef: setOrNodeRef,
74
+ isOver: isOrOver,
75
+ node: orNode,
76
+ } = useDroppable({id: 'or'});
77
+ const {
78
+ setNodeRef: setAndNodeRef,
79
+ isOver: isAndOver,
80
+ node: andNode,
81
+ } = useDroppable({id: "and"});
82
+
83
+ const {
84
+ portal,
85
+ locale = "de",
86
+ } = sdk.parameters.invocation;
87
+
88
+ useEffect(() => {
89
+ setLoading(true);
90
+ const params = {
91
+ limit: 100,
92
+ skip: 0,
93
+ content_type: "tags",
94
+ locale,
95
+ order: "-sys.updatedAt",
96
+ "fields.portal": portal,
97
+ select: [
98
+ "fields.title",
99
+ "fields.isCategory",
100
+ "fields.hidden",
101
+ "sys.publishedAt",
102
+ "sys.id",
103
+ ].join(","),
104
+ };
105
+
106
+ if (debouncedSearch) {
107
+ params["fields.title[match]"] = debouncedSearch;
108
+ }
109
+
110
+ contentfulClient.getEntries(params).then((response) => {
111
+ setLoading(false);
112
+ setData(response);
113
+ });
114
+ }, [debouncedSearch, portal, locale]);
115
+
116
+ const andTags = selectedTags.filter((t) => t.type === "and");
117
+ const orTags = selectedTags.filter((t) => t.type === "or");
118
+
119
+ return <div className="flex flex-col gap-2 p-4">
120
+ <div className="sticky top-0 z-10 flex w-full flex-col gap-4 border-b border-gray-200 bg-white py-4">
121
+ <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
122
+ <SectionHeading style={{marginBottom: 0}}>Gewählte Zeitpunkte:</SectionHeading>
123
+ <div className="flex flex-row items-center gap-2">
124
+ {selectedTimepoints.map((timepoint) => {
125
+ return <Pill
126
+ key={timepoint}
127
+ draggable={false}
128
+ testId="pill-item"
129
+ variant="active"
130
+ label={`${timepoint} Uhr`}
131
+ onClose={() => {
132
+ setSelectedTimepoints(selectedTimepoints.filter((t) => t !== timepoint));
133
+ }}
134
+ />;
135
+ })}
136
+ <TextInput
137
+ type="number"
138
+ min={1}
139
+ max={24}
140
+ value={addTimepoint}
141
+ onChange={(e) => {
142
+ setAddTimepoint(e.target.value);
143
+ }}
144
+ />
145
+ <SectionHeading style={{marginBottom: 0}}>Uhr</SectionHeading>
146
+ <Button
147
+ variant="secondary"
148
+ onClick={() => {
149
+ if (selectedTimepoints.includes(addTimepoint)) {
150
+ return;
151
+ }
152
+
153
+ setSelectedTimepoints([...selectedTimepoints, addTimepoint]);
154
+ }}
155
+ >
156
+ <PlusIcon />
157
+ </Button>
158
+ </div>
159
+ </div>
160
+ <div className="flex w-full flex-row items-center gap-2" style={{minHeight: 36}}>
161
+ <SectionHeading style={{marginBottom: 0}}>Gewählte Kategorien & Tags:</SectionHeading>
162
+ <div className="grid w-full grid-cols-2 gap-4">
163
+ <div>
164
+ <SectionHeading style={{marginBottom: 0}}>Oder</SectionHeading>
165
+ </div>
166
+ <div>
167
+ <SectionHeading style={{marginBottom: 0}}>Und</SectionHeading>
168
+ </div>
169
+ <div
170
+ ref={setOrNodeRef}
171
+ style={{
172
+ minHeight: 36,
173
+ backgroundColor: isOrOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
174
+ }}
175
+ className="flex w-full flex-row flex-wrap gap-2"
176
+ >
177
+ {orTags.map((tag) => {
178
+ return <DraggablePill
179
+ key={tag.id}
180
+ id={tag.id}
181
+ variant="active"
182
+ label={(`${tag.label}`)}
183
+ onClose={() => {
184
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
185
+ }}
186
+ />;
187
+ })}
188
+ </div>
189
+ <div
190
+ ref={setAndNodeRef}
191
+ style={{
192
+ minHeight: 36,
193
+ backgroundColor: isAndOver ? "rgba(0, 255, 0, 0.3)" : "transparent",
194
+ }}
195
+ className="flex w-full flex-row flex-wrap gap-2"
196
+ >
197
+ {andTags.map((tag) => {
198
+ return <DraggablePill
199
+ key={tag.id}
200
+ id={tag.id}
201
+ testId={tag.id}
202
+ variant="active"
203
+ label={(`${tag.label}`)}
204
+ onClose={() => {
205
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.id));
206
+ }}
207
+ />;
208
+ })}
209
+ </div>
210
+ </div>
211
+ </div>
212
+ <div className="flex w-full flex-row gap-2">
213
+ <TextInput
214
+ placeholder="Suche nach Kategorie oder Tag"
215
+ value={searchQuery}
216
+ onChange={(e) => setSearchQuery(e.target.value)}
217
+ />
218
+ </div>
219
+ </div>
220
+ <div className="grid grid-cols-3 gap-4">
221
+ {data?.items?.map((tag) => {
222
+ return (
223
+ <Checkbox
224
+ key={tag.sys.id}
225
+ isChecked={!!selectedTags.find((t) => t.id === tag.sys.id)}
226
+ onChange={(e) => {
227
+ if (e.target.checked) {
228
+ setSelectedTags([...selectedTags, {
229
+ label: tag.fields.title[locale],
230
+ id: tag.sys.id,
231
+ type: "or",
232
+ }]);
233
+ } else {
234
+ setSelectedTags(selectedTags.filter((t) => t.id !== tag.sys.id));
235
+ }
236
+ }}
237
+ >
238
+ {tag.fields.title[locale]}
239
+ </Checkbox>
240
+ );
241
+ })}
242
+ </div>
243
+ <div className="sticky bottom-0 flex w-full flex-row justify-between gap-2 border-t border-gray-200 bg-white py-4">
244
+ <Button
245
+ variant="positive"
246
+ type="button"
247
+ onClick={() => onSave({
248
+ tags: selectedTags,
249
+ timepoints: selectedTimepoints,
250
+ })}>Speichern</Button>
251
+ <Button
252
+ variant="negative"
253
+ type="button"
254
+ onClick={() => onSave({
255
+ tags: [],
256
+ timepoints: [],
257
+ })}>Logik löschen</Button>
258
+ </div>
259
+ </div>;
260
+ };
261
+
262
+
263
+ function DraggablePill({
264
+ id, ...props
265
+ }) {
266
+ const {
267
+ attributes,
268
+ listeners,
269
+ setNodeRef,
270
+ transform,
271
+ transition,
272
+ } = useDraggable({id});
273
+ const style = {
274
+ transform: CSS.Translate.toString(transform),
275
+ transition,
276
+ };
277
+
278
+ return (
279
+ <Pill
280
+ dragHandleComponent={
281
+ <DragHandle
282
+ label="Reorder item"
283
+ variant="transparent"
284
+ {...attributes}
285
+ {...listeners}
286
+ />
287
+ }
288
+ isDraggable
289
+ ref={setNodeRef}
290
+ style={style}
291
+ {...props}
292
+ />
293
+ );
294
+ }
@@ -1,7 +1,11 @@
1
- import {EntityList, Pagination, Spinner, Select, Autocomplete, TextInput} from "@contentful/f36-components";
1
+ import {
2
+ EntityList, Pagination, Spinner, Select, Autocomplete, TextInput,
3
+ } from "@contentful/f36-components";
2
4
  import {getContentfulClient} from "../../../lib/contentfulClient";
3
- import {useEffect, useState} from "react";
4
- import format from "date-fns/format"
5
+ import {
6
+ useEffect, useState,
7
+ } from "react";
8
+ import format from "date-fns/format";
5
9
  import useDebounce from "../../../hooks/useDebounce";
6
10
  import classNames from "classnames";
7
11
 
@@ -12,7 +16,10 @@ export const NewestArticles = ({
12
16
  onEntryClick = () => alert("onEntryClick missing"),
13
17
  getArticleThumbnailUrl = (article) => alert("getArticleThumbnailUrl missing"),
14
18
  }) => {
15
- const {portal, slotId} = sdk.parameters.invocation;
19
+ const {
20
+ portal,
21
+ slotId,
22
+ } = sdk.parameters.invocation;
16
23
  const contentfulClient = getContentfulClient();
17
24
  const limit = 25;
18
25
  const [searchQuery, setSearchQuery] = useState("");
@@ -20,7 +27,10 @@ export const NewestArticles = ({
20
27
  const [enabledPortals, setEnabledPortals] = useState([portal]);
21
28
  const [selectedPortal, setSelectedPortal] = useState(portal);
22
29
  const [loading, setLoading] = useState(true);
23
- const [data, setData] = useState({items: [], total: 0});
30
+ const [data, setData] = useState({
31
+ items: [],
32
+ total: 0,
33
+ });
24
34
  const [page, setPage] = useState(0);
25
35
  const [selectedAuthor, setSelectedAuthor] = useState(null);
26
36
  const [authors, setAuthors] = useState([]);
@@ -75,7 +85,7 @@ export const NewestArticles = ({
75
85
  setLoading(false);
76
86
  setData(response);
77
87
  });
78
- }, [debouncedSearch, selectedAuthor,selectedPortal, limit, page]);
88
+ }, [debouncedSearch, selectedAuthor, selectedPortal, limit, page]);
79
89
 
80
90
  useEffect(() => {
81
91
  setAuthorsLoading(true);
@@ -87,7 +97,7 @@ export const NewestArticles = ({
87
97
  };
88
98
 
89
99
  if (debouncedAuthorSearch) {
90
- params["fields.name[match]"] = debouncedAuthorSearch
100
+ params["fields.name[match]"] = debouncedAuthorSearch;
91
101
  } else {
92
102
  setSelectedAuthor(null);
93
103
  }
@@ -107,15 +117,15 @@ export const NewestArticles = ({
107
117
  }, [authors, selectedAuthor]);
108
118
 
109
119
  return (
110
- <div className="p-4 flex flex-col space-y-4">
120
+ <div className="flex flex-col space-y-4 p-4">
111
121
  <div className="flex space-x-2">
112
- <div className="flex-grow">
122
+ <div className="grow">
113
123
  <TextInput
114
124
  width={"full"}
115
125
  placeholder={"Suche nach Titel"}
116
126
  onChange={(e) => setSearchQuery(e.target.value)}/>
117
127
  </div>
118
- <div className="flex-grow-0">
128
+ <div className="grow-0">
119
129
  <Autocomplete
120
130
  isLoading={authorsLoading}
121
131
  onSelectItem={item => setSelectedAuthor(item?.sys?.id)}
@@ -129,11 +139,11 @@ export const NewestArticles = ({
129
139
  noMatchesMessage={'Kein Autor gefunden'}
130
140
  >
131
141
  {(options) => {
132
- return options.map(option => <span key={option.value}>{option.label}</span>)
142
+ return options.map(option => <span key={option.value}>{option.label}</span>);
133
143
  }}
134
144
  </Autocomplete>
135
145
  </div>
136
- <div className="flex-grow-0">
146
+ <div className="grow-0">
137
147
  <Select value={selectedPortal} onChange={e => setSelectedPortal(e.target.value)}>
138
148
  {Object.keys(portals).filter(v => enabledPortals.includes(v)).sort().map(optionValue => <Select.Option
139
149
  key={optionValue}
@@ -145,7 +155,7 @@ export const NewestArticles = ({
145
155
  {loading && <Spinner/>}
146
156
  {!loading && data.items.map((entry) => {
147
157
  const isCurrentlySelectedInOtherSlot = !! Object.keys(slotState || {}).find(slot => slotState[slot] === entry.sys.id && slot !== slotId);
148
- const isCurrentlySelectedInSlot = !!Object.keys(slotState || {}).find(slot => slotState[slot] === entry.sys.id && slot === slotId);
158
+ const isCurrentlySelectedInSlot = !!Object.keys(slotState || {}).find(slot => slotState[slot] === entry.sys.id && slot === slotId);
149
159
 
150
160
  let status = "draft";
151
161
  if (entry.sys.publishedAt) {
@@ -156,7 +166,7 @@ export const NewestArticles = ({
156
166
  if (isCurrentlySelectedInSlot) {
157
167
  description = "Aktuell ausgewählt";
158
168
  } else if (isCurrentlySelectedInOtherSlot) {
159
- description = "Aktuell in anderem Teaser ausgewählt"
169
+ description = "Aktuell in anderem Teaser ausgewählt";
160
170
  }
161
171
 
162
172
  return <div
@@ -175,7 +185,7 @@ export const NewestArticles = ({
175
185
  description={description}
176
186
  status={status}
177
187
  />
178
- </div>
188
+ </div>;
179
189
  })}
180
190
  </EntityList>
181
191
  <Pagination
@@ -186,4 +196,4 @@ export const NewestArticles = ({
186
196
  />
187
197
  </div>
188
198
  );
189
- }
199
+ };
@@ -3,6 +3,7 @@ import React, {
3
3
  } from 'react';
4
4
  import {NewestArticles} from "./Dialog/NewestArticles";
5
5
  import {NoAccess} from "../NoAccess";
6
+ import {LogicEditor} from './Dialog/LogicEditor';
6
7
 
7
8
  const Dialog = ({
8
9
  sdk,
@@ -10,9 +11,20 @@ const Dialog = ({
10
11
  getArticleThumbnailUrl,
11
12
  }) => {
12
13
  const [slotState, setSlotState] = useState({});
14
+ const [logicData, setLogicData] = useState({});
13
15
 
14
16
  const selectEntry = (entry) => {
15
- sdk.close(entry);
17
+ sdk.close({
18
+ entry,
19
+ isArticleResponse: true,
20
+ });
21
+ };
22
+
23
+ const onSave = (data) => {
24
+ sdk.close({
25
+ data,
26
+ isLogicResponse: true,
27
+ });
16
28
  };
17
29
 
18
30
  const loadSlotStateForPage = async () => {
@@ -28,30 +40,41 @@ const Dialog = ({
28
40
  console.error(response.message);
29
41
  }
30
42
 
31
- return response?.data || {};
43
+ return response || {};
32
44
  } catch (e) {
33
45
  console.error(e);
34
46
  }
35
47
  };
36
48
 
37
49
  useEffect(() => {
38
- loadSlotStateForPage().then(setSlotState);
50
+ loadSlotStateForPage().then(({
51
+ data = {},
52
+ logicData = {},
53
+ }) => {
54
+ setSlotState(data);
55
+ setLogicData(logicData[sdk.parameters.invocation.slotId]);
56
+ });
39
57
  }, [sdk.parameters.invocation]);
40
58
 
41
59
  return <div style={{
42
60
  backgroundColor: "#FFFFFF",
43
61
  minHeight: "100vh",
44
62
  }}>
45
- <NewestArticles sdk={sdk} onEntryClick={selectEntry} slotState={slotState} getArticleThumbnailUrl={getArticleThumbnailUrl} portals={portals}/>
46
- {/*
47
- <Tabs defaultTab="newest">
48
- <Tabs.List>
49
- <Tabs.Tab panelId="newest">Neuste Artikel</Tabs.Tab>
50
- </Tabs.List>
51
- <Tabs.Panel id="newest">
52
- </Tabs.Panel>
53
- </Tabs>
54
- */}
63
+ {sdk.parameters.invocation.isLogicEditor ?
64
+ <LogicEditor
65
+ sdk={sdk}
66
+ onSave={onSave}
67
+ logicData={logicData}
68
+ portals={portals}
69
+ /> :
70
+ <NewestArticles
71
+ sdk={sdk}
72
+ onEntryClick={selectEntry}
73
+ slotState={slotState}
74
+ getArticleThumbnailUrl={getArticleThumbnailUrl}
75
+ portals={portals}
76
+ />
77
+ }
55
78
  </div>;
56
79
  };
57
80
 
@@ -42,8 +42,11 @@ const Entry = ({sdk}) => {
42
42
  slotId: slotId,
43
43
  entryId: sdk.entry.getSys().id,
44
44
  },
45
- }).then(async (entry) => {
46
- if (!entry) {
45
+ }).then(async ({
46
+ entry,
47
+ isLogicResponse,
48
+ } = {}) => {
49
+ if (!entry || isLogicResponse) {
47
50
  return;
48
51
  }
49
52
 
@@ -69,12 +72,61 @@ const Entry = ({sdk}) => {
69
72
  });
70
73
  };
71
74
 
75
+ const onSlotClickLogic = (slotId, currentDate) => {
76
+ const entryId = sdk.entry.getSys().id;
77
+ return sdk.dialogs.openCurrentApp({
78
+ title: "Positionslogik bearbeiten",
79
+ width: "fullWidth",
80
+ position: "top",
81
+ allowHeightOverflow: true,
82
+ minHeight: "500px",
83
+ shouldCloseOnOverlayClick: true,
84
+ shouldCloseOnEscapePress: true,
85
+ parameters: {
86
+ portal,
87
+ slotId: slotId,
88
+ entryId: entryId,
89
+ isLogicEditor: true,
90
+ date: currentDate,
91
+ },
92
+ }).then(async ({
93
+ data: {
94
+ timepoints,
95
+ tags,
96
+ } = {},
97
+ isArticleResponse,
98
+ } = {}) => {
99
+ if (!timepoints || !tags || isArticleResponse) {
100
+ return;
101
+ }
102
+
103
+ console.log(`Selected timepoints ${timepoints.map(v => `${v} Uhr`).join(", ")} and tags ${tags.map(v => `${v.label} (${v.id})`).join(", ")} for page ${entryId} with portal ${portal} for slot ${slotId}`);
104
+
105
+ const {apiRoot} = sdk.parameters.instance;
106
+ try {
107
+ await fetch(`${apiRoot}/api/saveLogicForSlot`, {
108
+ method: "POST",
109
+ body: JSON.stringify({
110
+ "project": portal,
111
+ "slot": slotId,
112
+ "page": entryId,
113
+ "timepoints": timepoints,
114
+ "tags": tags,
115
+ }),
116
+ });
117
+ } catch (e) {
118
+ console.error(e);
119
+ alert("Fehler beim speichern!");
120
+ }
121
+ });
122
+ };
123
+
72
124
  const removeSlotData = async (slotId, currentDate) => {
73
125
  console.log(`Removing slot data for page ${sdk.entry.getSys().id} with portal ${portal} for slot ${slotId} and date ${currentDate}`);
74
126
 
75
127
  try {
76
128
  const apiRoot = sdk.parameters.instance.apiRoot;
77
- const response = await fetch(`${apiRoot}/api/saveStateForSlot`, {
129
+ await fetch(`${apiRoot}/api/saveStateForSlot`, {
78
130
  method: "POST",
79
131
  body: JSON.stringify({
80
132
  "project": portal,
@@ -100,7 +152,10 @@ const Entry = ({sdk}) => {
100
152
  console.error(response.message);
101
153
  }
102
154
 
103
- return response?.data || {};
155
+ return response || {
156
+ data: {},
157
+ logicData: {},
158
+ };
104
159
  } catch (e) {
105
160
  console.error(e);
106
161
  }
@@ -133,7 +188,9 @@ const Entry = ({sdk}) => {
133
188
  loadSlotStateForPage={loadSlotStateForPage}
134
189
  removeSlotData={removeSlotData}
135
190
  loadTimelineStateForPage={loadTimelineStateForPage}
136
- onSlotClick={onSlotClick}/>;
191
+ onSlotClick={onSlotClick}
192
+ onSlotClickLogic={onSlotClickLogic}
193
+ />;
137
194
  };
138
195
 
139
196
  export default Entry;
@@ -15,6 +15,7 @@ export const Teasermanager = ({
15
15
  loadSlotStateForPage = () => console.error("missing loadSlotStateForPage"),
16
16
  loadTimelineStateForPage = () => console.error("missing loadTimelineStateForPage"),
17
17
  onSlotClick = async () => console.error("missing onSlotClick"),
18
+ onSlotClickLogic = async () => console.error("missing onSlotClickLogic"),
18
19
  removeSlotData = async () => console.error("missing removeSlotData"),
19
20
  contentFieldName = "content",
20
21
  locale = "de",
@@ -26,6 +27,7 @@ export const Teasermanager = ({
26
27
  const [loadedContent, setLoadedContent] = useState(null);
27
28
  const [currentDate, _setCurrentDate] = useState(null);
28
29
  const [currentSlotState, setCurrentSlotState] = useState(null);
30
+ const [currentSlotLogicState, setCurrentSlotLogicState] = useState(null);
29
31
  const currentLoadingOperation = useRef(0);
30
32
 
31
33
  const setCurrentDate = (date) => {
@@ -34,7 +36,7 @@ export const Teasermanager = ({
34
36
  }
35
37
 
36
38
  _setCurrentDate(date);
37
- }
39
+ };
38
40
 
39
41
  const reloadAllData = async () => {
40
42
  let loadingId = ++currentLoadingOperation.current;
@@ -56,7 +58,9 @@ export const Teasermanager = ({
56
58
  setLoading(true);
57
59
  }
58
60
 
59
- const content = await runLoaders(contentValue, portalValue, entry?.sys?.id, currentDate);
61
+ const content = await runLoaders(
62
+ contentValue, portalValue, entry?.sys?.id, currentDate,
63
+ );
60
64
 
61
65
  // only apply the LAST update
62
66
  if (currentLoadingOperation.current === loadingId) {
@@ -66,7 +70,7 @@ export const Teasermanager = ({
66
70
  setLoadedContent(content);
67
71
  setLoading(false);
68
72
  }
69
- }
73
+ };
70
74
 
71
75
  const initSlot = (node) => {
72
76
  const existingManagementNode = node.querySelector(`.__teasermanager_management_node`);
@@ -80,14 +84,21 @@ export const Teasermanager = ({
80
84
 
81
85
  const managementLabelNode = document.createElement("div");
82
86
  managementLabelNode.classList.add(styles.label);
87
+ const managementLabelNodeLogic = document.createElement("div");
88
+ managementLabelNodeLogic.classList.add(styles.labelLogic);
89
+
83
90
  if (currentSlotState?.[slotId]) {
84
91
  managementNode.classList.add(styles.changedBorder);
85
92
  }
86
93
 
94
+ if (currentSlotLogicState?.[slotId]) {
95
+ managementNode.classList.add(styles.logicBorder);
96
+ }
97
+
87
98
  if (currentSlotState?.[slotId] && currentSlotState?.[slotId] !== "RESET") {
88
99
  managementNode.classList.add(styles.changed);
89
100
 
90
- managementLabelNode.innerHTML = `Klicken um den Artikel zu wechseln`;
101
+ managementLabelNode.innerHTML = `Artikel wechseln`;
91
102
  const managementRemoveButtonNode = document.createElement("div");
92
103
  managementRemoveButtonNode.classList.add(styles.removeButton);
93
104
  managementRemoveButtonNode.title = `Artikel entfernen`;
@@ -104,7 +115,7 @@ export const Teasermanager = ({
104
115
  managementNode.classList.add(styles.loading);
105
116
  reloadAllData().then(() => {
106
117
  // DONT remove, in case multiple loading operations are running we want the state to stay loading :)
107
- //managementNode.classList.remove(styles.loading);
118
+ // managementNode.classList.remove(styles.loading);
108
119
  });
109
120
  });
110
121
  e.preventDefault();
@@ -113,16 +124,18 @@ export const Teasermanager = ({
113
124
  });
114
125
  managementNode.appendChild(managementRemoveButtonNode);
115
126
  } else {
116
- if (currentSlotState?.[slotId] === "RESET") {
117
- managementLabelNode.innerHTML = `Hier wurde ein bereits verslotteter Artikel wieder entfernt<br />Klicken um einen neuen Artikel festzulegen`;
118
- } else {
119
- managementLabelNode.innerHTML = `Klicken um den Artikel festzulegen`;
120
- }
127
+ managementLabelNode.innerHTML = `Artikel festlegen`;
128
+ }
121
129
 
130
+ if (currentSlotLogicState?.[slotId]) {
131
+ managementLabelNodeLogic.innerHTML = `Logik bearbeiten`;
132
+ } else {
133
+ managementLabelNodeLogic.innerHTML = `Logik festlegen`;
122
134
  }
123
135
 
124
136
  managementNode.appendChild(managementLabelNode);
125
- managementNode.addEventListener("click", (e) => {
137
+ managementNode.appendChild(managementLabelNodeLogic);
138
+ managementLabelNode.addEventListener("click", (e) => {
126
139
  onSlotClick(slotId, currentDate).then((entry) => {
127
140
  if (!entry) {
128
141
  return; // nothing changed, no need to reload!
@@ -130,7 +143,7 @@ export const Teasermanager = ({
130
143
  managementNode.classList.add(styles.loading);
131
144
  reloadAllData().then(() => {
132
145
  // DONT remove, in case multiple loading operations are running we want the state to stay loading :)
133
- //managementNode.classList.remove(styles.loading);
146
+ // managementNode.classList.remove(styles.loading);
134
147
  });
135
148
  });
136
149
  e.preventDefault();
@@ -138,12 +151,25 @@ export const Teasermanager = ({
138
151
  return false;
139
152
  });
140
153
 
154
+ managementLabelNodeLogic.addEventListener("click", (e) => {
155
+ onSlotClickLogic(slotId, currentDate).then(() => {
156
+ managementNode.classList.add(styles.loading);
157
+ reloadAllData().then(() => {
158
+ // DONT remove, in case multiple loading operations are running we want the state to stay loading :)
159
+ // managementNode.classList.remove(styles.loading);
160
+ });
161
+ });
162
+ e.preventDefault();
163
+ e.stopPropagation();
164
+ return false;
165
+ });
166
+
167
+
141
168
  managementNode.classList.add("__teasermanager_management_node");
142
169
  node.appendChild(managementNode);
143
170
  };
144
171
 
145
172
  const updateSlots = (mutationList) => {
146
-
147
173
  if (!mutationList) {
148
174
  // update all slots
149
175
  const slots = wrapperRef.current.querySelectorAll(`*[data-teasermanager-slot]`);
@@ -162,13 +188,12 @@ export const Teasermanager = ({
162
188
  }
163
189
 
164
190
  return true;
165
- })
191
+ });
166
192
 
167
193
  if (isValidUpdate) {
168
194
  updateSlots();
169
195
  }
170
196
  }
171
-
172
197
  };
173
198
 
174
199
  useEffect(() => {
@@ -204,11 +229,18 @@ export const Teasermanager = ({
204
229
  if (!currentDate) {
205
230
  return;
206
231
  }
207
- setCurrentSlotState(await loadSlotStateForPage(currentDate))
232
+
233
+ const {
234
+ data,
235
+ logicData,
236
+ } = await loadSlotStateForPage(currentDate);
237
+
238
+ setCurrentSlotState(data);
239
+ setCurrentSlotLogicState(logicData);
208
240
  } catch (e) {
209
241
  console.error(e);
210
242
  }
211
- }
243
+ };
212
244
 
213
245
  useEffect(() => {
214
246
  updateSlots();
@@ -20,7 +20,7 @@
20
20
  display: flex;
21
21
  justify-items: center;
22
22
  align-items: center;
23
- padding: 2em;
23
+ padding: 1em;
24
24
  }
25
25
 
26
26
  .remove-button {
@@ -55,6 +55,7 @@
55
55
  0% {
56
56
  transform: rotate(0deg);
57
57
  }
58
+
58
59
  100% {
59
60
  transform: rotate(360deg);
60
61
  }
@@ -64,9 +65,11 @@
64
65
  from {
65
66
  opacity: 1;
66
67
  }
68
+
67
69
  50% {
68
70
  opacity: 0.3;
69
71
  }
72
+
70
73
  to {
71
74
  opacity: 1;
72
75
  }
@@ -85,14 +88,39 @@
85
88
  .label {
86
89
  visibility: hidden;
87
90
  width: 100%;
91
+ height: 100%;
92
+ font-weight: bold;
93
+ text-align: center;
94
+ align-content: center;
95
+ padding: 6px;
96
+ }
97
+
98
+ .labelLogic {
99
+ visibility: hidden;
100
+ width: 100%;
101
+ height: 100%;
88
102
  font-weight: bold;
89
103
  text-align: center;
104
+ align-content: center;
105
+ padding: 1em;
106
+ }
107
+
108
+ .labelLogic:hover {
109
+ background-color: rgba(0, 0, 0, 0.3);
110
+ }
111
+
112
+ .label:hover {
113
+ background-color: rgba(0, 0, 0, 0.3);
90
114
  }
91
115
 
92
116
  .management:hover .label {
93
117
  visibility: visible;
94
118
  }
95
119
 
120
+ .management:hover .labelLogic {
121
+ visibility: visible;
122
+ }
123
+
96
124
  .changed {
97
125
  background-color: rgba(172, 255, 156, 0.4);
98
126
  }
@@ -103,4 +131,8 @@
103
131
 
104
132
  .changed-border {
105
133
  outline-color: green;
134
+ }
135
+
136
+ .logic-border {
137
+ outline-color: orange;
106
138
  }