Skip to content

Commit 7142dff

Browse files
authored
Merge pull request #1597 from prismicio/dani/conflict
ensure final ids/names in ui while creating slices from images
2 parents f6e03c0 + a41eb29 commit 7142dff

2 files changed

Lines changed: 112 additions & 87 deletions

File tree

packages/manager/src/managers/slices/SlicesManager.ts

Lines changed: 0 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -344,70 +344,6 @@ export class SlicesManager extends BaseManager {
344344
};
345345
}
346346

347-
async addSlices(args: {
348-
library: string;
349-
models: SharedSlice[];
350-
}): Promise<SharedSlice[]> {
351-
const { library, models } = args;
352-
353-
/**
354-
* Fix ids and names - names are compared case-insensitively to avoid
355-
* conflicts between folder names with different casing
356-
*/
357-
const existingSlices = await this.readAllSlices();
358-
if (existingSlices.errors.length > 0) {
359-
throw new Error("Failed to read existing slices.");
360-
}
361-
const [existingIds, existingNames] = existingSlices.models.reduce<
362-
[Set<string>, Set<string>]
363-
>(
364-
([ids, names], { model }) => {
365-
ids.add(model.id);
366-
names.add(model.name.toLowerCase());
367-
368-
return [ids, names];
369-
},
370-
[new Set(), new Set()],
371-
);
372-
const updatedModels = models.map((model) => {
373-
// fix id
374-
let id = model.id;
375-
let counter = 2;
376-
while (existingIds.has(id)) {
377-
id = `${model.id}${counter}`;
378-
counter++;
379-
}
380-
existingIds.add(id);
381-
382-
// fix name
383-
let name = model.name;
384-
counter = 2;
385-
while (existingNames.has(name.toLowerCase())) {
386-
name = `${model.name}${counter}`;
387-
counter++;
388-
}
389-
existingNames.add(name.toLowerCase());
390-
391-
return {
392-
...model,
393-
id,
394-
name,
395-
};
396-
});
397-
398-
for (const model of updatedModels) {
399-
const { errors } = await this.createSlice({
400-
libraryID: library,
401-
model,
402-
});
403-
if (errors.length) {
404-
throw new Error(`Failed to create slice ${model.id}.`);
405-
}
406-
}
407-
408-
return updatedModels;
409-
}
410-
411347
async readSlice(
412348
args: SliceReadHookData,
413349
): Promise<SliceMachineManagerReadSliceReturnType> {

packages/slice-machine/src/features/customTypes/customTypesBuilder/CreateSliceFromImageModal/CreateSliceFromImageModal.tsx

Lines changed: 112 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
ScrollArea,
1818
} from "@prismicio/editor-ui";
1919
import { SharedSlice } from "@prismicio/types-internal/lib/customtypes";
20-
import { useRef, useState } from "react";
20+
import { useEffect, useRef, useState } from "react";
2121
import { toast } from "react-toastify";
2222

2323
import { getState, telemetry } from "@/apiClient";
@@ -70,8 +70,8 @@ export function CreateSliceFromImageModal(
7070

7171
const onOpenChange = (open: boolean) => {
7272
if (open || isCreatingSlices) return;
73-
id.current = crypto.randomUUID();
7473
onClose();
74+
id.current = crypto.randomUUID();
7575
setSlices([]);
7676
};
7777

@@ -124,6 +124,8 @@ export function CreateSliceFromImageModal(
124124
);
125125
};
126126

