Compare commits

..

2 Commits

Author SHA1 Message Date
MangoPig
fd00f83585 Merge branch 'Features/Backend/Bootstrap-Reset' 2026-06-19 19:26:37 +01:00
MangoPig
935bee357c Feat: Add development bootstrap reset 2026-06-19 19:26:14 +01:00

View File

@@ -1,6 +1,6 @@
// Path: Frontend/src/components/workspace-home/WorkspaceHome/WorkspaceHome.tsx
import { For, Show, createEffect, createMemo, createSignal, type JSX } from "solid-js";
import { For, Show, createEffect, createMemo, createSignal, onMount, type JSX } from "solid-js";
import { Portal } from "solid-js/web";
import { createStore } from "solid-js/store";
import { resolveAPIBase } from "../../../lib/api";
@@ -44,6 +44,9 @@ const bootstrapStepDefinitions: readonly BootstrapStepDefinition[] = [
},
];
const bootstrapCompletionStorageKey = "moku.bootstrap.completed";
const isDevelopmentEnvironment = import.meta.env.DEV;
const defaultInstanceForm = {
protocol: "http",
access: "local",
@@ -55,7 +58,7 @@ const defaultModeForm = {
} as const;
const defaultAdminForm = {
displayName: "Admin",
displayName: "First admin",
email: "admin@example.com",
password: "",
} as const;
@@ -80,6 +83,27 @@ const initialSubmissionState = (): BootstrapSubmissionState => ({
error: "",
});
const readBootstrapCompletion = (): boolean => {
if (typeof window === "undefined") {
return false;
}
return window.localStorage.getItem(bootstrapCompletionStorageKey) === "true";
};
const writeBootstrapCompletion = (isComplete: boolean): void => {
if (typeof window === "undefined") {
return;
}
if (isComplete) {
window.localStorage.setItem(bootstrapCompletionStorageKey, "true");
return;
}
window.localStorage.removeItem(bootstrapCompletionStorageKey);
};
const readResponseBody = async (response: Response): Promise<unknown> => {
const raw = await response.text();
@@ -116,6 +140,13 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
const [isWizardOpen, setIsWizardOpen] = createSignal(false);
const [currentStepIndex, setCurrentStepIndex] = createSignal(0);
onMount(() => {
const isComplete = readBootstrapCompletion();
setIsBootstrapComplete(isComplete);
setIsWizardOpen(!isComplete);
setIsBootstrapStateResolved(true);
});
createEffect(() => {
if (modeForm.mode === "personal") {
setStructureForm("departmentName", personalStructureDefaults.departmentName);
@@ -133,28 +164,29 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
});
createEffect(() => {
const shellStatus = appShellData.status();
if (shellStatus === "idle" || shellStatus === "loading") {
if (!isBootstrapStateResolved()) {
return;
}
if (shellStatus !== "success") {
if (appShellData.status() !== "success") {
return;
}
const installationAccessor = appShellData.installation;
const installation = typeof installationAccessor === "function" ? installationAccessor() : undefined;
const installation = appShellData.installation();
const isPersistedBootstrap = installation?.isBootstrapped ?? false;
const wasComplete = isBootstrapComplete();
setIsBootstrapComplete(isPersistedBootstrap);
setIsWizardOpen(!isPersistedBootstrap);
setIsBootstrapStateResolved(true);
if (!isPersistedBootstrap && wasComplete) {
resetWizardState();
if (isPersistedBootstrap) {
return;
}
if (!isBootstrapComplete() && isWizardOpen()) {
return;
}
writeBootstrapCompletion(false);
setIsBootstrapComplete(false);
setIsWizardOpen(true);
resetWizardState();
});
const sidebarToggleLabel = (): string =>
@@ -246,13 +278,9 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
if (isLastStep()) {
await appShellData.reload();
const installationAccessor = appShellData.installation;
const installation = typeof installationAccessor === "function" ? installationAccessor() : undefined;
const isPersistedBootstrap = installation?.isBootstrapped ?? false;
setIsBootstrapComplete(isPersistedBootstrap);
setIsWizardOpen(!isPersistedBootstrap);
setIsBootstrapStateResolved(true);
writeBootstrapCompletion(true);
setIsBootstrapComplete(true);
setIsWizardOpen(false);
return;
}
@@ -264,6 +292,32 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
void submitCurrentStep();
};
const handleDevelopmentReset = async (): Promise<void> => {
const response = await fetch(`${apiBase()}/dev/bootstrap/reset`, {
method: "POST",
headers: {
Accept: "application/json",
},
});
const data = await readResponseBody(response);
if (!response.ok) {
throw new Error(
typeof data?.message === "string"
? data.message
: typeof data?.error?.message === "string"
? data.error.message
: "Failed to reset development bootstrap state.",
);
}
writeBootstrapCompletion(false);
setIsBootstrapComplete(false);
setIsWizardOpen(true);
resetWizardState();
await appShellData.reload();
};
const statusLabel = (state: BootstrapSubmissionState): string => {
switch (state.status) {
case "submitting":
@@ -325,6 +379,20 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
</button>
</div>
</Show>
<Show when={isDevelopmentEnvironment && isBootstrapStateResolved()}>
<div class={styles.heroActions}>
<button
type="button"
class={styles.secondaryButton}
data-slot="development-bootstrap-reset"
onClick={() => {
void handleDevelopmentReset();
}}
>
Reset development bootstrap state
</button>
</div>
</Show>
</section>
</main>
@@ -428,13 +496,13 @@ export const WorkspaceHome = (props: WorkspaceHomeProps): JSX.Element => {
<>
<label class={styles.field}>
<span class={styles.fieldLabel}>Display name</span>
<input
type="text"
value={adminForm.displayName}
onInput={(event): void => setAdminForm("displayName", event.currentTarget.value)}
placeholder="Admin"
/>
</label>
<input
type="text"
value={adminForm.displayName}
onInput={(event): void => setAdminForm("displayName", event.currentTarget.value)}
placeholder="First admin"
/>
</label>
<label class={styles.field}>
<span class={styles.fieldLabel}>Email</span>
<input