diff --git a/web/README.md b/web/README.md index bcd2acd..2f70a70 100644 --- a/web/README.md +++ b/web/README.md @@ -99,11 +99,11 @@ ghost. The same rule covers every kanban status column too (below). `/projects/:id` shows one project. It renders on a **full-width** layout (the route sets `meta.wide`, which widens `.app__main` in `App.vue`), so the header -spans the full width and the **Manage** menu sits top right. The title is -inline-editable (saved on blur via `PATCH /api/projects/:id`). Manage -(`ProjectManageMenu.vue`) has a **Configure** link (to the status-management -view below) and a **Delete project** action that opens a confirmation modal; -confirming calls `DELETE /api/projects/:id` and returns to the dashboard. +spans the full width and the **Manage** menu sits top right. The title is a +plain heading here -- renaming lives on the configuration view (below). Manage +(`ProjectManageMenu.vue`) has a **Configure** link (to that view) and a +**Delete project** action that opens a confirmation modal; confirming calls +`DELETE /api/projects/:id` and returns to the dashboard. Below the header are two tabs (local `activeTab` state, `v-show` so both stay mounted). The tab order is fixed — **All tasks** first, **Kanban** second — but @@ -139,6 +139,14 @@ statuses. The header mirrors the project view's — title, then a alike (both `.menu__toggle`) (Manage's own Configure link is hidden here, since it would just point at the current page). +A **Project name** section (a plain `PATCH /api/projects/:id` form, +`{ title }`) sits above **Statuses**. On success it also calls the +`projects` store's `fetchProjects()` -- the local `project` ref (and this +view's own header) update from the PATCH response directly, but the +dashboard grid and the sidebar's project dropdown read from that store, so +without the extra fetch the new name (and alphabetical position -- projects +are API-ordered by title) wouldn't show up there until some other reload. + The status list is a `vuedraggable` list (its own list, no shared drag group with the kanban board) bound directly to a local `statuses` ref; dragging mutates it in place, and `@change` persists the whole new order via diff --git a/web/src/style.css b/web/src/style.css index f39cd0b..100a591 100644 --- a/web/src/style.css +++ b/web/src/style.css @@ -575,27 +575,7 @@ h1 { flex: 1; min-width: 0; margin: 0 0 0.5rem; -} - -.project-head__title input { - width: 100%; - font: inherit; - font-size: 1.4rem; - font-weight: 600; - color: var(--text); - background: transparent; - border: 1px solid transparent; - border-radius: 6px; - padding: 0.25rem 0.4rem; -} - -.project-head__title input:hover { - border-color: var(--border); -} - -.project-head__title input:focus { - color: var(--text); - background: var(--bg); + word-break: break-word; } .project-head__actions { diff --git a/web/src/views/ProjectConfigureView.vue b/web/src/views/ProjectConfigureView.vue index 65361c5..f7239ec 100644 --- a/web/src/views/ProjectConfigureView.vue +++ b/web/src/views/ProjectConfigureView.vue @@ -4,10 +4,12 @@ import { useRoute } from 'vue-router' import draggable from 'vuedraggable' import ProjectManageMenu from '../components/ProjectManageMenu.vue' import { ApiError, apiRequest } from '../lib/api' +import { useProjectsStore } from '../stores/projects' import type { CardStatus, Project } from '../types' const route = useRoute() const projectId = Number(route.params.id) +const projects = useProjectsStore() const project = ref(null) const statuses = ref([]) @@ -25,6 +27,7 @@ async function load() { ]) project.value = fetched statuses.value = fetchedStatuses + nameDraft.value = fetched.title } catch (e) { if (e instanceof ApiError && e.status === 404) { loadError.value = 'That project does not exist.' @@ -34,6 +37,37 @@ async function load() { } } +// --- Project name --------------------------------------------------------- +const nameDraft = ref('') +const renaming = ref(false) +const renameError = ref(null) + +async function onRenameProject() { + if (!project.value) return + const next = nameDraft.value.trim() + if (next === project.value.title) return + + renaming.value = true + renameError.value = null + try { + const { project: updated } = await apiRequest<{ project: Project }>(`/projects/${projectId}`, { + method: 'PATCH', + auth: true, + body: { title: next }, + }) + project.value = updated + nameDraft.value = updated.title + // The dashboard grid and the sidebar's project dropdown both read from + // this store -- re-fetch so the new name (and alphabetical position) + // show up there too, not just in this view's own header. + await projects.fetchProjects() + } catch (e) { + renameError.value = e instanceof ApiError ? e : new ApiError('Could not save the name.', 0) + } finally { + renaming.value = false + } +} + // --- Reorder (drag-and-drop) ---------------------------------------------- // vuedraggable mutates `statuses` in place as the user drags; @change fires // once the drop lands, and we persist the whole new order. @@ -161,6 +195,28 @@ onBeforeUnmount(() => window.removeEventListener('keydown', onKeydown)) +
+

Project name

+ +
+ + +

+ {{ renameError.message }} +

+ + +
+
+

Statuses

diff --git a/web/src/views/ProjectView.vue b/web/src/views/ProjectView.vue index 3af78c9..69eb28c 100644 --- a/web/src/views/ProjectView.vue +++ b/web/src/views/ProjectView.vue @@ -28,8 +28,6 @@ const tabs = [ ] as const const activeTab = ref<(typeof tabs)[number]['key']>('kanban') -const titleDraft = ref('') - const newText = ref('') const submitting = ref(false) @@ -96,10 +94,6 @@ async function onColumnChange(change: ColumnChange, column: Column) { } } -watch(project, (value) => { - if (value) titleDraft.value = value.title -}) - onMounted(() => void load()) async function load() { @@ -122,31 +116,6 @@ async function load() { } } -async function patchProject(fields: { title?: string }) { - actionError.value = null - try { - const { project: updated } = await apiRequest<{ project: Project }>(`/projects/${projectId}`, { - method: 'PATCH', - auth: true, - body: fields, - }) - project.value = updated - } catch (e) { - actionError.value = e instanceof ApiError ? e.message : 'Could not save the change.' - if (project.value) titleDraft.value = project.value.title - } -} - -function saveTitle() { - if (!project.value) return - const next = titleDraft.value.trim() - if (next === '') { - titleDraft.value = project.value.title // title is required - return - } - if (next !== project.value.title) void patchProject({ title: next }) -} - /** Run a store mutation, surfacing failures and resyncing from the server. */ async function run(op: Promise) { actionError.value = null @@ -178,16 +147,7 @@ async function onCreate() {