Handle deleted/moved workspace folders (missing state)

Detect when the current workspace folder cannot be opened and show a
dedicated "missing" screen that explains the workspace was not found.
Catch failures from get_lists during loadConfig, set a missingWorkspace
state and switch to the missing screen, and provide
forgetMissingWorkspace() to remove the missing workspace from the config
and either switch to the next available workspace or fall back to the
setup screen. Add UI in App.svelte to present the workspace name,
explanation, and a Continue button that invokes forgetting the missing
workspace.
This commit is contained in:
Tristan Michael 2026-04-05 14:29:30 -07:00
parent 753cb1cad5
commit ac789e8d56
3 changed files with 56 additions and 3 deletions

View file

@ -29,7 +29,25 @@
</div> </div>
{/if} {/if}
{#if app.screen === "setup"} {#if app.screen === "missing"}
<div class="flex h-full items-center justify-center p-6">
<div class="w-full max-w-sm rounded-2xl bg-card-light p-8 shadow-lg dark:bg-card-dark">
<h1 class="mb-1 text-2xl font-bold">Workspace Not Found</h1>
<p class="mb-2 text-sm text-text-secondary-light dark:text-text-secondary-dark">
The workspace <strong>{app.missingWorkspace}</strong> could not be opened. Its folder may have been moved or deleted.
</p>
<p class="mb-6 text-sm text-text-secondary-light dark:text-text-secondary-dark">
It will be removed from your workspace list. You can re-add it if the folder becomes available again.
</p>
<button
onclick={() => app.forgetMissingWorkspace()}
class="w-full rounded-lg bg-primary py-2.5 text-sm font-medium text-white hover:bg-primary-hover"
>
Continue
</button>
</div>
</div>
{:else if app.screen === "setup"}
<SetupScreen cancellable={app.hasWorkspace} /> <SetupScreen cancellable={app.hasWorkspace} />
{:else} {:else}
<TasksScreen /> <TasksScreen />

View file

@ -25,6 +25,7 @@ let syncing = $state(false);
let syncMode = $state<"full" | "push" | "pull">("full"); let syncMode = $state<"full" | "push" | "pull">("full");
let lastSyncResult = $state<SyncResult | null>(null); let lastSyncResult = $state<SyncResult | null>(null);
let error = $state<string | null>(null); let error = $state<string | null>(null);
let missingWorkspace = $state<string | null>(null);
// ── Derived ────────────────────────────────────────────────────────── // ── Derived ──────────────────────────────────────────────────────────
@ -70,8 +71,18 @@ async function loadConfig() {
try { try {
config = await invoke<AppConfig>("get_config"); config = await invoke<AppConfig>("get_config");
if (hasWorkspace) { if (hasWorkspace) {
// Try loading lists — if the workspace path is gone, get_lists will fail
lists = [];
try {
lists = await invoke<TaskList[]>("get_lists");
} catch {
missingWorkspace = config!.current_workspace;
screen = "missing";
return;
}
if (lists.length > 0 && !activeListId) activeListId = lists[0].id;
if (activeListId) await loadTasks();
screen = "tasks"; screen = "tasks";
await loadLists();
} else { } else {
screen = "setup"; screen = "setup";
} }
@ -343,6 +354,26 @@ async function addWebdavWorkspace(name: string, webdavUrl: string, webdavPath: s
} }
} }
async function forgetMissingWorkspace() {
if (!missingWorkspace) return;
await removeWorkspace(missingWorkspace);
missingWorkspace = null;
config = await invoke<AppConfig>("get_config");
if (hasWorkspace) {
// Switch to the next available workspace
const nextName = Object.keys(config!.workspaces)[0];
if (nextName) {
await switchWorkspace(nextName);
screen = "tasks";
return;
}
}
screen = "setup";
lists = [];
tasks = [];
activeListId = null;
}
function setScreen(s: Screen) { function setScreen(s: Screen) {
screen = s; screen = s;
} }
@ -399,6 +430,9 @@ export const app = {
get hasWorkspace() { get hasWorkspace() {
return hasWorkspace; return hasWorkspace;
}, },
get missingWorkspace() {
return missingWorkspace;
},
getSubtasks, getSubtasks,
loadConfig, loadConfig,
addWorkspace, addWorkspace,
@ -422,6 +456,7 @@ export const app = {
setSyncMode, setSyncMode,
setTheme, setTheme,
addWebdavWorkspace, addWebdavWorkspace,
forgetMissingWorkspace,
setScreen, setScreen,
clearError, clearError,
}; };

View file

@ -44,4 +44,4 @@ export interface SyncResult {
errors: string[]; errors: string[];
} }
export type Screen = "setup" | "tasks" | "settings"; export type Screen = "setup" | "tasks" | "settings" | "missing";