slice-machine-ui 1.16.0 → 1.16.1-dev-upgrade-legacy-2.2

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.
Files changed (31) hide show
  1. package/components/Forms/ConvertLegacySliceModal/ConvertLegacySliceModal.css.ts +35 -0
  2. package/components/Forms/ConvertLegacySliceModal/ConvertLegacySliceModal.tsx +295 -0
  3. package/components/Forms/ConvertLegacySliceModal/FormAsNewSlice.tsx +153 -0
  4. package/components/Forms/ConvertLegacySliceModal/FormAsNewVariation.tsx +234 -0
  5. package/components/Forms/ConvertLegacySliceModal/FormMergeWithIdentical.tsx +104 -0
  6. package/components/Forms/ConvertLegacySliceModal/index.tsx +1 -0
  7. package/components/Forms/ConvertLegacySliceModal/types.ts +48 -0
  8. package/lib/builders/CustomTypeBuilder/SliceZone/List.tsx +12 -7
  9. package/lib/builders/CustomTypeBuilder/SliceZone/index.tsx +10 -1
  10. package/out/404.html +1 -1
  11. package/out/[lib]/[sliceName]/[variation]/screenshot.html +1 -1
  12. package/out/[lib]/[sliceName]/[variation]/simulator.html +1 -1
  13. package/out/[lib]/[sliceName]/[variation].html +1 -1
  14. package/out/_next/static/{01TcH3t7vRO19qKqSjp5t → ERajXsW0rmM1eiFC9OrRN}/_buildManifest.js +1 -1
  15. package/out/_next/static/chunks/{125-84464edfe25651eb.js → 521-7d6a8c036ebc8ed7.js} +2 -2
  16. package/out/_next/static/chunks/{590-4bd4178a5452a02c.js → 590-fa9970ac3f1d9c1e.js} +1 -1
  17. package/out/_next/static/chunks/792-642ab5cdb981cc31.js +1 -0
  18. package/out/_next/static/chunks/pages/{_app-e72fbea2a8ecb363.js → _app-da0f1b0fa49ba1ba.js} +5 -5
  19. package/out/_next/static/chunks/pages/custom-types/{[customTypeId]-1baa6af0ddf0ab6b.js → [customTypeId]-9d0629783340bfd7.js} +1 -1
  20. package/out/_next/static/chunks/pages/page-types/{[pageTypeId]-708b782db680b804.js → [pageTypeId]-48d0d43500b8cc5d.js} +1 -1
  21. package/out/changelog.html +1 -1
  22. package/out/changes.html +1 -1
  23. package/out/custom-types/[customTypeId].html +1 -1
  24. package/out/custom-types.html +1 -1
  25. package/out/index.html +1 -1
  26. package/out/page-types/[pageTypeId].html +1 -1
  27. package/out/slices.html +1 -1
  28. package/package.json +4 -4
  29. package/src/features/slices/sliceCards/NonSharedSliceViewCard.tsx +16 -4
  30. package/out/_next/static/chunks/14-110c9b65156d5536.js +0 -1
  31. /package/out/_next/static/{01TcH3t7vRO19qKqSjp5t → ERajXsW0rmM1eiFC9OrRN}/_ssgManifest.js +0 -0
