@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 +6 -5
- package/src/components/Contentful/Dialog/LogicEditor.js +294 -0
- package/src/components/Contentful/Dialog/NewestArticles.js +26 -16
- package/src/components/Contentful/Dialog.js +36 -13
- package/src/components/Contentful/EntryEditor.js +62 -5
- package/src/components/Teasermanager.js +49 -17
- package/src/components/Teasermanager.module.css +33 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vonaffenfels/contentful-teasermanager",
|
|
3
|
-
"version": "1.
|
|
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": "^
|
|
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": "^
|
|
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.
|
|
100
|
+
"@vonaffenfels/slate-editor": "^1.2.0",
|
|
100
101
|
"webpack": "5.88.2"
|
|
101
102
|
},
|
|
102
|
-
"gitHead": "
|
|
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 {
|
|
1
|
+
import {
|
|
2
|
+
EntityList, Pagination, Spinner, Select, Autocomplete, TextInput,
|
|
3
|
+
} from "@contentful/f36-components";
|
|
2
4
|
import {getContentfulClient} from "../../../lib/contentfulClient";
|
|
3
|
-
import {
|
|
4
|
-
|
|
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 {
|
|
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({
|
|
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="
|
|
120
|
+
<div className="flex flex-col space-y-4 p-4">
|
|
111
121
|
<div className="flex space-x-2">
|
|
112
|
-
<div className="
|
|
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="
|
|
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="
|
|
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 =
|
|
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(
|
|
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
|
|
43
|
+
return response || {};
|
|
32
44
|
} catch (e) {
|
|
33
45
|
console.error(e);
|
|
34
46
|
}
|
|
35
47
|
};
|
|
36
48
|
|
|
37
49
|
useEffect(() => {
|
|
38
|
-
loadSlotStateForPage().then(
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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 (
|
|
46
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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 = `
|
|
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
|
-
|
|
117
|
-
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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
|
}
|