feat: editable unit/group IDs with full cascade
- Unit ID rename: updates group.baseUnitId refs + {old-id:*} in all descriptions
- Group ID rename: updates unit.group refs + {old-id:*} in all descriptions
- Admin form ID fields unlocked; hint text updated
- renameIdInDescription() shared helper in both PUT handlers
This commit is contained in:
@@ -24,7 +24,9 @@
|
|||||||
"labelPlural": "barrels",
|
"labelPlural": "barrels",
|
||||||
"symbol": "bbl",
|
"symbol": "bbl",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 19
|
"toBase": 13.319,
|
||||||
|
"description": "Weight is based on pure water at 4 degrees celsius.",
|
||||||
|
"hidden": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hogshead",
|
"id": "hogshead",
|
||||||
@@ -101,6 +103,14 @@
|
|||||||
"group": "another-group",
|
"group": "another-group",
|
||||||
"toBase": 1,
|
"toBase": 1,
|
||||||
"hidden": true
|
"hidden": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "us-fluid-bbl",
|
||||||
|
"label": "US fluid barrel",
|
||||||
|
"labelPlural": "US fluid barrels",
|
||||||
|
"symbol": "us fluid bbl",
|
||||||
|
"group": "dickloads",
|
||||||
|
"toBase": 9.9894438
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"groups": [
|
"groups": [
|
||||||
|
|||||||
@@ -918,8 +918,8 @@
|
|||||||
<div onkeydown={onUnitFormKeydown}>
|
<div onkeydown={onUnitFormKeydown}>
|
||||||
<h3>{unitFormNew ? 'New Unit' : 'Edit Unit'}</h3>
|
<h3>{unitFormNew ? 'New Unit' : 'Edit Unit'}</h3>
|
||||||
<label>
|
<label>
|
||||||
ID {#if !unitFormNew}<small class="muted">(readonly)</small>{/if}
|
ID {#if !unitFormNew}<small class="muted">(changing will update all references)</small>{/if}
|
||||||
<input type="text" bind:value={unitForm.id} readonly={!unitFormNew} placeholder="e.g. my-unit"
|
<input type="text" bind:value={unitForm.id} placeholder="e.g. my-unit"
|
||||||
class:field-error={unitFieldError === 'unit-id'}
|
class:field-error={unitFieldError === 'unit-id'}
|
||||||
oninput={() => unitFormNew && onUnitIdInput()} />
|
oninput={() => unitFormNew && onUnitIdInput()} />
|
||||||
</label>
|
</label>
|
||||||
@@ -977,8 +977,8 @@
|
|||||||
<div onkeydown={onGroupFormKeydown}>
|
<div onkeydown={onGroupFormKeydown}>
|
||||||
<h3>{groupFormNew ? 'New Group' : 'Edit Group'}</h3>
|
<h3>{groupFormNew ? 'New Group' : 'Edit Group'}</h3>
|
||||||
<label>
|
<label>
|
||||||
ID {#if !groupFormNew}<small class="muted">(readonly)</small>{/if}
|
ID {#if !groupFormNew}<small class="muted">(changing will update all references)</small>{/if}
|
||||||
<input type="text" bind:value={groupForm.id} readonly={!groupFormNew} placeholder="e.g. my-group"
|
<input type="text" bind:value={groupForm.id} placeholder="e.g. my-group"
|
||||||
class:field-error={groupFieldError === 'group-id'}
|
class:field-error={groupFieldError === 'group-id'}
|
||||||
oninput={() => groupFormNew && onGroupIdInput()} />
|
oninput={() => groupFormNew && onGroupIdInput()} />
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import { loadData, saveData } from '$lib/server/data';
|
import { loadData, saveData } from '$lib/server/data';
|
||||||
import { authRequest } from '$lib/server/auth';
|
import { authRequest, KEBAB_RE } from '$lib/server/auth';
|
||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import type { Group } from '$lib/types';
|
import type { Group } from '$lib/types';
|
||||||
|
|
||||||
|
function renameIdInDescription(desc: string, oldId: string, newId: string): string {
|
||||||
|
const re = new RegExp(`\\{${escapeRegex(oldId)}(:[^}]*)?\\}`, 'g');
|
||||||
|
return desc.replace(re, (_, suffix) => `{${newId}${suffix ?? ''}}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(s: string): string {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
export const PUT: RequestHandler = async ({ request, params }) => {
|
export const PUT: RequestHandler = async ({ request, params }) => {
|
||||||
if (!authRequest(request)) return json({ error: 'Unauthorized' }, { status: 401 });
|
if (!authRequest(request)) return json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
@@ -17,6 +26,18 @@ export const PUT: RequestHandler = async ({ request, params }) => {
|
|||||||
|
|
||||||
const patch = body as Partial<Group>;
|
const patch = body as Partial<Group>;
|
||||||
const currentGroup = data.groups[gIdx];
|
const currentGroup = data.groups[gIdx];
|
||||||
|
const newId = patch.id;
|
||||||
|
const idChanging = newId !== undefined && newId !== id;
|
||||||
|
|
||||||
|
// Validate new ID if changing
|
||||||
|
if (idChanging) {
|
||||||
|
if (!KEBAB_RE.test(newId!)) {
|
||||||
|
return json({ error: 'id must be kebab-case' }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (data.groups.some((g) => g.id === newId)) {
|
||||||
|
return json({ error: `Group id '${newId}' already exists` }, { status: 400 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Special case: changing baseUnitId requires recalculating all unit toBase values
|
// Special case: changing baseUnitId requires recalculating all unit toBase values
|
||||||
if (patch.baseUnitId !== undefined && patch.baseUnitId !== currentGroup.baseUnitId) {
|
if (patch.baseUnitId !== undefined && patch.baseUnitId !== currentGroup.baseUnitId) {
|
||||||
@@ -25,10 +46,7 @@ export const PUT: RequestHandler = async ({ request, params }) => {
|
|||||||
if (!newBaseUnit) {
|
if (!newBaseUnit) {
|
||||||
return json({ error: `baseUnitId '${newBaseId}' not found in this group` }, { status: 400 });
|
return json({ error: `baseUnitId '${newBaseId}' not found in this group` }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
const X = newBaseUnit.toBase;
|
||||||
const X = newBaseUnit.toBase; // Current toBase of the new base unit
|
|
||||||
// Recalculate all units in this group: unit.toBase = unit.toBase / X
|
|
||||||
// Then set new base unit toBase = 1.0
|
|
||||||
for (const unit of data.units) {
|
for (const unit of data.units) {
|
||||||
if (unit.group === id) {
|
if (unit.group === id) {
|
||||||
unit.toBase = unit.id === newBaseId ? 1.0 : unit.toBase / X;
|
unit.toBase = unit.id === newBaseId ? 1.0 : unit.toBase / X;
|
||||||
@@ -38,8 +56,22 @@ export const PUT: RequestHandler = async ({ request, params }) => {
|
|||||||
|
|
||||||
const updated: Group = { ...currentGroup, ...patch };
|
const updated: Group = { ...currentGroup, ...patch };
|
||||||
data.groups[gIdx] = updated;
|
data.groups[gIdx] = updated;
|
||||||
saveData(data);
|
|
||||||
|
|
||||||
|
// Cascade ID rename across the dataset
|
||||||
|
if (idChanging) {
|
||||||
|
// Update unit.group references
|
||||||
|
for (const u of data.units) {
|
||||||
|
if (u.group === id) u.group = newId!;
|
||||||
|
}
|
||||||
|
// Update {id:*} placeholders in all unit descriptions
|
||||||
|
for (const u of data.units) {
|
||||||
|
if (u.description) {
|
||||||
|
u.description = renameIdInDescription(u.description, id, newId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData(data);
|
||||||
return json(updated);
|
return json(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,17 @@ import { authRequest, KEBAB_RE } from '$lib/server/auth';
|
|||||||
import type { RequestHandler } from './$types';
|
import type { RequestHandler } from './$types';
|
||||||
import type { Unit } from '$lib/types';
|
import type { Unit } from '$lib/types';
|
||||||
|
|
||||||
|
/** Replace all {oldId}, {oldId:field} placeholder tokens in a description string. */
|
||||||
|
function renameIdInDescription(desc: string, oldId: string, newId: string): string {
|
||||||
|
// Match {oldId} and {oldId:anything}
|
||||||
|
const re = new RegExp(`\\{${escapeRegex(oldId)}(:[^}]*)?\\}`, 'g');
|
||||||
|
return desc.replace(re, (_, suffix) => `{${newId}${suffix ?? ''}}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegex(s: string): string {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
|
}
|
||||||
|
|
||||||
export const PUT: RequestHandler = async ({ request, params }) => {
|
export const PUT: RequestHandler = async ({ request, params }) => {
|
||||||
if (!authRequest(request)) return json({ error: 'Unauthorized' }, { status: 401 });
|
if (!authRequest(request)) return json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
|
|
||||||
@@ -16,14 +27,16 @@ export const PUT: RequestHandler = async ({ request, params }) => {
|
|||||||
if (idx === -1) return json({ error: 'Unit not found' }, { status: 404 });
|
if (idx === -1) return json({ error: 'Unit not found' }, { status: 404 });
|
||||||
|
|
||||||
const patch = body as Partial<Unit>;
|
const patch = body as Partial<Unit>;
|
||||||
|
const newId = patch.id;
|
||||||
|
const idChanging = newId !== undefined && newId !== id;
|
||||||
|
|
||||||
// If id is changing, validate new id
|
// Validate new ID if changing
|
||||||
if (patch.id !== undefined && patch.id !== id) {
|
if (idChanging) {
|
||||||
if (!KEBAB_RE.test(patch.id)) {
|
if (!KEBAB_RE.test(newId!)) {
|
||||||
return json({ error: 'id must be kebab-case' }, { status: 400 });
|
return json({ error: 'id must be kebab-case' }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (data.units.some((u) => u.id === patch.id)) {
|
if (data.units.some((u) => u.id === newId)) {
|
||||||
return json({ error: `Unit id '${patch.id}' already exists` }, { status: 400 });
|
return json({ error: `Unit id '${newId}' already exists` }, { status: 400 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (patch.toBase !== undefined && patch.toBase <= 0) {
|
if (patch.toBase !== undefined && patch.toBase <= 0) {
|
||||||
@@ -35,8 +48,22 @@ export const PUT: RequestHandler = async ({ request, params }) => {
|
|||||||
|
|
||||||
const updated: Unit = { ...data.units[idx], ...patch };
|
const updated: Unit = { ...data.units[idx], ...patch };
|
||||||
data.units[idx] = updated;
|
data.units[idx] = updated;
|
||||||
saveData(data);
|
|
||||||
|
|
||||||
|
// Cascade ID rename across the dataset
|
||||||
|
if (idChanging) {
|
||||||
|
// Update group.baseUnitId references
|
||||||
|
for (const g of data.groups) {
|
||||||
|
if (g.baseUnitId === id) g.baseUnitId = newId!;
|
||||||
|
}
|
||||||
|
// Update {id:*} placeholders in all unit descriptions
|
||||||
|
for (const u of data.units) {
|
||||||
|
if (u.description) {
|
||||||
|
u.description = renameIdInDescription(u.description, id, newId!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
saveData(data);
|
||||||
return json(updated);
|
return json(updated);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user