127+
const existingSlices = useExistingSlices({ open });
128+
127129
const inferSlice = (args: { index: number; imageUrl: string }) => {
128130
const { index, imageUrl } = args;
129131
const currentId = id.current;
@@ -140,16 +142,24 @@ export function CreateSliceFromImageModal(
140142
managerClient.customTypes.inferSlice({ imageUrl }).then(
141143
({ slice, langSmithUrl }) => {
142144
if (currentId !== id.current) return;
143-
setSlice({
144-
index,
145-
slice: (prevSlice) => ({
146-
...prevSlice,
147-
status: "success",
148-
thumbnailUrl: imageUrl,
149-
model: slice,
150-
langSmithUrl,
151-
}),
152-
});
145+
146+
setSlices((prevSlices) =>
147+
prevSlices.map((prevSlice, i) =>
148+
i === index
149+
? {
150+
...prevSlice,
151+
status: "success",
152+
thumbnailUrl: imageUrl,
153+
model: sliceWithoutConflicts({
154+
existingSlices: existingSlices.current,
155+
newSlices: prevSlices,
156+
slice,
157+
}),
158+
langSmithUrl,
159+
}
160+
: prevSlice,
161+
),
162+
);
153163
},
154164
() => {
155165
if (currentId !== id.current) return;
@@ -178,15 +188,19 @@ export function CreateSliceFromImageModal(
178188
addSlices(newSlices)
179189
.then(async ({ slices, library }) => {
180190
if (currentId !== id.current) return;
181-
id.current = crypto.randomUUID();
191+
182192
const serverState = await getState();
183193
createSliceSuccess(serverState.libraries);
184-
onSuccess({ slices, library });
185-
void completeStep("createSlice");
186194
syncChanges();
195+
196+
onSuccess({ slices, library });
197+
187198
setIsCreatingSlices(false);
199+
id.current = crypto.randomUUID();
188200
setSlices([]);
189201

202+
void completeStep("createSlice");
203+
190204
for (const { model, langSmithUrl } of slices) {
191205
void telemetry.track({
192206
event: "slice:created",
@@ -337,6 +351,77 @@ type NewSlice = {
337351
langSmithUrl?: string;
338352
};
339353

354+
/**
355+
* Keeps track of the existing slices in the project.
356+
* Re-fetches them when the modal is opened.
357+
*/
358+
function useExistingSlices({ open }: { open: boolean }) {
359+
const ref = useRef<SharedSlice[]>([]);
360+
361+
useEffect(() => {
362+
if (!open) return;
363+
364+
ref.current = [];
365+
managerClient.slices
366+
.readAllSlices()
367+
.then((slices) => {
368+
ref.current = slices.models.map(({ model }) => model);
369+
})
370+
.catch(() => null);
371+
}, [open]);
372+
373+
return ref;
374+
}
375+
376+
/**
377+
* If needed, assigns new ids and names to avoid conflicts with existing slices.
378+
* Names are compared case-insensitively to avoid conflicts
379+
* between folder names with different casing.
380+
*/
381+
function sliceWithoutConflicts({
382+
existingSlices,
383+
newSlices,
384+
slice,
385+
}: {
386+
existingSlices: SharedSlice[];
387+
newSlices: Slice[];
388+
slice: SharedSlice;
389+
}): SharedSlice {
390+
const existingIds = new Set<string>();
391+
const existingNames = new Set<string>();
392+
393+
for (const { id, name } of existingSlices) {
394+
existingIds.add(id);
395+
existingNames.add(name.toLowerCase());
396+
}
397+
398+
for (const slice of newSlices) {
399+
if (slice.status !== "success") continue;
400+
existingIds.add(slice.model.id);
401+
existingNames.add(slice.model.name.toLowerCase());
402+
}
403+
404+
let id = slice.id;
405+
let counter = 2;
406+
while (existingIds.has(id)) {
407+
id = `${slice.id}_${counter}`;
408+
counter++;
409+
}
410+
411+
let name = slice.name;
412+
counter = 2;
413+
while (existingNames.has(name.toLowerCase())) {
414+
name = `${slice.name}${counter}`;
415+
counter++;
416+
}
417+
418+
return {
419+
...slice,
420+
id,
421+
name,
422+
};
423+
}
424+
340425
async function addSlices(newSlices: NewSlice[]) {
341426
// use the first library
342427
const { libraries = [] } =
@@ -346,24 +431,28 @@ async function addSlices(newSlices: NewSlice[]) {
346431
throw new Error("No library found in the config.");
347432
}
348433

349-
// add the slices computing new ids/names if needed
350-
const models = await managerClient.slices.addSlices({
351-
library,
352-
models: newSlices.map((slice) => slice.model),
353-
});
434+
for (const { model } of newSlices) {
435+
const { errors } = await managerClient.slices.createSlice({
436+
libraryID: library,
437+
model,
438+
});
439+
if (errors.length) {
440+
throw new Error(`Failed to create slice ${model.id}.`);
441+
}
442+
}
354443

355444
// for each added slice, set the variation screenshot
356445
const slices = await Promise.all(
357-
models.map(async (model, index) => {
446+
newSlices.map(async ({ model, image, langSmithUrl }) => {
358447
await managerClient.slices.updateSliceScreenshot({
359448
libraryID: library,
360449
sliceID: model.id,
361450
variationID: model.variations[0].id,
362-
data: newSlices[index].image,
451+
data: image,
363452
});
364453
return {
365454
model,
366-
langSmithUrl: newSlices[index].langSmithUrl,
455+
langSmithUrl,
367456
};
368457
}),
369458
);

0 commit comments

Comments
 (0)