@@ -0,0 +1,35 @@
1
+ import { sprinkles } from "@prismicio/editor-ui";
2
+ import { style } from "@vanilla-extract/css";
3
+
4
+ export const form = style([
5
+ sprinkles({
6
+ display: "flex",
7
+ flexDirection: "column",
8
+ }),
9
+ ]);
10
+
11
+ export const scrollArea = style([
12
+ sprinkles({
13
+ height: "100%",
14
+ display: "flex",
15
+ gap: 8,
16
+ paddingInline: 16,
17
+ paddingBlock: 16,
18
+ }),
19
+ ]);
20
+
21
+ export const field = style([
22
+ sprinkles({
23
+ display: "flex",
24
+ flexDirection: "column",
25
+ gap: 4,
26
+ }),
27
+ ]);
28
+
29
+ export const label = style([
30
+ sprinkles({
31
+ display: "inline-flex",
32
+ alignItems: "center",
33
+ gap: 4,
34
+ }),
35
+ ]);
@@ -0,0 +1,295 @@
1
+ import { useMemo, useState } from "react";
2
+ import { useSelector } from "react-redux";
3
+ import {
4
+ DropdownMenu,
5
+ DropdownMenuContent,
6
+ DropdownMenuItem,
7
+ DropdownMenuTrigger,
8
+ Icon,
9
+ Button,
10
+ } from "@prismicio/editor-ui";
11
+ import {
12
+ LegacySlice,
13
+ CompositeSlice,
14
+ } from "@prismicio/types-internal/lib/customtypes";
15
+
16
+ import { VariationSM } from "@models/common/Slice";
17
+ import { CustomTypes } from "@models/common/CustomType";
18
+ import { getLibraries } from "@src/modules/slices";
19
+ import { SliceMachineStoreType } from "@src/redux/type";
20
+ import { managerClient } from "@src/managerClient";
21
+ import { getState, telemetry } from "@src/apiClient";
22
+ import useSliceMachineActions from "@src/modules/useSliceMachineActions";
23
+ import { ToasterType } from "@src/modules/toaster";
24
+
25
+ import { FormAsNewSlice } from "./FormAsNewSlice";
26
+ import { FormAsNewVariation } from "./FormAsNewVariation";
27
+ import { FormMergeWithIdentical } from "./FormMergeWithIdentical";
28
+ import {
29
+ ConvertLegacySliceModalProps,
30
+ ConvertLegacySliceAndTrackArgs,
31
+ IdenticalSlice,
32
+ Type,
33
+ } from "./types";
34
+
35
+ const getFieldMappingFingerprint = (
36
+ slice: LegacySlice | CompositeSlice | VariationSM,
37
+ sliceName: string
38
+ ): {
39
+ primary: string;
40
+ items: string;
41
+ } => {
42
+ const primary: Record<string, string> = {};
43
+ const items: Record<string, string> = {};
44
+
45
+ if ("type" in slice) {
46
+ if (slice.type === "Slice") {
47
+ for (const key in slice["non-repeat"]) {
48
+ primary[key] = slice["non-repeat"][key].type;
49
+ }
50
+ for (const key in slice.repeat) {
51
+ items[key] = slice.repeat[key].type;
52
+ }
53
+ } else if (slice.type === "Group") {
54
+ for (const key in slice.config?.fields) {
55
+ items[key] = slice.config?.fields[key].type ?? "";
56
+ }
57
+ } else {
58
+ primary[sliceName] = slice.type;
59
+ }
60
+ } else if ("id" in slice) {
61
+ for (const { key, value } of slice.primary ?? []) {
62
+ primary[key] = value.type;
63
+ }
64
+ for (const { key, value } of slice.items ?? []) {
65
+ items[key] = value.type;
66
+ }
67
+ }
68
+
69
+ return {
70
+ primary: JSON.stringify(
71
+ Object.keys(primary)
72
+ .sort()
73
+ .map((key) => [key, primary[key]])
74
+ ),
75
+ items: JSON.stringify(
76
+ Object.keys(items)
77
+ .sort()
78
+ .map((key) => [key, items[key]])
79
+ ),
80
+ };
81
+ };
82
+
83
+ export const ConvertLegacySliceModal: React.FC<
84
+ ConvertLegacySliceModalProps
85
+ > = ({ slice, path }) => {
86
+ const {
87
+ refreshState,
88
+ openToaster,
89
+ initCustomTypeStore,
90
+ saveCustomTypeSuccess,
91
+ } = useSliceMachineActions();
92
+
93
+ const [isLoading, setIsLoading] = useState(false);
94
+ const [isModalOpen, setIsModalOpen] = useState<false | Type>(false);
95
+
96
+ const { libraries: allLibraries } = useSelector(
97
+ (store: SliceMachineStoreType) => ({
98
+ libraries: getLibraries(store),
99
+ })
100
+ );
101
+
102
+ const {} = useSliceMachineActions();
103
+
104
+ const sliceName = useMemo(() => {
105
+ return slice.value.type === "Slice"
106
+ ? slice.value.fieldset ?? slice.key
107
+ : slice.key;
108
+ }, [slice]);
109
+ const libraries = useMemo(() => {
110
+ return allLibraries.filter((library) => library.isLocal);
111
+ }, [allLibraries]);
112
+ const localSharedSlices = useMemo(() => {
113
+ return libraries.map((library) => library.components).flat();
114
+ }, [libraries]);
115
+ const identicalSlices = useMemo<IdenticalSlice[]>(() => {
116
+ const results: IdenticalSlice[] = [];
117
+
118
+ const sliceFields = getFieldMappingFingerprint(slice.value, sliceName);
119
+
120
+ for (const sharedSlice of localSharedSlices) {
121
+ for (const variation of sharedSlice.model.variations) {
122
+ const variationFields = getFieldMappingFingerprint(
123
+ variation,
124
+ sharedSlice.model.name
125
+ );
126
+
127
+ if (
128
+ sliceFields.primary === variationFields.primary &&
129
+ sliceFields.items === variationFields.items
130
+ ) {
131
+ results.push({
132
+ libraryID: sharedSlice.from,
133
+ sliceID: sharedSlice.model.id,
134
+ variationID: variation.id,
135
+ path: `${sharedSlice.from}::${sharedSlice.model.id}::${variation.id}`,
136
+ });
137
+ }
138
+ }
139
+ }
140
+
141
+ return results;
142
+ }, [slice, sliceName, localSharedSlices]);
143
+
144
+ const convertLegacySliceAndTrack = (args: ConvertLegacySliceAndTrackArgs) => {
145
+ if (isModalOpen === false) {
146
+ return;
147
+ }
148
+
149
+ setIsLoading(true);
150
+ void (async () => {
151
+ void telemetry.track({
152
+ event: "legacy-slice:converted",
153
+ id: args.sliceID,
154
+ variation: args.variationID ?? "default",
155
+ library: args.libraryID,
156
+ conversionType: isModalOpen,
157
+ });
158
+
159
+ const { errors } =
160
+ await managerClient.slices.convertLegacySliceToSharedSlice({
161
+ model: slice.value,
162
+ src: {
163
+ ...path,
164
+ sliceID: slice.key,
165
+ },
166
+ dest: {
167
+ libraryID: args.libraryID,
168
+ sliceID: args.sliceID,
169
+ variationName: args.variationName ?? "Default",
170
+ variationID: args.variationID ?? "default",
171
+ },
172
+ });
173
+
174
+ if (errors.length) {
175
+ console.error(
176
+ `Errors happened when converting slice \`${sliceName}\``,
177
+ errors
178
+ );
179
+ }
180
+
181
+ const { model: customType, errors: customTypeReadErrors } =
182
+ await managerClient.customTypes.readCustomType({
183
+ id: path.customTypeID,
184
+ });
185
+
186
+ if (customTypeReadErrors.length || !customType) {
187
+ console.error(
188
+ `Errors happened when converting slice \`${sliceName}\``,
189
+ customTypeReadErrors
190
+ );
191
+
192
+ throw customTypeReadErrors;
193
+ }
194
+
195
+ // TODO(DT-1453): Remove the need of the global getState
196
+ const serverState = await getState();
197
+ // Update Redux store
198
+ refreshState(serverState);
199
+
200
+ setIsLoading(false);
201
+ setIsModalOpen(false);
202
+ switch (isModalOpen) {
203
+ case "as_new_slice":
204
+ openToaster(
205
+ `${sliceName} has been upgraded to a new slice ${args.libraryID} > ${args.sliceID}`,
206
+ ToasterType.SUCCESS
207
+ );
208
+ break;
209
+
210
+ case "as_new_variation":
211
+ openToaster(
212
+ `${sliceName} has been converted as a variation of ${args.libraryID} > ${args.sliceID}`,
213
+ ToasterType.SUCCESS
214
+ );
215
+ break;
216
+
217
+ case "merge_with_identical":
218
+ default:
219
+ openToaster(
220
+ `${sliceName} has been merged with ${args.libraryID} > ${args.sliceID}`,
221
+ ToasterType.SUCCESS
222
+ );
223
+ break;
224
+ }
225
+
226
+ const customTypeSM = CustomTypes.toSM(customType);
227
+ initCustomTypeStore(customTypeSM, customTypeSM);
228
+ saveCustomTypeSuccess(customType);
229
+ })();
230
+ };
231
+
232
+ const formProps = {
233
+ path,
234
+ slice,
235
+ sliceName,
236
+ libraries,
237
+ localSharedSlices,
238
+ identicalSlices,
239
+ close: () => setIsModalOpen(false),
240
+ onSubmit: convertLegacySliceAndTrack,
241
+ isLoading,
242
+ };
243
+
244
+ return (
245
+ <>
246
+ <DropdownMenu>
247
+ <DropdownMenuTrigger>
248
+ <Button
249
+ data-cy="convert-legacy-slice"
250
+ startIcon="refresh"
251
+ endIcon="arrowDropDown"
252
+ size="medium"
253
+ variant="secondary"
254
+ >
255
+ Migrate legacy slice
256
+ </Button>
257
+ </DropdownMenuTrigger>
258
+ <DropdownMenuContent align="start">
259
+ <DropdownMenuItem
260
+ startIcon={<Icon name="folder" size="large" />}
261
+ description="Use it with new types"
262
+ onSelect={() => setIsModalOpen("as_new_slice")}
263
+ >
264
+ Upgrade slice
265
+ </DropdownMenuItem>
266
+ <DropdownMenuItem
267
+ startIcon={<Icon name="viewDay" size="large" />}
268
+ description="Add it to another slice"
269
+ onSelect={() => setIsModalOpen("as_new_variation")}
270
+ disabled={!localSharedSlices.length}
271
+ >
272
+ Convert to slice variation
273
+ </DropdownMenuItem>
274
+ <DropdownMenuItem
275
+ startIcon={<Icon name="driveFileMove" size="large" />}
276
+ description="Combine identical slices"
277
+ onSelect={() => setIsModalOpen("merge_with_identical")}
278
+ disabled={!identicalSlices.length}
279
+ >
280
+ Merge with another slice
281
+ </DropdownMenuItem>
282
+ </DropdownMenuContent>
283
+ </DropdownMenu>
284
+ <FormAsNewSlice {...formProps} isOpen={isModalOpen === "as_new_slice"} />
285
+ <FormAsNewVariation
286
+ {...formProps}
287
+ isOpen={isModalOpen === "as_new_variation"}
288
+ />
289
+ <FormMergeWithIdentical
290
+ {...formProps}
291
+ isOpen={isModalOpen === "merge_with_identical"}
292
+ />
293
+ </>
294
+ );
295
+ };
@@ -0,0 +1,153 @@
1
+ import { useSelector } from "react-redux";
2
+ import { Formik } from "formik";
3
+ import {
4
+ Dialog,
5
+ DialogActions,
6
+ DialogContent,
7
+ DialogHeader,
8
+ ScrollArea,
9
+ FormInput,
10
+ Text,
11
+ Select,
12
+ SelectItem,
13
+ } from "@prismicio/editor-ui";
14
+
15
+ import { SliceMachineStoreType } from "@src/redux/type";
16
+ import { getRemoteSlices } from "@src/modules/slices";
17
+ import { pascalize } from "@lib/utils/str";
18
+
19
+ import { validateSliceModalValues as validateAsNewSliceValues } from "../formsValidator";
20
+ import * as styles from "./ConvertLegacySliceModal.css";
21
+ import { FormProps } from "./types";
22
+
23
+ export const FormAsNewSlice: React.FC<FormProps> = ({
24
+ isOpen,
25
+ close,
26
+ onSubmit,
27
+ isLoading,
28
+ slice,
29
+ libraries,
30
+ }) => {
31
+ const { remoteSlices } = useSelector((store: SliceMachineStoreType) => ({
32
+ remoteSlices: getRemoteSlices(store),
33
+ }));
34
+
35
+ return (
36
+ <Dialog
37
+ open={isOpen}
38
+ onOpenChange={(open) => !open && close()}
39
+ size={{ width: 448, height: "auto" }}
40
+ >
41
+ <DialogHeader title="Upgrade slice" />
42
+ <DialogContent>
43
+ <Formik
44
+ validateOnChange
45
+ initialValues={{
46
+ _init: false,
47
+ from: libraries[0]?.name,
48
+ sliceName: pascalize(slice.key),
49
+ }}
50
+ validate={(values) => {
51
+ return validateAsNewSliceValues(values, libraries, remoteSlices);
52
+ }}
53
+ onSubmit={(values) => {
54
+ onSubmit({ libraryID: values.from, sliceID: values.sliceName });
55
+ }}
56
+ >
57
+ {(formik) => {
58
+ // Just a hack to trigger validation on init for better UX
59
+ if (!formik.values._init) {
60
+ void formik.setFieldValue("_init", true);
61
+ }
62
+
63
+ return (
64
+ <form
65
+ id="convert-legacy-slice-modal-as-new-slice"
66
+ data-cy="convert-legacy-slice-modal-as-new-slice"
67
+ className={styles.form}
68
+ >
69
+ <ScrollArea className={styles.scrollArea}>
70
+ <Text variant="normal" color="grey11">
71
+ This will create a new slice with the same fields. The new
72
+ slice will replace the legacy slice in all of your types,
73
+ and the existing slice content will be re-mapped to the new
74
+ slice.
75
+ </Text>
76
+ <Text variant="normal" color="grey11">
77
+ This will not migrate your component. You will need to do
78
+ that manually.
79
+ </Text>
80
+ <div className={styles.field}>
81
+ <label className={styles.label}>
82
+ <Text variant="bold">Display name*</Text>
83
+ {typeof formik.errors.sliceName === "string" ? (
84
+ <Text variant="small" color="tomato10">
85
+ {formik.errors.sliceName}
86
+ </Text>
87
+ ) : null}
88
+ </label>
89
+ <FormInput
90
+ placeholder={`Pascalised Slice API ID (e.g. ${pascalize(
91
+ slice.key
92
+ )})`}
93
+ error={typeof formik.errors.sliceName === "string"}
94
+ value={formik.values.sliceName}
95
+ onValueChange={(value) =>
96
+ void formik.setFieldValue(
97
+ "sliceName",
98
+ value.slice(0, 30)
99
+ )
100
+ }
101
+ data-cy="slice-name-input"
102
+ />
103
+ <Text variant="normal" color="grey11">
104
+ A display name for the slice
105
+ </Text>
106
+ </div>
107
+ <div className={styles.field}>
108
+ <label className={styles.label}>
109
+ <Text variant="bold">Slice library*</Text>
110
+ {typeof formik.errors.from === "string" ? (
111
+ <Text variant="small" color="tomato10">
112
+ {formik.errors.from}
113
+ </Text>
114
+ ) : null}
115
+ </label>
116
+ <Select
117
+ size="medium"
118
+ variant="secondary"
119
+ startIcon="folder"
120
+ flexContent={true}
121
+ value={formik.values.from}
122
+ onValueChange={(value) =>
123
+ value ? void formik.setFieldValue("from", value) : null
124
+ }
125
+ >
126
+ {libraries.map((library) => (
127
+ <SelectItem key={library.name} value={library.name}>
128
+ {library.name}
129
+ </SelectItem>
130
+ ))}
131
+ </Select>
132
+ <Text variant="normal" color="grey11">
133
+ The library where we'll store your slice
134
+ </Text>
135
+ </div>
136
+ </ScrollArea>
137
+ <DialogActions
138
+ ok={{
139
+ text: "Upgrade",
140
+ onClick: () => void formik.submitForm(),
141
+ loading: isLoading,
142
+ disabled: !formik.isValid,
143
+ }}
144
+ cancel={{ text: "Cancel" }}
145
+ />
146
+ </form>
147
+ );
148
+ }}
149
+ </Formik>
150
+ </DialogContent>
151
+ </Dialog>
152
+ );
153
+ };