feat: sort-alpha button per group, settings in header, sortPosition pin
- ⇅ button on each group header: sorts units alpha (labelPlural + symbol tiebreak) via order API - ⚙ Settings moved to admin-header alongside Log out - Unit type gets sortPosition?: number (1-based, optional) - Converter sort: partitions pinned/floating, inserts pins at requested slots Pins > length append at end; gaps fill naturally with floating items - Admin unit form: Force position number input
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
"symbol": "dl",
|
"symbol": "dl",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 1,
|
"toBase": 1,
|
||||||
"description": "See {pound:description}. Let's also look at {us-oil-bbl}."
|
"description": "The basic unit of measurement for all weights and masses worldwide."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "pound",
|
"id": "pound",
|
||||||
@@ -16,25 +16,26 @@
|
|||||||
"symbol": "lb",
|
"symbol": "lb",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 0.038,
|
"toBase": 0.038,
|
||||||
"description": "My description is {dickload:description}"
|
"description": "Avoirdupois pounds, as used in the USA."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "us-oil-bbl",
|
"id": "us-oil-bbl",
|
||||||
"label": "oil barrel (US)",
|
"label": "barrel",
|
||||||
"labelPlural": "oil barrels (US)",
|
"labelPlural": "barrels",
|
||||||
"symbol": "us oil bbl",
|
"symbol": "bbl us oil",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 13.3192584,
|
"toBase": 13.3192584,
|
||||||
"description": "Weight is based on pure water at 4 degrees celsius.",
|
"description": "Weight is based on pure water at 4 degrees celsius.",
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "hogshead",
|
"id": "imperial-hogshead",
|
||||||
"label": "hogshead",
|
"label": "hogshead",
|
||||||
"labelPlural": "hogsheads",
|
"labelPlural": "hogsheads",
|
||||||
"symbol": "hhd",
|
"symbol": "hhd imperial",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 38
|
"toBase": 27.420633296,
|
||||||
|
"description": "The hogshead as understood by residents of Old Blighty."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "short-ton",
|
"id": "short-ton",
|
||||||
@@ -108,26 +109,34 @@
|
|||||||
"id": "us-fluid-bbl",
|
"id": "us-fluid-bbl",
|
||||||
"label": "barrel",
|
"label": "barrel",
|
||||||
"labelPlural": "barrels",
|
"labelPlural": "barrels",
|
||||||
"symbol": "us fluid bbl",
|
"symbol": "bbl us fluid",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 9.9894438
|
"toBase": 9.9894438
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "us-dry-bbl",
|
"id": "us-dry-bbl",
|
||||||
"label": "dry barrel",
|
"label": "barrel",
|
||||||
"labelPlural": "dry barrels",
|
"labelPlural": "barrels",
|
||||||
"symbol": "us dry bbl",
|
"symbol": "bbl us dry",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 1
|
"toBase": 9.6864954123
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "us-hogshead",
|
"id": "us-hogshead",
|
||||||
"label": "hogshead (US)",
|
"label": "hogshead",
|
||||||
"labelPlural": "hogsheads (US)",
|
"labelPlural": "hogsheads",
|
||||||
"symbol": "us hhd",
|
"symbol": "hhd us",
|
||||||
"group": "dickloads",
|
"group": "dickloads",
|
||||||
"toBase": 19.9788876,
|
"toBase": 19.9788876,
|
||||||
"description": "Equal to two US fluid barrels."
|
"description": "Equal to two US fluid barrels."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "imperial-barrel",
|
||||||
|
"label": "barrel",
|
||||||
|
"labelPlural": "barrels",
|
||||||
|
"symbol": "bbl imperial",
|
||||||
|
"group": "dickloads",
|
||||||
|
"toBase": 13.710316648
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"groups": [
|
"groups": [
|
||||||
|
|||||||
@@ -30,6 +30,14 @@ export interface Unit {
|
|||||||
|
|
||||||
/** If true, this unit is excluded from automatic rendering in the converter. */
|
/** If true, this unit is excluded from automatic rendering in the converter. */
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Optional fixed display position (1-based) within the group's rendered list.
|
||||||
|
* Pinned units are inserted at their requested slot after floating units are ordered.
|
||||||
|
* If position > total units, the unit is appended at the end in ascending sortPosition order.
|
||||||
|
* Ties are broken by the group's natural sort order.
|
||||||
|
*/
|
||||||
|
sortPosition?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A group of units sharing a base unit and a universal scale factor. */
|
/** A group of units sharing a base unit and a universal scale factor. */
|
||||||
|
|||||||
@@ -152,13 +152,29 @@
|
|||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const items = byGroup.get(group.id);
|
const items = byGroup.get(group.id);
|
||||||
if (items?.length) {
|
if (items?.length) {
|
||||||
const sorted = group.sortOrder === 'alpha'
|
// Base order: alpha or defined
|
||||||
|
const base = group.sortOrder === 'alpha'
|
||||||
? [...items].sort((a, b) => {
|
? [...items].sort((a, b) => {
|
||||||
const labelCmp = a.unit.labelPlural.localeCompare(b.unit.labelPlural);
|
const labelCmp = a.unit.labelPlural.localeCompare(b.unit.labelPlural);
|
||||||
return labelCmp !== 0 ? labelCmp : a.unit.symbol.localeCompare(b.unit.symbol);
|
return labelCmp !== 0 ? labelCmp : a.unit.symbol.localeCompare(b.unit.symbol);
|
||||||
})
|
})
|
||||||
: items;
|
: [...items];
|
||||||
sections.push({ groupLabel: group.label, alwaysShowLabel: group.alwaysShowLabel ?? false, items: sorted });
|
|
||||||
|
// Partition into pinned (sortPosition set) and floating
|
||||||
|
const pinned = base.filter((r) => r.unit.sortPosition != null)
|
||||||
|
.sort((a, b) => (a.unit.sortPosition ?? 0) - (b.unit.sortPosition ?? 0));
|
||||||
|
const floating = base.filter((r) => r.unit.sortPosition == null);
|
||||||
|
|
||||||
|
// Insert pinned units at their requested 1-based positions
|
||||||
|
// Position > length → append at end in sortPosition order
|
||||||
|
const result: typeof base = [...floating];
|
||||||
|
for (const p of pinned) {
|
||||||
|
const pos = Math.max(1, p.unit.sortPosition ?? 1);
|
||||||
|
const insertAt = Math.min(pos - 1, result.length);
|
||||||
|
result.splice(insertAt, 0, p);
|
||||||
|
}
|
||||||
|
|
||||||
|
sections.push({ groupLabel: group.label, alwaysShowLabel: group.alwaysShowLabel ?? false, items: result });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const orphaned = byGroup.get(null);
|
const orphaned = byGroup.get(null);
|
||||||
|
|||||||
@@ -638,6 +638,20 @@
|
|||||||
formError = null;
|
formError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function sortGroupAlpha(group: Group) {
|
||||||
|
const units = unitsData.units.filter((u) => u.group === group.id);
|
||||||
|
const sorted = [...units].sort((a, b) => {
|
||||||
|
const cmp = a.labelPlural.localeCompare(b.labelPlural);
|
||||||
|
return cmp !== 0 ? cmp : a.symbol.localeCompare(b.symbol);
|
||||||
|
});
|
||||||
|
const res = await fetch(`${api}/order`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ units: { [group.id]: sorted.map((u) => u.id) } })
|
||||||
|
});
|
||||||
|
if (res.ok) await loadData();
|
||||||
|
}
|
||||||
|
|
||||||
function closeDeleteGroup() { deleteGroupTarget = null; }
|
function closeDeleteGroup() { deleteGroupTarget = null; }
|
||||||
|
|
||||||
async function confirmDeleteGroup() {
|
async function confirmDeleteGroup() {
|
||||||
@@ -683,8 +697,11 @@
|
|||||||
|
|
||||||
<div class="admin-header">
|
<div class="admin-header">
|
||||||
<h2>Admin</h2>
|
<h2>Admin</h2>
|
||||||
|
<div class="admin-header-actions">
|
||||||
|
<button class="outline secondary" onclick={() => { selectedItem = { type: 'settings', id: 'settings' }; }}>⚙ Settings</button>
|
||||||
<button class="outline secondary logout-btn" onclick={logout}>Log out</button>
|
<button class="outline secondary logout-btn" onclick={logout}>Log out</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="admin-panels">
|
<div class="admin-panels">
|
||||||
<!-- ── Left panel ─────────────────────────────────────────────────────────── -->
|
<!-- ── Left panel ─────────────────────────────────────────────────────────── -->
|
||||||
@@ -800,6 +817,7 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Delete only — edit is via click on name -->
|
<!-- Delete only — edit is via click on name -->
|
||||||
|
<button class="plain icon-btn" title="Sort alphabetically" onclick={() => sortGroupAlpha(group)}>⇅</button>
|
||||||
<button class="plain icon-btn" title="Delete group" onclick={() => openDeleteGroup(group)}>🗑</button>
|
<button class="plain icon-btn" title="Delete group" onclick={() => openDeleteGroup(group)}>🗑</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -900,7 +918,6 @@
|
|||||||
<div class="tree-footer">
|
<div class="tree-footer">
|
||||||
<button onclick={() => startNewUnit()}>+ Add Unit</button>
|
<button onclick={() => startNewUnit()}>+ Add Unit</button>
|
||||||
<button class="outline" onclick={startNewGroup}>+ Add Group</button>
|
<button class="outline" onclick={startNewGroup}>+ Add Group</button>
|
||||||
<button class="outline secondary" onclick={() => { selectedItem = { type: 'settings', id: 'settings' }; }}>⚙ Settings</button>
|
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -960,6 +977,10 @@
|
|||||||
Description <small class="muted">(optional — shown as tooltip in converter)</small>
|
Description <small class="muted">(optional — shown as tooltip in converter)</small>
|
||||||
<textarea bind:value={unitForm.description} placeholder="e.g. A unit based on…" rows="2"></textarea>
|
<textarea bind:value={unitForm.description} placeholder="e.g. A unit based on…" rows="2"></textarea>
|
||||||
</label>
|
</label>
|
||||||
|
<label>
|
||||||
|
Force position <small class="muted">(optional — pin this unit to a specific slot in the group)</small>
|
||||||
|
<input type="number" bind:value={unitForm.sortPosition} min="1" step="1" placeholder="e.g. 3" />
|
||||||
|
</label>
|
||||||
<label class="checkbox-label">
|
<label class="checkbox-label">
|
||||||
<input type="checkbox" bind:checked={unitForm.hidden} />
|
<input type="checkbox" bind:checked={unitForm.hidden} />
|
||||||
<span>Don't show on front end</span>
|
<span>Don't show on front end</span>
|
||||||
@@ -1134,6 +1155,11 @@
|
|||||||
margin-bottom: 1.5rem;
|
margin-bottom: 1.5rem;
|
||||||
}
|
}
|
||||||
.admin-header h2 { margin: 0; }
|
.admin-header h2 { margin: 0; }
|
||||||
|
.admin-header-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.admin-panels {
|
.admin-panels {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in New Issue
Block a user