Root location is two states now. reading, it is a path and one Edit. editing, it is a field and the things you can do to it. the actions moved to the block's BOTTOM row, off the value's line. an absolute path is long, and it was being truncated to make room for buttons that are not needed most of the time — the thing somebody opened this to read was the thing squeezed. shown, it wraps with `break-all` rather than truncating, because a path is read from both ends: the machine at the front, the folder at the back, and hiding either is worse than two lines. Edit is a mode and not an always-live field. this is looked at far more often than it is changed, and a live input invites a stray keystroke into a path everything else will depend on. Cancel restores what is stored, so leaving the mode cannot leave half a path on screen looking like the setting. "back to the default" is gone, as asked — and `clearRootPath` and its DELETE route with it, since that button was the only caller. an unreferenced endpoint is the same rot as an unreferenced component, and this one was a second, quieter Save that wrote a value nobody chose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
86 lines
3.6 KiB
TypeScript
86 lines
3.6 KiB
TypeScript
import { isAbsolute, join, normalize } from 'node:path';
|
|
import { db } from 'officerdb/db';
|
|
import { eq } from 'drizzle-orm';
|
|
import { getOwnerHomeDir } from '@@/data-path';
|
|
import { fofosAdminSettings } from '../db/schema';
|
|
|
|
/**
|
|
* Administration's settings — currently one, where the fofos site is on this machine.
|
|
*
|
|
* ── The default is COMPUTED, not written ──
|
|
*
|
|
* No row means `<owner home>/fofos`, worked out on every read. The alternative — writing the default
|
|
* in at install — freezes whatever the home directory was on the day the plugin was installed, and a
|
|
* restore onto a machine with a different owner then points at a directory that does not exist while
|
|
* looking deliberately chosen. Computed, it follows the machine; written, it follows the install.
|
|
*
|
|
* A saved path is a DECISION and is stored exactly as given. Only the absence of one is derived — and
|
|
* once saved it stays saved: there is no route back to the default, because "reset this to something
|
|
* you never chose" is not a thing anybody asked for and it read as a second, quieter Save.
|
|
*
|
|
* ── Absolute, always ──
|
|
*
|
|
* `~` is expanded here and never stored. It means "whoever is asking", and the people asking are not
|
|
* all the same person: this plugin is grantable, and the site is in one place regardless of who is
|
|
* looking at it. Storing the tilde would give every user a different answer to a question that has
|
|
* one.
|
|
*/
|
|
|
|
export type SettingsView = {
|
|
/** Absolute. Never null — an unset root reports its default rather than nothing. */
|
|
rootPath: string;
|
|
/** False while the default is standing in, so a form can show it as a suggestion, not a choice. */
|
|
isSet: boolean;
|
|
};
|
|
|
|
const row = async () => {
|
|
const rows = await db.select().from(fofosAdminSettings).limit(1);
|
|
return rows[0] ?? null;
|
|
};
|
|
|
|
/** `<owner home>/fofos`. The owner's, not the caller's — see the note on absoluteness above. */
|
|
export const defaultRootPath = (email: string): string => join(getOwnerHomeDir(email), 'fofos');
|
|
|
|
export async function readSettings(email: string): Promise<SettingsView> {
|
|
const found = await row();
|
|
const stored = found?.rootPath ?? null;
|
|
return { rootPath: stored ?? defaultRootPath(email), isSet: stored !== null };
|
|
}
|
|
|
|
/**
|
|
* Expand `~`, normalise, and refuse anything that is still not absolute.
|
|
*
|
|
* Refuses rather than resolving against a working directory: a relative path here would be resolved
|
|
* against whatever `process.cwd()` happened to be, which is the platform's checkout — a silently wrong
|
|
* answer that only shows up as an empty listing much later.
|
|
*/
|
|
export function normaliseRootPath(input: string, email: string): string {
|
|
const trimmed = input.trim();
|
|
if (!trimmed) throw new Error('A path is required.');
|
|
|
|
const home = getOwnerHomeDir(email);
|
|
const expanded = trimmed === '~' ? home : trimmed.startsWith('~/') ? join(home, trimmed.slice(2)) : trimmed;
|
|
|
|
if (!isAbsolute(expanded)) throw new Error('The root location must be an absolute path.');
|
|
|
|
// No trailing slash, so two spellings of the same directory cannot be stored as different values.
|
|
const cleaned = normalize(expanded).replace(/\/+$/, '');
|
|
return cleaned === '' ? '/' : cleaned;
|
|
}
|
|
|
|
export async function saveRootPath(input: string, email: string): Promise<SettingsView> {
|
|
const rootPath = normaliseRootPath(input, email);
|
|
const existing = await row();
|
|
|
|
if (existing) {
|
|
await db
|
|
.update(fofosAdminSettings)
|
|
.set({ rootPath, updatedAt: new Date() })
|
|
.where(eq(fofosAdminSettings.id, existing.id));
|
|
} else {
|
|
await db.insert(fofosAdminSettings).values({ rootPath });
|
|
}
|
|
|
|
return readSettings(email);
|
|
}
|