From ea169ebed04348dddaf59114ce43ef8e65a69345 Mon Sep 17 00:00:00 2001 From: Aneurin Barker Snook Date: Fri, 4 Sep 2026 21:01:27 +0100 Subject: [PATCH] Move project renaming from the project view to configuration - ProjectView: title is now a plain

, no longer inline-editable (dropped titleDraft/saveTitle/patchProject and the input markup -- nothing else used patchProject). - ProjectConfigureView: new 'Project name' section above 'Statuses', a small PATCH /api/projects/:id form. On success it also calls the projects store's fetchProjects(), since the dashboard grid and the sidebar's project dropdown read from that store and otherwise wouldn't pick up the new name (or the project's new alphabetical position) until some unrelated reload. - style.css: dropped the now-dead .project-head__title input rules (both views render a plain heading now); .project-head__title gains word-break so a long static title still wraps instead of overflowing. Co-Authored-By: Claude Sonnet 5 --- web/README.md | 18 ++++++--- web/src/style.css | 22 +--------- web/src/views/ProjectConfigureView.vue | 56 ++++++++++++++++++++++++++ web/src/views/ProjectView.vue | 42 +------------------ 4 files changed, 71 insertions(+), 67 deletions(-) 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() {