lots more stuff
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {import('$lib/models/note.js').Note} Note
|
||||
*/
|
||||
|
||||
import { objectsStore } from '$lib/stores/objects.js';
|
||||
import ObjectRenderer from './ObjectRenderer.svelte';
|
||||
|
||||
/** @type {{ object: Note, depth?: number }} */
|
||||
let { object, depth = 0 } = $props();
|
||||
|
||||
let editing = $state(false);
|
||||
let draftText = $state('');
|
||||
|
||||
function startEdit() {
|
||||
draftText = $state.snapshot(object).text;
|
||||
editing = true;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing = false;
|
||||
}
|
||||
|
||||
async function saveText() {
|
||||
await objectsStore.updateText(object.objectId, draftText);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
await objectsStore.removeObject(object.objectId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="note">
|
||||
<div class="header">
|
||||
{#if editing}
|
||||
<input type="text" bind:value={draftText} />
|
||||
<button onclick={saveText}>Save</button>
|
||||
<button onclick={cancelEdit}>Cancel</button>
|
||||
{:else}
|
||||
<p class="note-text">{object.text}</p>
|
||||
<button onclick={startEdit}>Edit</button>
|
||||
{/if}
|
||||
<button onclick={handleDelete}>Delete</button>
|
||||
</div>
|
||||
|
||||
<div class="children">
|
||||
{#each object.childIds as childId (childId)}
|
||||
{@const childObject = $objectsStore.objects.get(childId)}
|
||||
{#if childObject}
|
||||
<ObjectRenderer object={childObject} depth={depth + 1} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.note {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.note-text {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.children {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {import('$lib/services/objectParser.js').TypedObject} TypedObject
|
||||
*/
|
||||
|
||||
import Root from './Root.svelte';
|
||||
import Project from './Project.svelte';
|
||||
import Task from './Task.svelte';
|
||||
import Note from './Note.svelte';
|
||||
|
||||
/** @type {{ object: TypedObject, depth?: number }} */
|
||||
let { object, depth = 0 } = $props();
|
||||
</script>
|
||||
|
||||
<div class="object-renderer" style:padding-left={`${depth * 1.5}rem`}>
|
||||
{#if object.type === 'root'}
|
||||
<Root {object} {depth} />
|
||||
{:else if object.type === 'project'}
|
||||
<Project {object} {depth} />
|
||||
{:else if object.type === 'task'}
|
||||
<Task {object} {depth} />
|
||||
{:else if object.type === 'note'}
|
||||
<Note {object} {depth} />
|
||||
{:else}
|
||||
<p>Unknown object type: {object.type}</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,79 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {import('$lib/models/project.js').Project} Project
|
||||
*/
|
||||
|
||||
import { objectsStore } from '$lib/stores/objects.js';
|
||||
import ObjectRenderer from './ObjectRenderer.svelte';
|
||||
|
||||
/** @type {{ object: Project, depth?: number }} */
|
||||
let { object, depth = 0 } = $props();
|
||||
|
||||
let editing = $state(false);
|
||||
let draftName = $state('');
|
||||
|
||||
function startEdit() {
|
||||
draftName = $state.snapshot(object).name;
|
||||
editing = true;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing = false;
|
||||
}
|
||||
|
||||
async function saveName() {
|
||||
await objectsStore.updateProjectName(object.objectId, draftName);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
await objectsStore.removeObject(object.objectId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="project">
|
||||
<div class="header">
|
||||
{#if editing}
|
||||
<input type="text" bind:value={draftName} />
|
||||
<button onclick={saveName}>Save</button>
|
||||
<button onclick={cancelEdit}>Cancel</button>
|
||||
{:else}
|
||||
<h3>{object.name}</h3>
|
||||
<button onclick={startEdit}>Edit</button>
|
||||
{/if}
|
||||
<button onclick={handleDelete}>Delete</button>
|
||||
</div>
|
||||
|
||||
<div class="children">
|
||||
{#each object.childIds as childId (childId)}
|
||||
{@const childObject = $objectsStore.objects.get(childId)}
|
||||
{#if childObject}
|
||||
<ObjectRenderer object={childObject} depth={depth + 1} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.project {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.header h3 {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.children {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,34 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {import('$lib/models/root.js').Root} Root
|
||||
* @typedef {import('$lib/services/objectParser.js').TypedObject} TypedObject
|
||||
*/
|
||||
|
||||
import { objectsStore } from '$lib/stores/objects.js';
|
||||
import ObjectRenderer from './ObjectRenderer.svelte';
|
||||
|
||||
/** @type {{ object: Root, depth?: number }} */
|
||||
let { object, depth = 0 } = $props();
|
||||
|
||||
/** @type {TypedObject | undefined} */
|
||||
let childObject = $derived(undefined);
|
||||
</script>
|
||||
|
||||
<section class="root">
|
||||
{#if depth === 0}
|
||||
<h2>Root</h2>
|
||||
{/if}
|
||||
|
||||
{#each object.childIds as childId (childId)}
|
||||
{@const childObject = $objectsStore.objects.get(childId)}
|
||||
{#if childObject}
|
||||
<ObjectRenderer object={childObject} depth={depth + 1} />
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.root {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,100 @@
|
||||
<script>
|
||||
/**
|
||||
* @typedef {import('$lib/models/task.js').Task} Task
|
||||
* @typedef {import('$lib/models/task.js').TaskStatus} TaskStatus
|
||||
*/
|
||||
|
||||
import { objectsStore } from '$lib/stores/objects.js';
|
||||
import ObjectRenderer from './ObjectRenderer.svelte';
|
||||
|
||||
/** @type {{ object: Task, depth?: number }} */
|
||||
let { object, depth = 0 } = $props();
|
||||
|
||||
let editing = $state(false);
|
||||
let draftText = $state('');
|
||||
|
||||
const statusOptions = /** @type {TaskStatus[]} */ ([
|
||||
'not-started',
|
||||
'in-progress',
|
||||
'completed',
|
||||
'cancelled'
|
||||
]);
|
||||
|
||||
function startEdit() {
|
||||
draftText = $state.snapshot(object).text;
|
||||
editing = true;
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
editing = false;
|
||||
}
|
||||
|
||||
async function saveText() {
|
||||
await objectsStore.updateText(object.objectId, draftText);
|
||||
editing = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Event} event
|
||||
*/
|
||||
async function handleStatusChange(event) {
|
||||
const target = /** @type {HTMLSelectElement} */ (event.target);
|
||||
await objectsStore.updateTaskStatus(object.objectId, /** @type {TaskStatus} */ (target.value));
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
await objectsStore.removeObject(object.objectId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<article class="task">
|
||||
<div class="header">
|
||||
{#if editing}
|
||||
<input type="text" bind:value={draftText} />
|
||||
<button onclick={saveText}>Save</button>
|
||||
<button onclick={cancelEdit}>Cancel</button>
|
||||
{:else}
|
||||
<p class="task-text">{object.text}</p>
|
||||
<button onclick={startEdit}>Edit</button>
|
||||
{/if}
|
||||
<select value={object.status} onchange={handleStatusChange}>
|
||||
{#each statusOptions as status}
|
||||
<option value={status}>{status}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<button onclick={handleDelete}>Delete</button>
|
||||
</div>
|
||||
|
||||
<div class="children">
|
||||
{#each object.childIds as childId (childId)}
|
||||
{@const childObject = $objectsStore.objects.get(childId)}
|
||||
{#if childObject}
|
||||
<ObjectRenderer object={childObject} depth={depth + 1} />
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.task {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.task-text {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.children {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* @typedef {Object} User
|
||||
* @property {string} userId
|
||||
* @property {string} name
|
||||
* @property {string} rootObjectId
|
||||
*/
|
||||
|
||||
@@ -8,9 +9,10 @@
|
||||
* Create a User object.
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} name
|
||||
* @param {string} rootObjectId
|
||||
* @returns {User}
|
||||
*/
|
||||
export function createUserModel(userId, rootObjectId) {
|
||||
return { userId, rootObjectId };
|
||||
export function createUserModel(userId, name, rootObjectId) {
|
||||
return { userId, name, rootObjectId };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { createRootModel } from '$lib/models/root.js';
|
||||
import { createProjectModel } from '$lib/models/project.js';
|
||||
import { createTaskModel } from '$lib/models/task.js';
|
||||
import { createNoteModel } from '$lib/models/note.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('$lib/models/root.js').Root} Root
|
||||
* @typedef {import('$lib/models/project.js').Project} Project
|
||||
* @typedef {import('$lib/models/task.js').Task} Task
|
||||
* @typedef {import('$lib/models/note.js').Note} Note
|
||||
* @typedef {Root | Project | Task | Note} TypedObject
|
||||
* @typedef {import('./objects.js').ObjectRecord} ObjectRecord
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse a backend ObjectRecord into a typed domain object.
|
||||
*
|
||||
* @param {ObjectRecord} record
|
||||
* @returns {TypedObject | null}
|
||||
*/
|
||||
export function parseObjectRecord(record) {
|
||||
try {
|
||||
const data = JSON.parse(record.objectData);
|
||||
|
||||
switch (data.type) {
|
||||
case 'root':
|
||||
return createRootModel(record.objectId, data.childIds ?? []);
|
||||
case 'project':
|
||||
return createProjectModel(record.objectId, data.name ?? '', data.childIds ?? []);
|
||||
case 'task':
|
||||
return createTaskModel(
|
||||
record.objectId,
|
||||
data.text ?? '',
|
||||
data.status ?? 'not-started',
|
||||
data.childIds ?? []
|
||||
);
|
||||
case 'note':
|
||||
return createNoteModel(record.objectId, data.text ?? '', data.childIds ?? []);
|
||||
default:
|
||||
console.warn(`Unknown object type: ${data.type}`);
|
||||
return null;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Failed to parse object ${record.objectId}:`, err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a list of ObjectRecords into a map of typed objects.
|
||||
* Also returns the root object ID if one is found.
|
||||
*
|
||||
* @param {ObjectRecord[]} records
|
||||
* @returns {{ objects: Map<string, TypedObject>, rootId: string | null }}
|
||||
*/
|
||||
export function parseObjectRecords(records) {
|
||||
/** @type {Map<string, TypedObject>} */
|
||||
const objects = new Map();
|
||||
/** @type {string | null} */
|
||||
let rootId = null;
|
||||
|
||||
for (const record of records) {
|
||||
const parsed = parseObjectRecord(record);
|
||||
if (!parsed) continue;
|
||||
|
||||
objects.set(parsed.objectId, parsed);
|
||||
|
||||
if (parsed.type === 'root') {
|
||||
rootId = parsed.objectId;
|
||||
}
|
||||
}
|
||||
|
||||
return { objects, rootId };
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a typed domain object into the JSON payload stored in objectData.
|
||||
*
|
||||
* @param {TypedObject} obj
|
||||
* @returns {string}
|
||||
*/
|
||||
export function serializeTypedObject(obj) {
|
||||
const { objectId, ...data } = obj;
|
||||
return JSON.stringify(data);
|
||||
}
|
||||
@@ -8,17 +8,18 @@ import { apiFetch } from './api.js';
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new object.
|
||||
* Create a new object. The backend will use the supplied objectId if provided,
|
||||
* otherwise it will assign a UUID.
|
||||
*
|
||||
* @param {string} objectId
|
||||
* @param {string} userId
|
||||
* @param {string} objectData
|
||||
* @returns {Promise<{status: string}>}
|
||||
* @param {string} [objectId]
|
||||
* @returns {Promise<{status: string, objectId: string}>}
|
||||
*/
|
||||
export function createObject(objectId, userId, objectData) {
|
||||
export function createObject(userId, objectData, objectId) {
|
||||
return apiFetch('/objects', {
|
||||
method: 'POST',
|
||||
body: { objectId, userId, objectData }
|
||||
body: { userId, objectData, objectId }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,25 +3,27 @@ import { apiFetch } from './api.js';
|
||||
/**
|
||||
* @typedef {Object} User
|
||||
* @property {string} userId
|
||||
* @property {string} name
|
||||
* @property {string} rootObjectId
|
||||
* @property {Object[]} objects
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a new user.
|
||||
* Create a new user. The backend will create an empty root object.
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} rootObjectId
|
||||
* @returns {Promise<{status: string}>}
|
||||
* @param {string} name
|
||||
* @returns {Promise<{status: string, rootObjectId: string}>}
|
||||
*/
|
||||
export function createUser(userId, rootObjectId) {
|
||||
export function createUser(userId, name) {
|
||||
return apiFetch('/users', {
|
||||
method: 'POST',
|
||||
body: { userId, rootObjectId }
|
||||
body: { userId, name }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a user by ID.
|
||||
* Fetch a user by ID. Returns the user profile and all associated objects.
|
||||
*
|
||||
* @param {string} userId
|
||||
* @returns {Promise<User>}
|
||||
@@ -31,16 +33,16 @@ export function getUser(userId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's root object.
|
||||
* Update a user's display name.
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {string} rootObjectId
|
||||
* @param {string} name
|
||||
* @returns {Promise<{status: string}>}
|
||||
*/
|
||||
export function updateUser(userId, rootObjectId) {
|
||||
export function updateUser(userId, name) {
|
||||
return apiFetch(`/users/${userId}`, {
|
||||
method: 'PUT',
|
||||
body: { rootObjectId }
|
||||
body: { name }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import { writable, get } from 'svelte/store';
|
||||
import { createObject, updateObject, deleteObject } from '$lib/services/objects.js';
|
||||
import { getUser } from '$lib/services/users.js';
|
||||
import { parseObjectRecords, serializeTypedObject } from '$lib/services/objectParser.js';
|
||||
import { createRootModel } from '$lib/models/root.js';
|
||||
import { createProjectModel } from '$lib/models/project.js';
|
||||
import { createTaskModel } from '$lib/models/task.js';
|
||||
import { createNoteModel } from '$lib/models/note.js';
|
||||
|
||||
/**
|
||||
* @typedef {import('$lib/services/objectParser.js').TypedObject} TypedObject
|
||||
* @typedef {import('$lib/models/task.js').TaskStatus} TaskStatus
|
||||
*/
|
||||
|
||||
function createObjectsStore() {
|
||||
const { subscribe, set, update } = writable({
|
||||
/** @type {Map<string, TypedObject>} */
|
||||
objects: new Map(),
|
||||
/** @type {string | null} */
|
||||
rootId: null,
|
||||
loading: false,
|
||||
error: /** @type {string | null} */ (null)
|
||||
});
|
||||
|
||||
/**
|
||||
* Load all objects for a user starting from the root object.
|
||||
* Creates a root object if the user has none.
|
||||
*
|
||||
* @param {string} userId
|
||||
*/
|
||||
async function loadObjects(userId) {
|
||||
update((state) => ({ ...state, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const user = await getUser(userId);
|
||||
let rootId = user.rootObjectId;
|
||||
|
||||
/** @type {Map<string, TypedObject>} */
|
||||
const objects = new Map();
|
||||
|
||||
// The bulk user endpoint returns all objects in one response.
|
||||
const records = user.objects ?? [];
|
||||
const parsed = parseObjectRecords(records);
|
||||
parsed.objects.forEach((obj, id) => objects.set(id, obj));
|
||||
if (parsed.rootId) {
|
||||
rootId = parsed.rootId;
|
||||
}
|
||||
|
||||
if (!rootId) {
|
||||
const root = createRootModel(crypto.randomUUID());
|
||||
rootId = root.objectId;
|
||||
const response = await createObject(userId, serializeTypedObject(root), rootId);
|
||||
rootId = response.objectId;
|
||||
objects.set(rootId, { ...root, objectId: rootId });
|
||||
}
|
||||
|
||||
update((state) => ({ ...state, objects, rootId, loading: false }));
|
||||
} catch (err) {
|
||||
update((state) => ({
|
||||
...state,
|
||||
loading: false,
|
||||
error: err instanceof Error ? err.message : 'Failed to load objects'
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a single object to the backend and update local state.
|
||||
*
|
||||
* @param {string} objectId
|
||||
*/
|
||||
async function persistObject(objectId) {
|
||||
const state = get({ subscribe });
|
||||
const obj = state.objects.get(objectId);
|
||||
if (!obj) return;
|
||||
|
||||
await updateObject(objectId, serializeTypedObject(obj));
|
||||
update((state) => ({ ...state, objects: new Map(state.objects) }));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new object, attach it to a parent, and persist both.
|
||||
*
|
||||
* @param {string} userId
|
||||
* @param {'project' | 'task' | 'note'} type
|
||||
* @param {string} parentId
|
||||
* @param {string} [nameOrText='']
|
||||
* @param {TaskStatus} [status='not-started']
|
||||
*/
|
||||
async function addObject(userId, type, parentId, nameOrText = '', status = 'not-started') {
|
||||
const state = get({ subscribe });
|
||||
const parent = state.objects.get(parentId);
|
||||
if (!parent) throw new Error('Parent not found');
|
||||
|
||||
/** @type {TypedObject} */
|
||||
let obj;
|
||||
switch (type) {
|
||||
case 'project':
|
||||
obj = createProjectModel(crypto.randomUUID(), nameOrText);
|
||||
break;
|
||||
case 'task':
|
||||
obj = createTaskModel(crypto.randomUUID(), nameOrText, status);
|
||||
break;
|
||||
case 'note':
|
||||
obj = createNoteModel(crypto.randomUUID(), nameOrText);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unsupported object type: ${type}`);
|
||||
}
|
||||
|
||||
const response = await createObject(userId, serializeTypedObject(obj), obj.objectId);
|
||||
const created = { ...obj, objectId: response.objectId };
|
||||
|
||||
const updatedParent = { ...parent, childIds: [...parent.childIds, created.objectId] };
|
||||
|
||||
update((state) => {
|
||||
const objects = new Map(state.objects);
|
||||
objects.set(created.objectId, created);
|
||||
objects.set(parentId, updatedParent);
|
||||
return { ...state, objects };
|
||||
});
|
||||
|
||||
await persistObject(parentId);
|
||||
await persistObject(created.objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a project name.
|
||||
*
|
||||
* @param {string} objectId
|
||||
* @param {string} name
|
||||
*/
|
||||
async function updateProjectName(objectId, name) {
|
||||
update((state) => {
|
||||
const obj = state.objects.get(objectId);
|
||||
if (!obj || obj.type !== 'project') return state;
|
||||
const objects = new Map(state.objects);
|
||||
objects.set(objectId, { ...obj, name });
|
||||
return { ...state, objects };
|
||||
});
|
||||
await persistObject(objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update text for a task or note.
|
||||
*
|
||||
* @param {string} objectId
|
||||
* @param {string} text
|
||||
*/
|
||||
async function updateText(objectId, text) {
|
||||
update((state) => {
|
||||
const obj = state.objects.get(objectId);
|
||||
if (!obj || (obj.type !== 'task' && obj.type !== 'note')) return state;
|
||||
const objects = new Map(state.objects);
|
||||
objects.set(objectId, { ...obj, text });
|
||||
return { ...state, objects };
|
||||
});
|
||||
await persistObject(objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a task status.
|
||||
*
|
||||
* @param {string} objectId
|
||||
* @param {TaskStatus} status
|
||||
*/
|
||||
async function updateTaskStatus(objectId, status) {
|
||||
update((state) => {
|
||||
const obj = state.objects.get(objectId);
|
||||
if (!obj || obj.type !== 'task') return state;
|
||||
const objects = new Map(state.objects);
|
||||
objects.set(objectId, { ...obj, status });
|
||||
return { ...state, objects };
|
||||
});
|
||||
await persistObject(objectId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an object and remove it from its parent's childIds.
|
||||
*
|
||||
* @param {string} objectId
|
||||
*/
|
||||
async function removeObject(objectId) {
|
||||
const state = get({ subscribe });
|
||||
if (objectId === state.rootId) {
|
||||
throw new Error('Cannot delete root object');
|
||||
}
|
||||
|
||||
const obj = state.objects.get(objectId);
|
||||
if (!obj) return;
|
||||
|
||||
// Find parent and remove childId.
|
||||
for (const [parentId, parent] of state.objects) {
|
||||
if (parent.childIds.includes(objectId)) {
|
||||
parent.childIds = parent.childIds.filter((id) => id !== objectId);
|
||||
await persistObject(parentId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await deleteObject(objectId);
|
||||
|
||||
update((state) => {
|
||||
const objects = new Map(state.objects);
|
||||
objects.delete(objectId);
|
||||
return { ...state, objects };
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe,
|
||||
loadObjects,
|
||||
addObject,
|
||||
updateProjectName,
|
||||
updateText,
|
||||
updateTaskStatus,
|
||||
removeObject
|
||||
};
|
||||
}
|
||||
|
||||
export const objectsStore = createObjectsStore();
|
||||
Reference in New Issue
Block a user