feat(FN-800): auto-generate model preset IDs and remove manual ID editing
- Add slugify and generatePresetId utilities with collision handling for unique preset IDs - Remove manual ID input field from preset editor UI in SettingsModal - Auto-generate preset IDs server-side from name on creation - Simplify Header tests by removing ID-related test cases - Deduplicate tablet header controls tests by importing from shared Header tests - Remove unused CSS rules for removed ID input field - Update reviewer to pass through preset ID generation logic
This commit is contained in:
@@ -3,6 +3,7 @@ import type { ModelPreset } from "@fusion/core";
|
||||
import {
|
||||
applyPresetToSelection,
|
||||
generatePresetId,
|
||||
generateUniquePresetId,
|
||||
getPresetByName,
|
||||
getRecommendedPresetForSize,
|
||||
validatePresetId,
|
||||
@@ -70,4 +71,50 @@ describe("modelPresets utils", () => {
|
||||
expect(generatePresetId("!!!")).toBe("preset");
|
||||
expect(generatePresetId("a".repeat(40))).toBe("a".repeat(32));
|
||||
});
|
||||
|
||||
describe("generateUniquePresetId", () => {
|
||||
it("returns the base slug when no collision", () => {
|
||||
// "standard" is not in the presets fixture
|
||||
expect(generateUniquePresetId("Standard", presets)).toBe("standard");
|
||||
});
|
||||
|
||||
it("returns base slug when existing list is empty", () => {
|
||||
expect(generateUniquePresetId("Budget", [])).toBe("budget");
|
||||
});
|
||||
|
||||
it("appends suffix when base slug is already taken", () => {
|
||||
// "budget" is already used in presets, so should get "budget-1"
|
||||
expect(generateUniquePresetId("Budget", presets)).toBe("budget-1");
|
||||
// "complex" is also taken, so should get "complex-1"
|
||||
expect(generateUniquePresetId("Complex", presets)).toBe("complex-1");
|
||||
});
|
||||
|
||||
it("increments suffix until finding a free id", () => {
|
||||
const crowded: ModelPreset[] = [
|
||||
{ id: "budget", name: "Budget" },
|
||||
{ id: "budget-1", name: "Budget Copy" },
|
||||
{ id: "budget-2", name: "Budget Copy 2" },
|
||||
];
|
||||
expect(generateUniquePresetId("Budget", crowded)).toBe("budget-3");
|
||||
});
|
||||
|
||||
it("truncates base slug to leave room for suffix", () => {
|
||||
const longName = "a".repeat(40);
|
||||
const existing: ModelPreset[] = [
|
||||
{ id: generatePresetId(longName), name: longName },
|
||||
];
|
||||
const result = generateUniquePresetId(longName, existing);
|
||||
// baseId is 32 a's, collision → truncate to 28 a's + "-1" = 30 chars
|
||||
expect(result).toBe(`${"a".repeat(28)}-1`);
|
||||
expect(result.length).toBeLessThanOrEqual(32);
|
||||
expect(validatePresetId(result)).toBe(true);
|
||||
});
|
||||
|
||||
it("handles fallback 'preset' slug collisions", () => {
|
||||
const existing: ModelPreset[] = [
|
||||
{ id: "preset", name: "!!!" },
|
||||
];
|
||||
expect(generateUniquePresetId("!!!", existing)).toBe("preset-1");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -45,3 +45,26 @@ export function generatePresetId(name: string): string {
|
||||
|
||||
return slug || "preset";
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique preset ID derived from the preset name, avoiding collisions
|
||||
* with existing preset IDs. If the base slug is already taken, appends `-1`,
|
||||
* `-2`, etc. until a unique ID is found.
|
||||
*/
|
||||
export function generateUniquePresetId(name: string, existingPresets: ModelPreset[]): string {
|
||||
const baseId = generatePresetId(name);
|
||||
const takenIds = new Set(existingPresets.map((p) => p.id));
|
||||
|
||||
if (!takenIds.has(baseId)) return baseId;
|
||||
|
||||
// Leave room for the numeric suffix (-N)
|
||||
const maxBase = 30;
|
||||
let candidate = baseId;
|
||||
let idx = 1;
|
||||
while (takenIds.has(candidate) && idx < 100) {
|
||||
const suffix = `-${idx}`;
|
||||
candidate = `${baseId.slice(0, maxBase - suffix.length)}${suffix}`;
|
||||
idx++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user