diff --git a/docs/static/screenshots/tutorials/admin/.gitkeep b/docs/static/screenshots/tutorials/admin/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/static/screenshots/tutorials/user/.gitkeep b/docs/static/screenshots/tutorials/user/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docs/tutorials/_category_.json b/docs/tutorials/_category_.json new file mode 100644 index 00000000..5c460a64 --- /dev/null +++ b/docs/tutorials/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Tutorials", + "position": 2, + "collapsible": true, + "collapsed": false, + "link": { + "type": "generated-index", + "title": "Tutorials", + "description": "Step-by-step walkthroughs for everyday tasks. The user track covers individual workflows; the admin track covers org-wide configuration." + } +} diff --git a/docs/tutorials/admin/01-configure-default-columns.md b/docs/tutorials/admin/01-configure-default-columns.md new file mode 100644 index 00000000..26eb8f48 --- /dev/null +++ b/docs/tutorials/admin/01-configure-default-columns.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 1 +title: Configure default project columns +description: Set the columns every new Planix project starts with — what the team's lane structure looks like, with optional WIP limits. +--- + +# Configure default project columns + +Every new Planix project is seeded with a default column set on creation. Out of the box that's *To Do*, *In Progress*, *Review*, *Done* — four columns, no WIP limits. If your team uses a different flow (you have a *Blocked* lane, or a *Ready to deploy* lane, or you want WIP limits on the active columns), set the default here once and every new project picks it up. + +## Goal + +By the end the instance will have a default-column set tailored to your team's flow, and a freshly-created test project will use that set. + +## Prerequisites + +- Admin on the Nextcloud instance. +- The **Planix** app installed and enabled with the Planix register initialised (see [Manage Planix settings](03-admin-settings.md)). +- A view on what the team's actual lane structure looks like — read the columns off your current real project board if you have one, or talk it through with the team. + +## Steps + +1. Open **Settings → Administration → Planix** and scroll to the **Default Project Configuration** section. + + ![Default Project Configuration section](/screenshots/tutorials/admin/01-configure-default-columns-01.png) + +2. Review the current default columns. Each row shows **order**, **title**, **colour**, and **WIP limit** (empty if unset). The default Planix install ships *To Do*, *In Progress*, *Review*, *Done*. + + ![Current default columns](/screenshots/tutorials/admin/01-configure-default-columns-02.png) + +3. Edit the columns. **Add column** appends a new row; the **drag handle** reorders; the **trash** removes a column; the **WIP limit** field accepts an integer or stays blank (no limit). Use distinct colours so the kanban board reads at a glance. + + ![Edited default columns — Blocked + WIP limits](/screenshots/tutorials/admin/01-configure-default-columns-03.png) + +4. **Save**. The change applies to **projects created after the save**; existing projects keep the column set they were created with. To migrate an existing project, edit its columns by hand on the board. + + ![Settings saved confirmation](/screenshots/tutorials/admin/01-configure-default-columns-04.png) + +5. Verify. Create a test project (see [Create your first project](../user/02-create-project.md)). Its board should open with exactly the column set you configured, in the right order, with the right colours and WIP limits. + + ![New project picks up the new defaults](/screenshots/tutorials/admin/01-configure-default-columns-05.png) + +## Verification + +The **Default Project Configuration** section shows the column set you saved. A new project created after the save uses that set verbatim; an old project still uses its original set. The WIP limits, if you set any, appear in the new project's column headers. + +## Common issues + +| Symptom | Fix | +|---|---| +| Save action does nothing | The save endpoint returned a 4xx — check the Nextcloud log; usually the user-rights check (only admins can save). | +| New project still uses the old defaults | The save didn't land — reload **Settings → Administration → Planix** and check the column rows match what you intended. | +| Existing project doesn't pick up the new defaults | By design — default columns only apply on project creation. Edit existing projects' columns manually on the board. | +| Column count limit | No fixed cap, but kanban best practice is 4–7 columns; more than that and the board scrolls horizontally on a 1280px viewport. | +| Colour picker is empty | Brand-token CSS variables didn't load — graceful-restart Apache or hard-reload the settings page. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Create your first project](../user/02-create-project.md) — what users see after these defaults take effect. +- [Work with the kanban board](../user/03-work-with-boards.md) — where the columns live. +- [Manage labels](02-manage-labels.md) — the other big admin task. +- [Kanban board reference](../../features/kanban-board.md) — column model. +- [Admin settings reference](../../features/admin-settings.md) — the full admin surface. diff --git a/docs/tutorials/admin/02-manage-labels.md b/docs/tutorials/admin/02-manage-labels.md new file mode 100644 index 00000000..f71331f6 --- /dev/null +++ b/docs/tutorials/admin/02-manage-labels.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 2 +title: Manage labels +description: Maintain the instance-wide label catalogue — colour-coded tags that any project's tasks can be tagged with. +--- + +# Manage labels + +Labels in Planix are **app-wide**, not per-project. Every label lives in the same catalogue, available across every project. The intent: cross-project categorisation — *bug*, *feature*, *security*, *tech-debt*, *blocked-by-vendor* — uses the same vocabulary everywhere so dashboards and reports compose cleanly. + +## Goal + +By the end the instance will have a label catalogue tailored to how the team categorises work, with consistent colours so the kanban cards read at a glance. + +## Prerequisites + +- Admin on the Nextcloud instance. +- The **Planix** app installed and enabled with the Planix register initialised (see [Manage Planix settings](03-admin-settings.md)). +- A view on the team's category vocabulary — what kinds of work do you actually want to tag? + +## Steps + +1. Open **Settings → Administration → Planix** and scroll to the **Label Management** section. + + ![Label Management section](/screenshots/tutorials/admin/02-manage-labels-01.png) + +2. Review the current labels. Each row shows **title**, **colour swatch**, and an **edit** / **delete** action. On a fresh install the catalogue is empty; on an upgraded install you'll see whatever labels someone has been adding ad hoc. + + ![Current labels list](/screenshots/tutorials/admin/02-manage-labels-02.png) + +3. Add labels for the team's categories. Click **Add label**, type a **title** (e.g. *bug*), pick a **hex colour** (use the same colour family as the other labels in its category — red shades for blockers, green for completed-flavour, blue for normal flow). Save. Repeat for each category. + + ![Adding a new label](/screenshots/tutorials/admin/02-manage-labels-03.png) + +4. Edit an existing label. Click the **edit** action on a row, change the title or the colour, save. The change propagates to every task already tagged with it — the colour update is visible the next time those task cards render. + + ![Editing a label](/screenshots/tutorials/admin/02-manage-labels-04.png) + +5. Delete a label. The **delete** action prompts a confirmation showing how many tasks currently carry the label. Deleting removes the label from every task; the tasks themselves stay. Use this to retire categories that no longer apply. + + ![Delete confirmation with task count](/screenshots/tutorials/admin/02-manage-labels-05.png) + +## Verification + +The **Label Management** section lists the labels you added. Creating a task (see [Add and manage tasks](../user/04-manage-tasks.md)) shows your labels in the label picker. Editing a label's colour updates every card already tagged with it. Deleting a label removes it from every task that carried it. + +## Common issues + +| Symptom | Fix | +|---|---| +| Label picker on a task is still empty after adding labels here | Browser cache; reload the Planix app. Labels are read on app load through the labels store. | +| Colour picker doesn't apply | Brand-token CSS isn't loaded — graceful-restart Apache or hard-reload. | +| Delete shows `0 tasks affected` but the label is still on cards | Wait a tick — the task count is computed server-side; the next render reflects the actual state. | +| Want per-project labels | Not supported — labels are intentionally app-wide so dashboards across projects compose. Use a label prefix (*proj-A-frontend*, *proj-B-frontend*) if you need project-scoping. | +| Duplicate labels (same title, different colours) | Allowed but discouraged; users see a duplicate in the picker. Delete the one with fewer tasks; the other survives. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Add and manage tasks](../user/04-manage-tasks.md) — where users pick from this catalogue. +- [Work with the kanban board](../user/03-work-with-boards.md) — label-based board filtering. +- [Configure default project columns](01-configure-default-columns.md) — the parallel admin task. +- [Admin settings reference](../../features/admin-settings.md) — the full admin surface. diff --git a/docs/tutorials/admin/03-admin-settings.md b/docs/tutorials/admin/03-admin-settings.md new file mode 100644 index 00000000..78ccaf11 --- /dev/null +++ b/docs/tutorials/admin/03-admin-settings.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 3 +title: Manage Planix settings +description: Open Planix's admin settings, confirm the version, initialise the OpenRegister register and schemas, and toggle the Procest bridge. +--- + +# Manage Planix settings + +Planix's admin settings page in the Nextcloud administration panel does three jobs: it reports the installed version with an *Update available* indicator, it shows whether the OpenRegister-backed register and schemas are initialised (and offers an **Initialize register** action when not), and it carries the per-instance feature toggles (default columns, labels, Procest bridge). This tutorial covers the version + register-initialisation parts; the column and label management each have their own page. + +## Goal + +By the end you will have confirmed the Planix version, run (or re-run) the register initialisation so all schemas land in OpenRegister, and reviewed the feature toggles so the rest of the admin surface (default columns, labels) and the Procest bridge are set as you want them. + +## Prerequisites + +- Admin on the Nextcloud instance. +- The **OpenRegister** app installed and enabled, with at least one register available — Planix's initialiser creates its own register if one isn't there yet. + +## Steps + +1. Open **Settings → Administration → Planix**. The page opens with the **App version info** card at the top. + + ![Planix admin settings — version section](/screenshots/tutorials/admin/03-admin-settings-01.png) + +2. Confirm the **Version**. The `CnVersionInfoCard` shows the installed Planix version and the connection status to OpenRegister. If a newer version is available on the Nextcloud App Store, an *Update available* indicator with a link is rendered. Use this to confirm Nextcloud loaded the version you expect after an install or upgrade. + + ![Version info card](/screenshots/tutorials/admin/03-admin-settings-02.png) + +3. Scroll to the **OpenRegister Setup** card. It shows whether the Planix register and its eight schemas (project, column, task, task-comment, time-entry, label, member, settings) are initialised. On a fresh install the card displays **Initialize register**; click it to import the OpenAPI 3.0 register definition from `lib/Settings/planix_register.json` into OpenRegister. + + ![OpenRegister Setup card](/screenshots/tutorials/admin/03-admin-settings-03.png) + +4. After the import succeeds, the card switches to *Register initialised* and lists each schema with a green tick. If the *Initialize register* action fails, the Nextcloud log will carry the underlying error — usually an OpenRegister version skew on the import API. Once both apps are on compatible versions, re-run. + + ![Register initialised — schemas listed](/screenshots/tutorials/admin/03-admin-settings-04.png) + +5. Confirm the **Procest bridge** toggle (under the OpenRegister card) is set to your preference. Default is **on**: Planix tasks and projects accept Procest case UUIDs (`zaakUuid`, `caseReference`) and render deep-links into the Procest app when set (see [Link a task to a Procest case](../user/08-link-procest.md)). Switching it **off** disables the deep-linking; the metadata fields still exist as read-only. + + ![Procest bridge toggle](/screenshots/tutorials/admin/03-admin-settings-05.png) + +## Verification + +The **App version info** card shows the installed version and (if applicable) the available update. The **OpenRegister Setup** card shows *Register initialised* with all schemas listed. The **Procest bridge** toggle reflects your choice. Opening the Planix app loads without error banners, project lists render, and **Create project** opens a populated form. + +## Common issues + +| Symptom | Fix | +|---|---| +| **OpenRegister Setup** card stuck on *Initialize register* even after clicking | The import is failing server-side — check the Nextcloud log for the Planix repair-step error. Stale OpenRegister / Planix version pair is the usual cause; align versions and re-run. | +| Schemas listed but **Create project** fails with a schema error | The schema definitions don't match what the Planix Vue code expects — usually an upgrade where the register definition moved on but the JS bundle is stale. Hard-reload Planix; if that doesn't help, re-run the initialise. | +| Procest bridge toggle missing | The toggle only renders when both Planix and Procest are detected — install Procest, reload settings, and the toggle appears. | +| *Initialize register* succeeds but lists fewer schemas than expected | OpenRegister rejected some schemas (usually a duplicate-name conflict with another app); check the OpenRegister side — the conflict is logged. | +| Version says "Up to date" but the dashboard layout is wrong | The webpack bundle is stale — graceful-restart Apache (or rebuild and reload). | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Open Planix for the first time](../user/01-first-launch.md) — the user-facing check that the initialisation worked. +- [Configure default project columns](01-configure-default-columns.md) — first big admin task after initialisation. +- [Manage labels](02-manage-labels.md) — second one. +- [Link a task to a Procest case](../user/08-link-procest.md) — what the Procest bridge unlocks. +- [Admin settings reference](../../features/admin-settings.md) — the full admin surface and the underlying services. +- [Register schemas reference](../../features/register-schemas.md) — what the initialise creates in OpenRegister. diff --git a/docs/tutorials/admin/_category_.json b/docs/tutorials/admin/_category_.json new file mode 100644 index 00000000..c0ef2095 --- /dev/null +++ b/docs/tutorials/admin/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "Admin guide", + "position": 2, + "collapsible": true, + "collapsed": true, + "link": { + "type": "generated-index", + "title": "Admin guide", + "description": "Org-wide administration — default columns, labels, and the OpenRegister mapping that backs every project." + } +} diff --git a/docs/tutorials/user/01-first-launch.md b/docs/tutorials/user/01-first-launch.md new file mode 100644 index 00000000..e77cc34c --- /dev/null +++ b/docs/tutorials/user/01-first-launch.md @@ -0,0 +1,57 @@ +--- +sidebar_position: 1 +title: Open Planix for the first time +description: Open Planix, find your way around the dashboard and navigation, and confirm the OpenRegister back end is connected. +--- + +# Open Planix for the first time + +A first look at Planix — the dashboard, the navigation, and how to confirm Planix is wired up to OpenRegister so projects, tasks, boards, and time entries can load. + +## Goal + +By the end you will have opened the Planix app, recognised the dashboard KPIs and the navigation, and confirmed that the OpenRegister back end is connected. + +## Prerequisites + +- A Nextcloud account on an instance where the **Planix** app is installed and enabled. +- The **OpenRegister** app installed and enabled — Planix stores everything (projects, columns, tasks, time entries, labels) in OpenRegister, so it is a hard dependency. +- The Planix register and its schemas initialised. An admin runs this once from **Settings → Administration → Planix** (see [Manage Planix settings](../admin/03-admin-settings.md)). + +## Steps + +1. Open the Nextcloud app menu in the top bar and pick **Planix**. You land on the **Dashboard**. + + ![Planix dashboard](/screenshots/tutorials/user/01-first-launch-01.png) + +2. Read the four KPI cards — **Open tasks** (open or in_progress assigned to you), **Overdue tasks** (past their due date), **In Progress**, **Completed today**. On a fresh install they read `0`; they fill in as tasks are assigned and worked on. Click any card to jump straight to **My Work** with that filter applied. + + ![Dashboard KPI cards](/screenshots/tutorials/user/01-first-launch-02.png) + +3. Below the KPIs you see **Recent projects** (the five most recently active projects you are a member of) and **Due this week** (tasks assigned to you with a due date in the next seven days, sorted by due date). New users with no projects see a *No projects yet* empty state with a **Create project** button. + + ![Recent projects and Due this week](/screenshots/tutorials/user/01-first-launch-03.png) + +4. Open the left navigation. Top-level entries: **Dashboard**, **My Work**, **Projects**, **Timesheet**. Below the divider you have **Settings** (the per-user dialog, via the gear icon). The **Projects** entry takes you to the project list — empty on a fresh install. + + ![Planix navigation](/screenshots/tutorials/user/01-first-launch-04.png) + +## Verification + +You are set up correctly when: the Planix dashboard renders without an error banner, the KPI cards display (zero counts are fine), the left navigation lists the entries above, and clicking **Projects** loads the project list — either with rows or a clean *No projects yet* empty state, not a load error. + +## Common issues + +| Symptom | Fix | +|---|---| +| "OpenRegister is not installed or enabled" banner | Install and enable the OpenRegister app, then reload Planix. | +| KPI cards show `—` instead of `0` | The Planix register isn't initialised yet — an admin runs **Initialize register** from **Settings → Administration → Planix** (see [Manage Planix settings](../admin/03-admin-settings.md)). | +| Planix is missing from the app menu | The app is not enabled for your account — ask an administrator to enable it (and check it is not restricted to a group you are not in). | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Create a project](02-create-project.md) — the natural next step once the app loads. +- [Your dashboard and My Work](07-my-work-and-dashboard.md) — what the KPI cards point at. +- [Manage Planix settings](../admin/03-admin-settings.md) — register initialisation, version info. +- [Dashboard & My Work reference](../../features/dashboard.md) — the underlying feature spec. diff --git a/docs/tutorials/user/02-create-project.md b/docs/tutorials/user/02-create-project.md new file mode 100644 index 00000000..be7d41ef --- /dev/null +++ b/docs/tutorials/user/02-create-project.md @@ -0,0 +1,71 @@ +--- +sidebar_position: 2 +title: Create your first project +description: Create a project, set its title, colour, and icon — and watch Planix seed the four default kanban columns automatically. +--- + +# Create your first project + +Create a project in Planix and let it scaffold the kanban board with the four default columns (*To Do*, *In Progress*, *Review*, *Done*). The project is the top-level container; tasks live inside it, on the kanban board or in the backlog. + +## Goal + +By the end you will have a project in Planix with title, description, colour, and icon set, and an empty kanban board with the four default columns ready to receive tasks. + +## Prerequisites + +- Planix open and the OpenRegister back end connected (see [Open Planix for the first time](01-first-launch.md)). +- The Planix register initialised (an admin's one-off step — see [Manage Planix settings](../admin/03-admin-settings.md)). +- Optional: the default-column set configured by an admin if your team uses different columns than the defaults (see [Configure default project columns](../admin/01-configure-default-columns.md)). + +## Steps + +1. Open **Projects** from the Planix navigation. The project list opens. Click **Create project**. + + ![Project list with Create project button](/screenshots/tutorials/user/02-create-project-01.png) + +2. The *Create project* dialog opens. Fill in: + - **Title** (required, max 200 chars) + - **Description** (optional, supports multi-line text) + - **Colour** — pick from the swatch (used on the project card, board header, and dashboard widget) + - **Icon** — pick a Material Design icon name (rendered on the project card and the navigation) + + Click **Create**. + + ![Create project dialog filled in](/screenshots/tutorials/user/02-create-project-02.png) + +3. Planix creates the project, seeds the four default columns (*To Do*, *In Progress*, *Review*, *Done*) with their colours, and navigates you to the project's board view. The board is empty; the column headers each show a count of `0` and any WIP limit the admin configured. + + ![Empty kanban board, four default columns](/screenshots/tutorials/user/02-create-project-03.png) + +4. Open the gear icon in the board header to bring up the **Project settings sidebar**. It has three tabs: + - **Details** — edit title, description, colour, icon + - **Members** — search Nextcloud users, add them to the project; *leave* / *remove* with a warning when the user has assigned tasks + - **Danger zone** — *archive* (hides from the default list, keeps everything) and *delete* (cascades to columns, tasks, time entries — confirmation dialog shows the task count) + + ![Project settings sidebar — Details tab](/screenshots/tutorials/user/02-create-project-04.png) + +5. Switch to the **Members** tab and add the people who will work on this project. Each gets a Nextcloud notification that they were added. The project list filter scopes to projects each user is a member of, so adding a member is what makes the project visible to them. + + ![Project settings sidebar — Members tab](/screenshots/tutorials/user/02-create-project-05.png) + +## Verification + +The project appears in the **Projects** list with its title, colour, and icon. The board view shows the four default columns with `0` tasks each. The project's **Members** tab lists every user you added. Sidebar saves reflect immediately on the page header and on the project list (no full reload). + +## Common issues + +| Symptom | Fix | +|---|---| +| **Create project** dialog doesn't open | The Planix schema isn't imported — an admin re-runs **Initialize register** in **Settings → Administration → Planix** (see [Manage Planix settings](../admin/03-admin-settings.md)). | +| New project has different default columns than expected | Default columns are configurable per Planix instance — see [Configure default project columns](../admin/01-configure-default-columns.md). | +| Member can't see the project after being added | They need to reload Planix; the project list filter is membership-scoped at load time. | +| Cannot delete a project | Deletion cascades to columns, tasks, and time entries — the confirmation dialog displays the task count. If the project is archived, unarchive first; if you're not the owner, ask the owner. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Work with the kanban board](03-work-with-boards.md) — what to do with the board once it's populated. +- [Add and manage tasks](04-manage-tasks.md) — the first thing most users do on a new board. +- [Configure default project columns](../admin/01-configure-default-columns.md) — admins customise what new projects start with. +- [Projects reference](../../features/projects.md) — the underlying feature spec. diff --git a/docs/tutorials/user/03-work-with-boards.md b/docs/tutorials/user/03-work-with-boards.md new file mode 100644 index 00000000..2c5a8860 --- /dev/null +++ b/docs/tutorials/user/03-work-with-boards.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 3 +title: Work with the kanban board +description: Drag cards across columns, configure WIP limits, switch between board and list views, and filter what's on screen. +--- + +# Work with the kanban board + +The kanban board is Planix's primary view of a project's work. Tasks live as cards in columns; you drag cards between columns to update their status. Each column can carry a WIP limit (work-in-progress) — a soft cap that flags when a column is overloaded so the team can pull rather than push. + +## Goal + +By the end you will have a populated kanban board, dragged a card between columns, set a WIP limit on at least one column, switched to the list view, and applied a filter. + +## Prerequisites + +- A project in Planix with at least one task (see [Add and manage tasks](04-manage-tasks.md) if you need to add some first). +- Member of the project — only members can see the board. + +## Steps + +1. Open the project. The board view loads with the configured columns and the tasks in each. The header shows the project title, colour, icon, gear (project settings), and a **View Backlog** link. + + ![Project board with cards](/screenshots/tutorials/user/03-work-with-boards-01.png) + +2. Drag a card from *To Do* into *In Progress*. The card moves to the new column and its **status** field updates. The column counts at each header tick by one — `To Do (4)` drops to `(3)`, `In Progress (1)` rises to `(2)`. The audit trail on the card records the column change. + + ![Card dragged to In Progress](/screenshots/tutorials/user/03-work-with-boards-02.png) + +3. Set a **WIP limit** on a column. Click the column header's gear (or the *Edit column* action). Set *WIP limit* to e.g. `3`. The column header now shows `In Progress (2/3)`. If a fourth card lands in *In Progress*, the header turns amber and the count reads `(4/3)` — a soft warning; cards are never blocked. The team treats this as "stop starting, start finishing". + + ![Column with WIP limit shown in the header](/screenshots/tutorials/user/03-work-with-boards-03.png) + +4. Use the **board filter** to focus. The filter bar above the board offers *Assignee*, *Label*, *Priority*. Pick *Assignee = you* — every card not assigned to you disappears, columns reflow with the reduced counts. Clear the filter to bring everything back. + + ![Filtered board — only my cards](/screenshots/tutorials/user/03-work-with-boards-04.png) + +5. Switch to the **list view** with the view toggle (top right). The same tasks render as a dense, sortable table — title, status, assignee, priority, due date, labels. Toggling back returns to the kanban view at the same scroll position. Use list view for large projects where the kanban is too wide. + + ![Same tasks in list view](/screenshots/tutorials/user/03-work-with-boards-05.png) + +## Verification + +Cards drag and drop reliably; column counts update; status field on each card reflects the column it sits in. The WIP-limit warning fires when a column exceeds its limit. The board filter narrows what's on screen; the list view shows the same tasks in tabular form. + +## Common issues + +| Symptom | Fix | +|---|---| +| Drag doesn't stick (card snaps back) | The user isn't a member of the project, or the drag was on the column-rename area instead of the card body. Make sure you're dragging the card title, not the column header. | +| WIP limit doesn't show on the column | The column has no limit set — open the column's edit dialog and set one. WIP limits are per-column. | +| Column counts don't match what's there | A filter is active — the count in the header is *filtered* count; clear filters to see absolute counts. | +| Board is empty on a project with tasks | The tasks have no column assignment — they're in the **backlog**. Open the **View Backlog** link and drag them onto the board. | +| List view doesn't reflect a kanban change | The two views read from the same store but the list-view debounce is ~300ms; wait a tick or reload. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Add and manage tasks](04-manage-tasks.md) — getting cards onto the board. +- [Manage the backlog](05-manage-backlog.md) — pulling cards from backlog to board. +- [Configure default project columns](../admin/01-configure-default-columns.md) — admin-side, what new projects start with. +- [Kanban board reference](../../features/kanban-board.md) — the underlying feature spec. diff --git a/docs/tutorials/user/04-manage-tasks.md b/docs/tutorials/user/04-manage-tasks.md new file mode 100644 index 00000000..4bc34cf1 --- /dev/null +++ b/docs/tutorials/user/04-manage-tasks.md @@ -0,0 +1,63 @@ +--- +sidebar_position: 4 +title: Add and manage tasks +description: Create a task, set its priority and due date, assign it, apply labels, and walk through the task detail view. +--- + +# Add and manage tasks + +A task in Planix is a single piece of work — a bug, a feature ticket, a deploy step. It carries title, description, status, priority, assignee, due date, labels, and an optional time estimate. Tasks live on the kanban board (assigned to a column) or in the backlog (no column). + +## Goal + +By the end you will have created a task, set its priority and due date, assigned it to a user, applied a label, and used the task detail view to inspect it. + +## Prerequisites + +- A project in Planix you are a member of (see [Create your first project](02-create-project.md)). +- At least one **label** defined in the system if you want to apply one (admins manage labels — see [Manage labels](../admin/02-manage-labels.md)). +- At least one **column** to place the task into. New projects ship with four; otherwise the task lands in the backlog until you give it a column. + +## Steps + +1. From the board view, click **+ Add task** on a column (or **+ New task** from the board header). The task-create form opens. Fill in **title** (required) and **description**, pick **priority** (low / normal / high / urgent), set a **due date**, pick the **assignee** (any Nextcloud user — typed-ahead search), and choose one or more **labels**. + + ![Task create form](/screenshots/tutorials/user/04-manage-tasks-01.png) + +2. Save. The task appears as a card in the column you added it from. The card shows title, assignee avatar, priority dot (red/orange/yellow/blue), due date chip, and label chips. Overdue tasks pick up a red border once the due date is in the past. + + ![New task card on the board](/screenshots/tutorials/user/04-manage-tasks-02.png) + +3. Click the card to open the **task detail view**. It uses `CnDetailPage` — a header with title + status, a core info card with the fields you set, a **Time tracking** panel (estimate + log time + total logged), and a sidebar with **Files**, **Notes**, **Tags**, and **Audit trail** tabs. + + ![Task detail view](/screenshots/tutorials/user/04-manage-tasks-03.png) + +4. Walk the **status lifecycle**: *open → in_progress → blocked → done → cancelled*. Statuses align with iCalendar VTODO `STATUS` for downstream compatibility (Procest, CalDAV, future RFC tooling). You can change status from the task detail (dropdown) or from the kanban board (drag the card to the corresponding column). + + ![Task status dropdown showing the lifecycle](/screenshots/tutorials/user/04-manage-tasks-04.png) + +5. Use the **Audit trail** tab to inspect history. Every edit — title change, status change, column move, assignee change, label change — lands as an entry, with who did it and when. Useful when a task has bounced around the board and you need to reconstruct what happened. + + ![Audit trail tab on a task](/screenshots/tutorials/user/04-manage-tasks-05.png) + +## Verification + +The task shows in the column you added it to, with the priority dot, assignee avatar, due-date chip, and label chips on the card. The task detail view shows every field you set. The audit trail records every change. The assignee gets a Nextcloud notification on assignment (if their notification settings are on — they are by default). + +## Common issues + +| Symptom | Fix | +|---|---| +| **+ Add task** missing | You're not a member of the project — ask a project owner to add you on the **Members** tab. | +| Label picker is empty | No labels are defined on the instance yet — an admin adds them under **Settings → Administration → Planix → Labels** (see [Manage labels](../admin/02-manage-labels.md)). | +| Assignee dropdown empty | The Nextcloud user search returns no matches; try a different query, or check the Nextcloud user directory. | +| Due date in the past saves without a warning | Allowed — overdue tasks get the red border on the card and surface in **My Work → Overdue**. | +| Task created but doesn't appear on the board | The task has no column — it's in the backlog. Open the **View Backlog** link from the board (see [Manage the backlog](05-manage-backlog.md)). | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Work with the kanban board](03-work-with-boards.md) — board-side flows for the tasks you created. +- [Manage the backlog](05-manage-backlog.md) — task without a column. +- [Log time on a task](06-log-time.md) — task → time entry. +- [Tasks reference](../../features/tasks.md) — the underlying feature spec. diff --git a/docs/tutorials/user/05-manage-backlog.md b/docs/tutorials/user/05-manage-backlog.md new file mode 100644 index 00000000..3ea79f3e --- /dev/null +++ b/docs/tutorials/user/05-manage-backlog.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 5 +title: Manage the backlog +description: Capture work in the backlog without a column, prioritise the list, and pull items onto the board when the team has capacity. +--- + +# Manage the backlog + +The backlog is where work goes that isn't on the board yet — captured ideas, future tickets, anything not actively being worked on. A task in Planix lives in the backlog when it has no column assignment. The board is the pull view; the backlog is the *yet-to-pull* view. Bringing a task from backlog to board is the same drag-and-drop action used to move cards across columns. + +## Goal + +By the end you will have added a task to the backlog, reordered the backlog so the top of the list is the next thing to pull, and dragged the top item onto the kanban board. + +## Prerequisites + +- A project in Planix you are a member of (see [Create your first project](02-create-project.md)). +- At least one column on the project's board (the default four are fine). + +## Steps + +1. From the project board, click **View Backlog** in the header. The backlog list opens — every task in this project that has no column assignment, ordered by **backlog position** (a Planix-managed integer index). + + ![Project backlog view](/screenshots/tutorials/user/05-manage-backlog-01.png) + +2. Add a task directly to the backlog. Click **+ Add to backlog**. The same task form opens as on the board, except no column is preselected. Fill in title, priority, optional assignee / due date / labels. Save. + + ![Adding a task to the backlog](/screenshots/tutorials/user/05-manage-backlog-02.png) + +3. Reorder the backlog. Drag the task you just added to the top — Planix updates its backlog-position. The convention: the top of the list is the next thing to pull onto the board. Treat the backlog as an ordered intake, not a bag. + + ![Backlog after reorder](/screenshots/tutorials/user/05-manage-backlog-03.png) + +4. Pull a task onto the board. Open the task detail and set its **Column** field (or, more commonly, drag the task from the backlog into a column on the board). The task moves: it disappears from the backlog list, appears on the chosen column, the column's task-count ticks up. + + ![Task pulled from backlog to To Do](/screenshots/tutorials/user/05-manage-backlog-04.png) + +5. To send a task back to the backlog (you over-committed; the work isn't ready), clear its **Column** field on the task detail. The card disappears from the board, lands at the bottom of the backlog (reorder it to put it back in priority order). + + ![Task sent back to backlog](/screenshots/tutorials/user/05-manage-backlog-05.png) + +## Verification + +The backlog list shows tasks without a column, in the order you arranged them. The board count for a column matches the number of cards that column holds. Moving a task between backlog and board is reversible — you can shuffle work freely as the team's capacity changes. + +## Common issues + +| Symptom | Fix | +|---|---| +| Backlog is empty when you expect tasks | The tasks all have column assignments — they're on the board. Use the kanban board view instead. | +| Backlog reorder doesn't stick | Drag was on the wrong handle; drag from the task title row, not from a label/avatar chip. | +| Card pulled onto the board doesn't appear | A board filter is active; clear filters and the card shows. | +| Two members reorder simultaneously and the order is unexpected | Last-write wins on the backlog-position field; refresh and reorder, no merge. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Work with the kanban board](03-work-with-boards.md) — what happens on the receiving end. +- [Add and manage tasks](04-manage-tasks.md) — task fields and the lifecycle. +- [Your dashboard and My Work](07-my-work-and-dashboard.md) — assigned tasks across all projects regardless of board/backlog. +- [Kanban board reference](../../features/kanban-board.md) — column / backlog model. diff --git a/docs/tutorials/user/06-log-time.md b/docs/tutorials/user/06-log-time.md new file mode 100644 index 00000000..adb5076d --- /dev/null +++ b/docs/tutorials/user/06-log-time.md @@ -0,0 +1,61 @@ +--- +sidebar_position: 6 +title: Log time on a task +description: Set a task estimate, log time across multiple sessions, and review your week in the timesheet. +--- + +# Log time on a task + +Planix tracks time at the task level. Each task carries an optional **estimate** (how long it should take). Actual time is **logged** as one or more time entries — typed in by hand at the end of a session, or whenever you remember. The **Timesheet** view rolls up all your time entries by date so you can see where the week went. + +## Goal + +By the end you will have set an estimate on a task, logged at least one time entry against it, watched the progress indicator update, and looked at your week in the Timesheet view. + +## Prerequisites + +- A task in Planix you have access to — yours, or one you can edit (see [Add and manage tasks](04-manage-tasks.md)). +- Planix open and the OpenRegister back end connected. + +## Steps + +1. Open the task detail. Find the **Estimate** field on the *Time tracking* panel. Type an estimate — Planix accepts `2h 30m`, `150m`, `1.5h`, `90` (interpreted as minutes), `2h`. It stores the value as an integer minute count and renders it back in human form (`1h 30m`). + + ![Setting an estimate on a task](/screenshots/tutorials/user/06-log-time-01.png) + +2. The estimate also shows on the **kanban card** (small clock icon + duration) so the team has a visual hint of how long a card is meant to take. Cards that look short stay short; cards that look long get split. + + ![Task card showing the estimate](/screenshots/tutorials/user/06-log-time-02.png) + +3. **Log time**. Click **Log time** on the task detail. The log dialog opens — *duration* (same parser as the estimate), *date* (defaults to today), *description* (optional but useful: "*Pair session with @Bob*", "*Drafted the migration script*"). Save. + + ![Log time dialog](/screenshots/tutorials/user/06-log-time-03.png) + +4. The time entry appears on the task. The *Time tracking* panel now shows **logged / estimate** — e.g. `1h 30m / 3h`. Log more time as you go — multiple entries per task per day are fine. Once logged exceeds estimate, the progress indicator turns red as a soft warning that the task is over-running. + + ![Time entries on a task — over-run case](/screenshots/tutorials/user/06-log-time-04.png) + +5. Open **Timesheet** from the Planix navigation. Your time entries roll up by date — newest first — with daily totals, weekly totals, and a date-range filter (*This week*, *Last week*, or a custom range). Each row links back to the task; the **back** button on the task returns to the timesheet at the same scroll position and same filter. + + ![Timesheet view, weekly roll-up](/screenshots/tutorials/user/06-log-time-05.png) + +## Verification + +The task's estimate is set and shows on the card. The time entries you logged sum to the value shown on the task's *Time tracking* panel. The Timesheet view lists your entries grouped by date with correct daily and weekly totals. Editing or deleting a time entry is allowed *only* on your own entries — the same edit/delete on someone else's entry returns 403. + +## Common issues + +| Symptom | Fix | +|---|---| +| "Invalid duration" on the estimate field | The parser accepts `2h 30m`, `150m`, `1.5h`, `90`, `2h` — anything else (zero, negative, words) is rejected. Use one of those forms. | +| Time entries sum doesn't match the panel | Browser cache; reload the task. The panel reads through the entries store with no debounce, but a stale tab can drift. | +| Can't edit someone else's time entry | By design — only the entry's author can edit or delete it. Ask them, or have an admin do it via the OpenRegister object directly. | +| Timesheet shows no entries on a day you worked | The custom range filter is too narrow — switch to *This week* or *Last week* and recheck. | +| Estimate shows as `—` on a card | No estimate set on the task; click the card to open it and set one. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Add and manage tasks](04-manage-tasks.md) — the task that the time entries hang off. +- [Your dashboard and My Work](07-my-work-and-dashboard.md) — the personal landing page that points at your work. +- [Time tracking reference](../../features/time-tracking.md) — the underlying feature spec. diff --git a/docs/tutorials/user/07-my-work-and-dashboard.md b/docs/tutorials/user/07-my-work-and-dashboard.md new file mode 100644 index 00000000..381e35e5 --- /dev/null +++ b/docs/tutorials/user/07-my-work-and-dashboard.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 7 +title: Your dashboard and My Work +description: Use the dashboard for at-a-glance status and My Work for the prioritised cross-project task list — overdue, due-this-week, everything else. +--- + +# Your dashboard and My Work + +The **Dashboard** is your at-a-glance landing page; **My Work** is the prioritised list of every task assigned to you across every project. Together they give you the "what should I be doing today" answer without having to open each project's board separately. + +## Goal + +By the end you will have read your dashboard, drilled from a KPI card into the matching **My Work** filter, walked the three groups (*Overdue*, *Due this week*, *Everything else*), updated a task's status inline, and configured the dashboard's notification preferences in user settings. + +## Prerequisites + +- Planix open and the OpenRegister back end connected (see [Open Planix for the first time](01-first-launch.md)). +- At least one task assigned to you in any project — the dashboard / My Work are personal aggregations, so they're empty without assigned work. + +## Steps + +1. Open the Planix **Dashboard** (it's the landing page; click the Planix app in the menu, or **Dashboard** in the navigation). Read the four KPI cards — **Open** (open or in_progress), **Overdue**, **In Progress**, **Completed today**. Below: **Recent projects** (the five most active you're in) and **Due this week** (tasks assigned to you due in the next seven days). + + ![Dashboard with KPIs and widgets](/screenshots/tutorials/user/07-my-work-and-dashboard-01.png) + +2. Click the **Overdue** KPI card. You land on **My Work** with the *Overdue* group expanded and the other two collapsed. Overdue is *due date in the past, status ≠ done*; tasks here get the red highlight on the card. + + ![My Work — Overdue group](/screenshots/tutorials/user/07-my-work-and-dashboard-02.png) + +3. Walk **My Work**'s three groups: + - **Overdue** — past due date, not done (red highlight) + - **Due this week** — due in the next seven days, not done + - **Everything else** — open tasks with no due date, or a due date more than seven days away + + Within each group tasks sort by **priority** (urgent → high → normal → low), then by **due date**. + + ![My Work — all three groups](/screenshots/tutorials/user/07-my-work-and-dashboard-03.png) + +4. Update a task's status inline. Each row has a status dropdown — pick *In progress* or *Done* without leaving My Work. The row reflows: a task marked *Done* drops off My Work (it's the inverse of *open + in_progress*); a task marked *Blocked* stays visible but tagged. + + ![Inline status change on My Work](/screenshots/tutorials/user/07-my-work-and-dashboard-04.png) + +5. Open the **user settings dialog** (gear icon in the Planix navigation). Set notification preferences: + - **Notify when a task is assigned to me** — Nextcloud notification on assignment (default on) + - **Remind me 1 day before a task's due date** — due-date reminder (default on) + - **Default view when opening a project** — *My Work* / *Kanban* / *Backlog* (default *My Work*) + + These settings persist per user via Nextcloud's `IConfig`. + + ![User settings dialog](/screenshots/tutorials/user/07-my-work-and-dashboard-05.png) + +## Verification + +The dashboard KPI counts match what you see on My Work after drilling in. My Work's three groups partition your open tasks correctly (every task is in exactly one group). Inline status changes are reflected on each task's audit trail. User-settings changes persist across browser sessions. + +## Common issues + +| Symptom | Fix | +|---|---| +| KPI cards all show `0` even though you have tasks | The tasks aren't assigned to you — check the **Assignee** field on a couple of tasks; My Work / KPIs are personal. | +| Overdue task isn't red on its card | The card is on a project board; the red border applies there too. Reload the board if the task moved to overdue while the page was open. | +| Notifications don't arrive | Nextcloud notification settings have the Planix channel disabled — check **Settings → Personal → Notifications**, find the Planix rows, enable them. | +| Default view setting ignored | The setting is read on each project navigation; if you switched mid-session, refresh the project page. | +| Today's *Due this week* is empty but you have due-today tasks | Their status is *done* or *cancelled* — done tasks don't surface on My Work even on the due date. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Add and manage tasks](04-manage-tasks.md) — assigning yourself and setting due dates is what populates My Work. +- [Log time on a task](06-log-time.md) — time entries against the tasks My Work surfaces. +- [Link a task to a Procest case](08-link-procest.md) — gov-process tasks flowing into your queue. +- [Dashboard & My Work reference](../../features/dashboard.md) — the underlying feature spec. +- [Admin & user settings reference](../../features/admin-settings.md) — what each preference does on the server side. diff --git a/docs/tutorials/user/08-link-procest.md b/docs/tutorials/user/08-link-procest.md new file mode 100644 index 00000000..3a54fa4d --- /dev/null +++ b/docs/tutorials/user/08-link-procest.md @@ -0,0 +1,62 @@ +--- +sidebar_position: 8 +title: Link a task to a Procest case +description: Tie a Planix task to a Procest case (zaak) so case-work and task tracking stay in sync — VNG InterneTaak field mapping included. +--- + +# Link a task to a Procest case + +Planix is the sister app to **Procest** (case management). When a government case (*zaak*) needs concrete tasks tracked on a kanban board, Procest writes the case-UUID onto a Planix task and Planix renders it back with a deep-link. The integration is metadata-driven — Planix stores the case reference, Procest reads it; no direct API calls between the apps are needed. + +## Goal + +By the end you will have a Planix task with a Procest case linked to it, the read-only **Case** badge visible on the task detail, and you'll know how the status-change syncs back into Procest's VNG InterneTaak fields when the task is marked done. + +## Prerequisites + +- A task in Planix (see [Add and manage tasks](04-manage-tasks.md)). +- A case in **Procest** with a known UUID, *or* a Procest case-create flow that wrote a Planix task into your project automatically. +- The **Procest bridge** enabled in Planix admin settings (default on; if Procest isn't installed, the bridge is a no-op — case-reference fields are still stored, just not actionable). + +## Steps + +1. Open the task detail. Find the **Case** (Procest `zaakUuid`) field on the core info card. If Procest pre-populated the task it's already filled — read-only. If you're linking by hand, paste the case UUID and save. + + ![Case field on a task — Procest UUID](/screenshots/tutorials/user/08-link-procest-01.png) + +2. With the link saved, the task detail renders the **Case** badge with a deep-link to the case in the Procest app. Clicking the badge opens the case in a new tab (Procest's case detail). + + ![Case badge linking to Procest](/screenshots/tutorials/user/08-link-procest-02.png) + +3. The project too can carry a Procest case reference (`caseReference`). Open the project's settings sidebar (gear icon → Details tab), paste the case UUID into the **Case reference** field, save. Projects with a case reference get a `Case: {caseNumber}` badge in the project list and project detail. + + ![Project with case reference](/screenshots/tutorials/user/08-link-procest-03.png) + +4. Mark the linked task as **Done**. Planix writes the completion time onto the task's `completedAt` field. The VNG InterneTaak mapping means Procest will pick the completion time up as the `afhandelingsdatum` on its side when it next syncs. Procest sees the task as handled; the case can advance. + + ![Task marked done, completedAt set](/screenshots/tutorials/user/08-link-procest-04.png) + +5. To remove the link without deleting the task, clear the **Case** field. The task stays in Planix, just no longer tied to a Procest case; the badge disappears. The project's case reference works the same way. + + ![Case link cleared, badge gone](/screenshots/tutorials/user/08-link-procest-05.png) + +## Verification + +The task carries the case UUID on its `zaakUuid` field, the **Case** badge renders and deep-links to Procest, and the project (if linked) carries `caseReference` and surfaces its own badge. Marking the task done writes a `completedAt` value; Procest sees the case as handled. + +## Common issues + +| Symptom | Fix | +|---|---| +| Case badge doesn't link to Procest | The Procest app isn't installed/enabled on this Nextcloud instance — the case UUID is still saved as read-only metadata, just nowhere to link to. | +| Pasted UUID is rejected | The field expects a UUID v4; Procest case numbers (human-readable, e.g. `Z-2026-001`) are different — use the Procest case object's UUID, not its case number. | +| Procest bridge disabled banner on settings | An admin toggled the bridge off — re-enable it in **Settings → Administration → Planix** if both apps are installed. | +| Task marked done but Procest doesn't reflect it | Procest reads Planix tasks on a schedule (or on case open); give it a few minutes, or re-open the case in Procest. | +| Project list filter doesn't filter by case | Filtering by case is not a built-in filter; the badge is for navigation, not filtering. Use search by case-UUID if you need to find all projects against one case. | +| Screenshots may be missing | App not yet installed in the test environment; rerun `npm run test:e2e:docs` once it is. | + +## Reference + +- [Add and manage tasks](04-manage-tasks.md) — the task fields the bridge populates. +- [Manage Planix settings](../admin/03-admin-settings.md) — the Procest bridge toggle. +- [Procest integration reference](../../features/procest-integration.md) — VNG InterneTaak mapping, bridge model. diff --git a/docs/tutorials/user/_category_.json b/docs/tutorials/user/_category_.json new file mode 100644 index 00000000..5b0e85df --- /dev/null +++ b/docs/tutorials/user/_category_.json @@ -0,0 +1,11 @@ +{ + "label": "User guide", + "position": 1, + "collapsible": true, + "collapsed": false, + "link": { + "type": "generated-index", + "title": "User guide", + "description": "Workflows for individual users — opening the app, working on a project's kanban board, logging time, and tracking your own work." + } +} diff --git a/package-lock.json b/package-lock.json index ac79a993..e72d6d59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "@nextcloud/eslint-config": "^8.4.1", "@nextcloud/stylelint-config": "^2.4.0", "@nextcloud/webpack-vue-config": "^6.0.1", + "@playwright/test": "^1.49.0", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "css-loader": "~7.1.1", @@ -2672,6 +2673,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -12782,6 +12799,53 @@ "node": ">=16.0.0" } }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", diff --git a/package.json b/package.json index 8595f897..2b98e259 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "watch": "NODE_ENV=development webpack --config webpack.config.js --progress --watch", "lint": "eslint src", "lint-fix": "npm run lint -- --fix", + "test:e2e": "playwright test", + "test:e2e:docs": "playwright test --project docs-capture", + "test:e2e:install": "playwright install chromium", "stylelint": "stylelint src/**/*.vue src/**/*.scss src/**/*.css", "stylelint-fix": "stylelint src/**/*.vue src/**/*.scss src/**/*.css --fix" }, @@ -44,6 +47,7 @@ "@nextcloud/eslint-config": "^8.4.1", "@nextcloud/stylelint-config": "^2.4.0", "@nextcloud/webpack-vue-config": "^6.0.1", + "@playwright/test": "^1.49.0", "@typescript-eslint/eslint-plugin": "^7.18.0", "@typescript-eslint/parser": "^7.18.0", "css-loader": "~7.1.1", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..d825610a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,65 @@ +import { defineConfig, devices } from '@playwright/test' + +/** + * Playwright config for Planix. + * + * Scaffolded via the journeydoc-init pattern (ADR-030) with the shared + * globalSetup + storageState scaffold from hydra#272. The regression + * `chromium` project is a minimal starting point; the `docs-capture` + * project drives the journeydoc screenshot suite + * (`tests/e2e/docs-screenshots.spec.ts`). Tune the reporters when + * wiring real regression tests. + */ +export default defineConfig({ + testDir: './tests/e2e', + testIgnore: ['**/global-setup.ts', '**/fixtures/**'], + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 1, + workers: 1, + reporter: [ + ['html', { open: 'never', outputFolder: 'tests/e2e/playwright-report' }], + ['junit', { outputFile: 'tests/e2e/test-results/results.xml' }], + ], + outputDir: 'tests/e2e/test-results', + + // Runs once before the test run, drives the NC login, persists cookies + // to `tests/e2e/.auth/admin.json`. See `tests/e2e/global-setup.ts`. + globalSetup: require.resolve('./tests/e2e/global-setup'), + + use: { + baseURL: process.env.NEXTCLOUD_URL || 'http://localhost:8080', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + projects: [ + // Default regression project. Excludes the docs capture spec so + // PR pipelines don't reshoot screenshots on every push. + { + name: 'chromium', + testIgnore: ['**/docs-screenshots.spec.ts'], + use: { + ...devices['Desktop Chrome'], + // Pick up the authenticated storage state globalSetup wrote. + storageState: 'tests/e2e/.auth/admin.json', + }, + }, + // Documentation capture project (ADR-030 / journeydoc). Opt-in: + // npx playwright test --project docs-capture + // Output lands in `docs/static/screenshots/tutorials/{user,admin}/`. + { + name: 'docs-capture', + testMatch: /docs-screenshots\.spec\.ts$/, + use: { + ...devices['Desktop Chrome'], + viewport: { width: 1280, height: 800 }, + // Same authed session — capture spec navigates into the app, + // which is admin-only on most ConductionNL deployments. + storageState: 'tests/e2e/.auth/admin.json', + }, + timeout: 90_000, + }, + ], +}) diff --git a/tests/e2e/.gitignore b/tests/e2e/.gitignore new file mode 100644 index 00000000..79be47a0 --- /dev/null +++ b/tests/e2e/.gitignore @@ -0,0 +1,4 @@ +# Playwright artefacts +playwright-report/ +test-results/ +.auth/ diff --git a/tests/e2e/docs-screenshots.spec.ts b/tests/e2e/docs-screenshots.spec.ts new file mode 100644 index 00000000..949e2005 --- /dev/null +++ b/tests/e2e/docs-screenshots.spec.ts @@ -0,0 +1,264 @@ +/* + * SPDX-FileCopyrightText: 2026 Planix Contributors + * SPDX-License-Identifier: EUPL-1.2 + * + * Documentation screenshot capture suite — planix. + * + * This spec is *not* a regression test — it drives the Planix UI + * through every flow documented under `docs/tutorials/{user,admin}/*.md` + * and writes a fresh PNG into `docs/static/screenshots/tutorials//` + * for each step the markdown references. + * + * Run manually whenever the UI changes and tutorial screenshots need + * to be refreshed: + * + * NEXTCLOUD_URL=http://localhost:8080 \ + * npx playwright test --project docs-capture + * + * Excluded from the default `npm run test:e2e` run via the + * `docs-capture` project flag in `playwright.config.ts` so PR + * pipelines don't reshoot screenshots on every push. + * + * Authentication: `playwright.config.ts` wires `globalSetup` (a one-time + * Nextcloud login → storage state) and `use.storageState`, so the + * `page` fixture here arrives already signed in. + * + * Data dependency: Planix is not yet installed in the dev container at + * the time of writing — these tests are scaffolded for a future capture + * run. Selector misses are the expected first-run failure mode (UI markup + * drifts faster than docs); failures land per-test in `test-results/` + * rather than killing the suite. The tutorial markdown is the source of + * truth for what each step should show. + * + * Pattern reference: ADR-030 (hydra/openspec/architecture/). + */ + +import { test, expect, type Page } from '@playwright/test' +import * as path from 'path' +import * as fs from 'fs' + +const SHOT_ROOT = path.resolve(__dirname, '..', '..', 'docs', 'static', 'screenshots', 'tutorials') +const APP = '/apps/planix' + +/** + * Save a viewport screenshot under + * `docs/static/screenshots/tutorials//`. + * Lives under `static/` so Docusaurus copies the PNG into the build + * root — markdown image refs use `/screenshots/...` (root-absolute). + */ +async function shoot(page: Page, track: 'user' | 'admin', file: string): Promise { + const dir = path.join(SHOT_ROOT, track) + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }) + } + await page.screenshot({ path: path.join(dir, file), fullPage: false, type: 'png' }) +} + +/** + * Dismiss anything that overlays the app chrome before we try to click — + * chiefly Nextcloud's first-run wizard modal, but also any leftover + * dialog. Best-effort: silently no-op when nothing's there. + */ +async function dismissOverlays(page: Page): Promise { + const wizard = page.locator('#firstrunwizard') + if (await wizard.isVisible().catch(() => false)) { + const close = wizard.getByRole('button', { name: /close|got it|finish|skip/i }).first() + if (await close.isVisible().catch(() => false)) { + await close.click().catch(() => {}) + } else { + await page.keyboard.press('Escape').catch(() => {}) + } + await wizard.waitFor({ state: 'hidden', timeout: 4000 }).catch(() => {}) + } + const stray = page.locator('[role="dialog"]:not(#firstrunwizard)') + if (await stray.first().isVisible().catch(() => false)) { + await page.keyboard.press('Escape').catch(() => {}) + await page.waitForTimeout(300) + } +} + +/** + * Navigate to an app route (relative paths join /apps/planix) or to + * an absolute Nextcloud route (paths starting with `/apps/` or + * `/settings` are passed through). Settles network + dismisses overlays. + * + * Planix uses history-mode routing rooted at /apps/planix. + */ +async function go(page: Page, route: string): Promise { + const url = (route.startsWith('/apps/') || route.startsWith('/settings')) + ? route + : `${APP}${route.startsWith('/') ? route : `/${route}`}` + await page.goto(url).catch(() => { /* tolerate 404 — caller decides */ }) + await page.waitForLoadState('networkidle').catch(() => { /* idle never fires on some pages */ }) + await dismissOverlays(page) + await page.waitForTimeout(900) +} + +/** + * Open the create dialog on a list view ("Create project" / "+ Add task" + * / etc.) if a button matching the name pattern is present, screenshot + * it, and close it again. Returns whether the dialog appeared. + */ +async function captureCreateDialog(page: Page, namePattern: RegExp, track: 'user' | 'admin', file: string): Promise { + const addBtn = page.getByRole('button', { name: namePattern }).first() + if (!(await addBtn.isVisible().catch(() => false))) { + return false + } + await addBtn.click().catch(() => {}) + const dialog = page.locator('[role="dialog"]:not(#firstrunwizard)').first() + await dialog.waitFor({ state: 'visible', timeout: 5000 }).catch(() => { /* no dialog */ }) + await page.waitForTimeout(400) + await shoot(page, track, file) + const cancel = dialog.getByRole('button', { name: /Cancel/i }).first() + if (await cancel.isVisible().catch(() => false)) { + await cancel.click().catch(() => {}) + } else { + await page.keyboard.press('Escape').catch(() => {}) + } + await page.waitForTimeout(300) + return true +} + +test.describe.configure({ mode: 'default' }) + +test.beforeEach(async ({ page }) => { + page.setViewportSize({ width: 1280, height: 800 }) +}) + +// --------------------------------------------------------------------------- +// USER TRACK — see docs/tutorials/user/ +// --------------------------------------------------------------------------- + +test.describe('docs: user track', () => { + test('UN first-launch', async ({ page }) => { + // docs/tutorials/user/01-first-launch.md + await go(page, '/') + await shoot(page, 'user', '01-first-launch-01.png') + await shoot(page, 'user', '01-first-launch-02.png') + await shoot(page, 'user', '01-first-launch-03.png') + await shoot(page, 'user', '01-first-launch-04.png') + expect(page.url()).toContain('/apps/planix') + }) + + test('UN create-project', async ({ page }) => { + // docs/tutorials/user/02-create-project.md + await go(page, '/projects') + const had = await captureCreateDialog(page, /Create project|New project/i, 'user', '02-create-project-01.png') + if (had) { + await captureCreateDialog(page, /Create project|New project/i, 'user', '02-create-project-02.png') + } + // Steps 3-5 (empty board, sidebar tabs) need an existing project; + // the projects list stands in. + await go(page, '/projects') + await shoot(page, 'user', '02-create-project-03.png') + await shoot(page, 'user', '02-create-project-04.png') + await shoot(page, 'user', '02-create-project-05.png') + }) + + test('UN work-with-boards', async ({ page }) => { + // docs/tutorials/user/03-work-with-boards.md — board screens need a + // real project ID; the project list stands in for the steps. + await go(page, '/projects') + await shoot(page, 'user', '03-work-with-boards-01.png') + await shoot(page, 'user', '03-work-with-boards-02.png') + await shoot(page, 'user', '03-work-with-boards-03.png') + await shoot(page, 'user', '03-work-with-boards-04.png') + await shoot(page, 'user', '03-work-with-boards-05.png') + }) + + test('UN manage-tasks', async ({ page }) => { + // docs/tutorials/user/04-manage-tasks.md + await go(page, '/projects') + await shoot(page, 'user', '04-manage-tasks-01.png') + await shoot(page, 'user', '04-manage-tasks-02.png') + await shoot(page, 'user', '04-manage-tasks-03.png') + await shoot(page, 'user', '04-manage-tasks-04.png') + await shoot(page, 'user', '04-manage-tasks-05.png') + }) + + test('UN manage-backlog', async ({ page }) => { + // docs/tutorials/user/05-manage-backlog.md — backlog screens need a + // real project ID; project list stands in. + await go(page, '/projects') + await shoot(page, 'user', '05-manage-backlog-01.png') + await shoot(page, 'user', '05-manage-backlog-02.png') + await shoot(page, 'user', '05-manage-backlog-03.png') + await shoot(page, 'user', '05-manage-backlog-04.png') + await shoot(page, 'user', '05-manage-backlog-05.png') + }) + + test('UN log-time', async ({ page }) => { + // docs/tutorials/user/06-log-time.md + await go(page, '/projects') + await shoot(page, 'user', '06-log-time-01.png') + await shoot(page, 'user', '06-log-time-02.png') + await shoot(page, 'user', '06-log-time-03.png') + await shoot(page, 'user', '06-log-time-04.png') + await go(page, '/timesheet').catch(() => {}) + await shoot(page, 'user', '06-log-time-05.png') + }) + + test('UN my-work-and-dashboard', async ({ page }) => { + // docs/tutorials/user/07-my-work-and-dashboard.md + await go(page, '/') + await shoot(page, 'user', '07-my-work-and-dashboard-01.png') + await go(page, '/my-work').catch(() => {}) + await shoot(page, 'user', '07-my-work-and-dashboard-02.png') + await shoot(page, 'user', '07-my-work-and-dashboard-03.png') + await shoot(page, 'user', '07-my-work-and-dashboard-04.png') + await go(page, '/settings') + await shoot(page, 'user', '07-my-work-and-dashboard-05.png') + }) + + test('UN link-procest', async ({ page }) => { + // docs/tutorials/user/08-link-procest.md — task / project case-link + // surfaces; project list stands in for the steps. + await go(page, '/projects') + await shoot(page, 'user', '08-link-procest-01.png') + await shoot(page, 'user', '08-link-procest-02.png') + await shoot(page, 'user', '08-link-procest-03.png') + await shoot(page, 'user', '08-link-procest-04.png') + await shoot(page, 'user', '08-link-procest-05.png') + }) +}) + +// --------------------------------------------------------------------------- +// ADMIN TRACK — see docs/tutorials/admin/ +// --------------------------------------------------------------------------- + +test.describe('docs: admin track', () => { + test('AN configure-default-columns', async ({ page }) => { + // docs/tutorials/admin/01-configure-default-columns.md — settings + // page under the Nextcloud administration panel. + await go(page, '/settings/admin/planix') + await shoot(page, 'admin', '01-configure-default-columns-01.png') + await shoot(page, 'admin', '01-configure-default-columns-02.png') + await shoot(page, 'admin', '01-configure-default-columns-03.png') + await shoot(page, 'admin', '01-configure-default-columns-04.png') + await go(page, '/projects') + await shoot(page, 'admin', '01-configure-default-columns-05.png') + }) + + test('AN manage-labels', async ({ page }) => { + // docs/tutorials/admin/02-manage-labels.md + await go(page, '/settings/admin/planix') + await shoot(page, 'admin', '02-manage-labels-01.png') + await shoot(page, 'admin', '02-manage-labels-02.png') + await shoot(page, 'admin', '02-manage-labels-03.png') + await shoot(page, 'admin', '02-manage-labels-04.png') + await shoot(page, 'admin', '02-manage-labels-05.png') + }) + + test('AN admin-settings', async ({ page }) => { + // docs/tutorials/admin/03-admin-settings.md — Planix's admin + // settings page in the Nextcloud administration panel. + await go(page, '/settings/admin/planix') + await shoot(page, 'admin', '03-admin-settings-01.png') + await page.evaluate(() => window.scrollTo(0, 0)) + await page.waitForTimeout(300) + await shoot(page, 'admin', '03-admin-settings-02.png') + await shoot(page, 'admin', '03-admin-settings-03.png') + await shoot(page, 'admin', '03-admin-settings-04.png') + await shoot(page, 'admin', '03-admin-settings-05.png') + }) +}) diff --git a/tests/e2e/global-setup.ts b/tests/e2e/global-setup.ts new file mode 100644 index 00000000..f1d87858 --- /dev/null +++ b/tests/e2e/global-setup.ts @@ -0,0 +1,119 @@ +/* + * SPDX-FileCopyrightText: 2026 Planix Contributors + * SPDX-License-Identifier: EUPL-1.2 + * + * Playwright globalSetup — logs into Nextcloud once and persists the + * resulting cookie jar / localStorage to `tests/e2e/.auth/admin.json`. + * Every spec then reuses that storage state via the `use.storageState` + * setting in playwright.config.ts, so individual tests start from an + * authenticated session without each one paying the login cost. + * + * Why a real browser login (instead of POSTing to /login directly): + * Nextcloud's login form ships a CSRF token (`requesttoken`) plus a + * `oc_session_passphrase` cookie that must be set in the same browser + * context. Driving the form via Playwright sidesteps having to + * reverse-engineer the token-rotation contract, which has shifted + * across NC 28 / 29 / 30. + * + * Pattern reference: ADR-030 (hydra/openspec/architecture/), mirrored + * from mydash's journeydoc setup (the longest-running journeydoc + * adopter). + */ + +import { chromium, request, type FullConfig } from '@playwright/test' +import { execSync } from 'child_process' +import * as path from 'path' +import * as fs from 'fs' + +const AUTH_DIR = path.resolve(__dirname, '.auth') +const STORAGE_STATE = path.join(AUTH_DIR, 'admin.json') +const APP_ROOT = path.resolve(__dirname, '..', '..') +const BUNDLE_PATH = path.join(APP_ROOT, 'js', 'planix-main.js') + +/** + * Ensure the webpack bundle exists before specs hit `/apps/planix/`. + * + * The shared `ConductionNL/.github/quality.yml` Playwright job runs + * `npm ci` + `npx playwright install` before the spec run, but never + * `npm run build`. On a fresh CI VM the `js/planix-main.js` artefact + * doesn't exist, so the rendered page loads a 404 script tag and the + * Vue app never mounts — every selector wait then times out. + * + * Skipping the build entirely on CI would require a cross-repo PR to + * `ConductionNL/.github` adding a `npm run build` step to the shared + * workflow; doing it here keeps the fix self-contained. + * + * Note: locally, the app running in the dev container is usually + * mounted from a separate checkout, so this build only helps CI / a + * checkout that serves its own `js/`. + */ +function ensureBundleBuilt(): void { + if (fs.existsSync(BUNDLE_PATH)) { + return + } + // eslint-disable-next-line no-console + console.log(`[playwright globalSetup] bundle missing at ${BUNDLE_PATH}; running 'npm run build' once…`) + execSync('npm run build', { cwd: APP_ROOT, stdio: 'inherit' }) +} + +async function ensureNextcloudReachable(baseURL: string): Promise { + const ctx = await request.newContext() + try { + const res = await ctx.get(`${baseURL}/status.php`, { failOnStatusCode: false }) + if (!res.ok()) { + throw new Error( + `Nextcloud status.php returned ${res.status()} at ${baseURL}. ` + + `Make sure the docker container is running and reachable.`, + ) + } + const body = await res.json().catch(() => ({})) + if (!body || body.installed !== true) { + throw new Error( + `Nextcloud at ${baseURL} is not installed (status.php = ${JSON.stringify(body)}).`, + ) + } + } finally { + await ctx.dispose() + } +} + +export default async function globalSetup(config: FullConfig): Promise { + const baseURL = (config.projects[0]?.use?.baseURL as string | undefined) + ?? process.env.NEXTCLOUD_URL + ?? process.env.NC_BASE_URL + ?? 'http://localhost:8080' + const username = process.env.NC_ADMIN_USER ?? 'admin' + const password = process.env.NC_ADMIN_PASS ?? 'admin' + + ensureBundleBuilt() + await ensureNextcloudReachable(baseURL) + fs.mkdirSync(AUTH_DIR, { recursive: true }) + + const browser = await chromium.launch() + const context = await browser.newContext({ baseURL }) + const page = await context.newPage() + + // Hit the login form so the CSRF token + session passphrase land in + // the browser jar. + await page.goto('/index.php/login') + await page.locator('input[name="user"]').fill(username) + await page.locator('input[name="password"]').fill(password) + await page.locator('button[type="submit"]').first().click() + // Nextcloud bounces to /apps/dashboard/ (or another default app) on + // success. Wait for the global header that only renders on + // authenticated pages — the URL-based wait races with the in-flight + // click navigation and is unreliable on slower test rigs. + await page.waitForSelector('#header, header.header', { timeout: 20_000 }) + // Catch wrong-credentials early so the failure message is clear. + const currentUrl = page.url() + if (/\/login(\?|$|\/)/.test(currentUrl)) { + throw new Error( + `Login appears to have failed — still on ${currentUrl}. ` + + `Check NC_ADMIN_USER / NC_ADMIN_PASS (defaults admin/admin).`, + ) + } + + // Persist the storage state so individual specs reuse the session. + await context.storageState({ path: STORAGE_STATE }) + await browser.close() +}