@taskmark/ui 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -7
- package/app/board/page.tsx +3 -272
- package/app/page.tsx +14 -14
- package/app/setup/actions.ts +4 -4
- package/app/setup/page.tsx +12 -4
- package/bin/lib/resolve-board.mjs +50 -1
- package/bin/taskmark.js +154 -32
- package/components/board/board-screen.tsx +285 -0
- package/components/board/epic-list.tsx +2 -2
- package/components/board/overall-work-items-list.tsx +1 -1
- package/components/board/story-list.tsx +1 -1
- package/dist/prod-next/BUILD_ID +1 -1
- package/dist/prod-next/app-path-routes-manifest.json +2 -2
- package/dist/prod-next/build-manifest.json +3 -3
- package/dist/prod-next/prerender-manifest.json +3 -3
- package/dist/prod-next/react-loadable-manifest.json +2 -2
- package/dist/prod-next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/_global-error.html +1 -1
- package/dist/prod-next/server/app/_global-error.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/prod-next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/_not-found.html +1 -1
- package/dist/prod-next/server/app/_not-found.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/prod-next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/prod-next/server/app/api/taskmark-reload/route.js +1 -1
- package/dist/prod-next/server/app/board/page.js +2 -50
- package/dist/prod-next/server/app/board/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/board/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/page.js +2 -2
- package/dist/prod-next/server/app/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app/setup/page.js +2 -2
- package/dist/prod-next/server/app/setup/page.js.nft.json +1 -1
- package/dist/prod-next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/dist/prod-next/server/app-paths-manifest.json +2 -2
- package/dist/prod-next/server/chunks/192.js +49 -0
- package/dist/prod-next/server/chunks/463.js +1 -0
- package/dist/prod-next/server/chunks/487.js +1 -0
- package/dist/prod-next/server/chunks/54.js +19 -0
- package/dist/prod-next/server/chunks/57.js +8 -19
- package/dist/prod-next/server/middleware-build-manifest.js +1 -1
- package/dist/prod-next/server/middleware-react-loadable-manifest.js +1 -1
- package/dist/prod-next/server/pages/404.html +1 -1
- package/dist/prod-next/server/pages/500.html +1 -1
- package/dist/prod-next/server/server-reference-manifest.js +1 -1
- package/dist/prod-next/server/server-reference-manifest.json +1 -1
- package/dist/prod-next/static/chunks/636-626c380f021b95c8.js +1 -0
- package/dist/prod-next/static/chunks/909.932f2af3be7f1e89.js +1 -0
- package/dist/prod-next/static/chunks/app/_global-error/page-a1e3320be3da9be1.js +1 -0
- package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-a1e3320be3da9be1.js +1 -0
- package/dist/prod-next/static/chunks/app/board/page-636f60191db62f52.js +1 -0
- package/dist/prod-next/static/chunks/app/page-562a33b50da7665b.js +1 -0
- package/dist/prod-next/static/chunks/app/setup/page-4cf527b758b1d51e.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-a1e3320be3da9be1.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-a1e3320be3da9be1.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-a1e3320be3da9be1.js +1 -0
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-a1e3320be3da9be1.js +1 -0
- package/dist/prod-next/static/chunks/webpack-d33b8a6298c94a64.js +1 -0
- package/dist/public/taskmark-snapshot.json +1 -1
- package/lib/taskmark/autoconfig.ts +54 -3
- package/lib/taskmark/discover.ts +2 -1
- package/lib/taskmark/list-view-mode.ts +1 -1
- package/lib/taskmark/workspace-mode.ts +24 -0
- package/lib/taskmark/workspace.ts +5 -2
- package/package.json +4 -2
- package/public/taskmark-snapshot.json +1 -1
- package/scripts/build-static.mjs +61 -3
- package/dist/prod-next/server/chunks/318.js +0 -1
- package/dist/prod-next/server/chunks/490.js +0 -8
- package/dist/prod-next/server/chunks/616.js +0 -1
- package/dist/prod-next/server/chunks/708.js +0 -1
- package/dist/prod-next/static/chunks/51.e9139a9ea6eb36b9.js +0 -1
- package/dist/prod-next/static/chunks/app/_global-error/page-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/app/api/taskmark-reload/route-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/app/board/page-cd1d9be0c0127225.js +0 -1
- package/dist/prod-next/static/chunks/app/page-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/app/setup/page-c3003359e8becc84.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/app-error-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/forbidden-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/not-found-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/next/dist/client/components/builtin/unauthorized-d34451c04262dd7d.js +0 -1
- package/dist/prod-next/static/chunks/webpack-bfe8992962004e2b.js +0 -1
- /package/dist/prod-next/static/{wbcgYS3Jw87KqhnA9ttOL → yyRnYAFQrJjF5_b5ZIeqh}/_buildManifest.js +0 -0
- /package/dist/prod-next/static/{wbcgYS3Jw87KqhnA9ttOL → yyRnYAFQrJjF5_b5ZIeqh}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -2,12 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
Install in a Taskmark board folder (or product root with `./taskmark/`) and open the board in the browser.
|
|
4
4
|
|
|
5
|
+
## Two modes
|
|
6
|
+
|
|
7
|
+
| Mode | When | Behaviour |
|
|
8
|
+
|------|------|-----------|
|
|
9
|
+
| **Bound** | Board found in/near cwd, or `serve` / `dev` | Opens that one board. No setup wizard, no project picker. |
|
|
10
|
+
| **Workspace** | No local board, or `open` / `--workspace` | Standalone setup wizard + app-bar project picker. |
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# Global / any folder without a board → setup & pick projects
|
|
14
|
+
npx @taskmark/ui
|
|
15
|
+
# or: npx -p @taskmark/ui taskmark
|
|
16
|
+
|
|
17
|
+
# Inside a board folder (or product with ./taskmark/) → that board only
|
|
18
|
+
cd my-app-taskmark && npx taskmark
|
|
19
|
+
# or: npm i @taskmark/ui --save && npx taskmark serve
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
> **`npx taskmark` only works where `@taskmark/ui` is installed.** `taskmark` is the
|
|
23
|
+
> *bin* name, not a package name — an unrelated `taskmark` package exists on npm, and
|
|
24
|
+
> npx will fetch that one instead. Use `npx @taskmark/ui` when running from a folder
|
|
25
|
+
> with no local install, or install the CLI once (see below).
|
|
26
|
+
|
|
27
|
+
### Install the CLI globally
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install -g @taskmark/ui # published release
|
|
31
|
+
# from a clone of taskmark-frontend:
|
|
32
|
+
npm run build && npm link # local build on PATH as `taskmark`
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Then `taskmark` works anywhere: setup / project picker outside a board, bound board inside one.
|
|
36
|
+
|
|
37
|
+
`taskmark open` always forces the setup / picker, even when a board is nearby. Chosen master folders and the active project persist in cookies.
|
|
38
|
+
|
|
5
39
|
## Install and serve
|
|
6
40
|
|
|
7
41
|
```bash
|
|
8
42
|
# from nested ./taskmark/ or a dedicated *-taskmark board root
|
|
9
43
|
npm install @taskmark/ui --save
|
|
10
|
-
npx taskmark
|
|
44
|
+
npx taskmark
|
|
45
|
+
# or: npx taskmark serve
|
|
11
46
|
```
|
|
12
47
|
|
|
13
48
|
Opens [http://localhost:8275](http://localhost:8275) bound to **this** board.
|
|
@@ -24,20 +59,23 @@ Runs Next.js in development on port **8275** and refreshes the UI when board `.m
|
|
|
24
59
|
Without a local install (once published):
|
|
25
60
|
|
|
26
61
|
```bash
|
|
27
|
-
npx
|
|
62
|
+
npx @taskmark/ui
|
|
63
|
+
# or: npx -p @taskmark/ui taskmark
|
|
28
64
|
```
|
|
29
65
|
|
|
30
66
|
| Option | Meaning |
|
|
31
67
|
|--------|---------|
|
|
32
68
|
| `--port` / `-p` | Listen port (default **8275**; also `PORT` / `TASKMARK_PORT`) |
|
|
33
69
|
| `--board <path>` | Board or product root (sets `TASKMARK_BOARD`) |
|
|
70
|
+
| `--workspace` / `-w` | Force setup / multi-project mode; cannot be combined with `--board` |
|
|
34
71
|
| `--no-open` | Print the URL only |
|
|
35
72
|
|
|
36
|
-
Board resolution:
|
|
73
|
+
Board resolution (bound mode only):
|
|
37
74
|
|
|
38
75
|
1. `--board` / `TASKMARK_BOARD`
|
|
39
76
|
2. `TASKMARK_MASTER`
|
|
40
77
|
3. `TASKMARK_CWD` → npm `INIT_CWD` → `process.cwd()` — nested `./taskmark/` or flat `*-taskmark` root
|
|
78
|
+
4. Sibling `<parent>-taskmark` next to a product repo (multi-git workspaces)
|
|
41
79
|
|
|
42
80
|
### Nested vs flat boards
|
|
43
81
|
|
|
@@ -87,12 +125,14 @@ Favicon, webmanifest, and Open Graph assets ship in `public/` and are included i
|
|
|
87
125
|
|
|
88
126
|
```bash
|
|
89
127
|
npm install
|
|
90
|
-
npm run build
|
|
91
|
-
npm run serve
|
|
92
|
-
npm run dev
|
|
128
|
+
npm run build # next standalone + dist/ for the CLI
|
|
129
|
+
npm run serve # production standalone against cwd/env board
|
|
130
|
+
npm run dev # taskmark dev (Next + markdown live reload) on :8275
|
|
131
|
+
npm run dev:workspace # same, but unbound — work on the setup wizard / picker
|
|
132
|
+
npm run open # prebuilt UI in workspace mode
|
|
93
133
|
```
|
|
94
134
|
|
|
95
|
-
|
|
135
|
+
`npm run dev` / `serve` always bind to a board (sibling `taskmark-taskmark` from this repo). Bare `node ./bin/taskmark.js` uses the smart default. Use `open` / `dev:workspace` to work on the setup wizard.
|
|
96
136
|
|
|
97
137
|
## Stack
|
|
98
138
|
|
package/app/board/page.tsx
CHANGED
|
@@ -1,49 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import Link from "next/link"
|
|
3
|
-
import { redirect } from "next/navigation"
|
|
4
|
-
|
|
5
|
-
import { AppBar } from "@/components/board/app-bar"
|
|
6
|
-
import { EpicList } from "@/components/board/epic-list"
|
|
7
|
-
import { ListViewSwitcher } from "@/components/board/list-view-switcher"
|
|
8
|
-
import { OverallWorkItemsList } from "@/components/board/overall-work-items-list"
|
|
9
|
-
import { ProjectStatusMetricsStrip } from "@/components/board/project-status-metrics-strip"
|
|
10
|
-
import { StaticBoardApp } from "@/components/board/static-board-app"
|
|
11
|
-
import { TaskList } from "@/components/board/task-list"
|
|
12
|
-
import { WorkItemsList } from "@/components/board/work-items-list"
|
|
13
|
-
import { WorkItemSheetProvider } from "@/components/board/work-item-sheet"
|
|
14
|
-
import { resolveActiveProject } from "@/lib/taskmark/active-project"
|
|
15
|
-
import {
|
|
16
|
-
getActiveProjectCookie,
|
|
17
|
-
getHideCompletedCookies,
|
|
18
|
-
getMasterFoldersCookie,
|
|
19
|
-
} from "@/lib/taskmark/cookies"
|
|
20
|
-
import {
|
|
21
|
-
LIST_VIEW_LABELS,
|
|
22
|
-
boardHref,
|
|
23
|
-
parseListViewMode,
|
|
24
|
-
type ListViewMode,
|
|
25
|
-
} from "@/lib/taskmark/list-view-mode"
|
|
26
|
-
import { DEV_RELOAD_TOKEN } from "@/lib/taskmark/dev-reload-token"
|
|
27
|
-
import { loadSnapshotFromPublic } from "@/lib/taskmark/load-snapshot-server"
|
|
28
|
-
import { parseEpicsForProject } from "@/lib/taskmark/parse-epics"
|
|
29
|
-
import {
|
|
30
|
-
parseWorkItemsForEpic,
|
|
31
|
-
parseWorkItemsViewForProject,
|
|
32
|
-
} from "@/lib/taskmark/parse-flat-lists"
|
|
33
|
-
import { parseItemsForStory } from "@/lib/taskmark/parse-items"
|
|
34
|
-
import {
|
|
35
|
-
collectCompletedLeafPointSamples,
|
|
36
|
-
collectMetricLeaves,
|
|
37
|
-
computeProjectStatusMetrics,
|
|
38
|
-
} from "@/lib/taskmark/project-metrics"
|
|
39
|
-
import { isStaticRuntime } from "@/lib/taskmark/static-mode"
|
|
40
|
-
import { loadWorkspace } from "@/lib/taskmark/workspace"
|
|
1
|
+
import { BoardScreen } from "@/components/board/board-screen"
|
|
41
2
|
|
|
42
3
|
export const dynamic = "force-dynamic"
|
|
43
4
|
|
|
44
|
-
// Keep this import so `taskmark dev` can touch the token module and refresh RSC.
|
|
45
|
-
void DEV_RELOAD_TOKEN
|
|
46
|
-
|
|
47
5
|
type BoardPageProps = {
|
|
48
6
|
searchParams: Promise<{
|
|
49
7
|
view?: string | string[]
|
|
@@ -53,234 +11,7 @@ type BoardPageProps = {
|
|
|
53
11
|
}>
|
|
54
12
|
}
|
|
55
13
|
|
|
56
|
-
|
|
57
|
-
if (typeof value === "string" && value.trim()) return value.trim()
|
|
58
|
-
return null
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function headingForView(view: ListViewMode): string {
|
|
62
|
-
if (view === "overall") return "Epics"
|
|
63
|
-
return LIST_VIEW_LABELS[view]
|
|
64
|
-
}
|
|
65
|
-
|
|
14
|
+
/** Alias of `/` — kept so older links and bookmarks keep working. */
|
|
66
15
|
export default async function BoardPage({ searchParams }: BoardPageProps) {
|
|
67
|
-
|
|
68
|
-
const snapshot = loadSnapshotFromPublic()
|
|
69
|
-
return (
|
|
70
|
-
<Suspense fallback={<div className="p-8 text-sm text-muted-foreground">Loading board…</div>}>
|
|
71
|
-
<StaticBoardApp snapshot={snapshot} />
|
|
72
|
-
</Suspense>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const masters = await getMasterFoldersCookie()
|
|
77
|
-
const workspace = loadWorkspace(masters)
|
|
78
|
-
if (workspace.projects.length === 0) {
|
|
79
|
-
redirect("/setup")
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const savedActiveId = await getActiveProjectCookie()
|
|
83
|
-
const hideCompletedPrefs = await getHideCompletedCookies()
|
|
84
|
-
const activeProject = resolveActiveProject(
|
|
85
|
-
workspace.projects,
|
|
86
|
-
// Autoconfig: prefer the bound board; ignore stale active-project cookies.
|
|
87
|
-
workspace.autoconfig ? null : savedActiveId
|
|
88
|
-
)
|
|
89
|
-
if (!activeProject) {
|
|
90
|
-
redirect("/setup")
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const params = await searchParams
|
|
94
|
-
const activeView = parseListViewMode(params.view)
|
|
95
|
-
const selectedEpicId =
|
|
96
|
-
activeView === "overall" ? paramValue(params.epic) : null
|
|
97
|
-
const selectedStoryId =
|
|
98
|
-
activeView === "overall" && selectedEpicId
|
|
99
|
-
? paramValue(params.story)
|
|
100
|
-
: null
|
|
101
|
-
const selectedItemId = paramValue(params.item)
|
|
102
|
-
|
|
103
|
-
const list = parseEpicsForProject(activeProject)
|
|
104
|
-
const epicCount = list.epics.length
|
|
105
|
-
const errorCount = list.errors.length
|
|
106
|
-
|
|
107
|
-
const selectedEpic = selectedEpicId
|
|
108
|
-
? list.epics.find((e) => e.id === selectedEpicId) ?? null
|
|
109
|
-
: null
|
|
110
|
-
|
|
111
|
-
const epicWorkItems =
|
|
112
|
-
activeView === "overall" && selectedEpicId != null
|
|
113
|
-
? parseWorkItemsForEpic(
|
|
114
|
-
activeProject,
|
|
115
|
-
selectedEpicId,
|
|
116
|
-
selectedEpic?.title ?? null
|
|
117
|
-
)
|
|
118
|
-
: null
|
|
119
|
-
|
|
120
|
-
const selectedStory =
|
|
121
|
-
epicWorkItems && selectedStoryId
|
|
122
|
-
? (epicWorkItems.rows.find(
|
|
123
|
-
(row) => row.kind === "story" && row.id === selectedStoryId
|
|
124
|
-
) ?? null)
|
|
125
|
-
: null
|
|
126
|
-
|
|
127
|
-
const itemList =
|
|
128
|
-
activeView === "overall" &&
|
|
129
|
-
selectedEpicId != null &&
|
|
130
|
-
selectedStory != null
|
|
131
|
-
? parseItemsForStory(activeProject, selectedEpicId, selectedStory.id)
|
|
132
|
-
: null
|
|
133
|
-
|
|
134
|
-
const workItemsList =
|
|
135
|
-
activeView === "overall" || activeView === "workitems"
|
|
136
|
-
? parseWorkItemsViewForProject(activeProject)
|
|
137
|
-
: null
|
|
138
|
-
|
|
139
|
-
/** Match Current Speed exactly: completed task/bug leaf points only. */
|
|
140
|
-
const countableCompletions = collectCompletedLeafPointSamples(
|
|
141
|
-
collectMetricLeaves(activeProject)
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
const statusMetrics = computeProjectStatusMetrics(activeProject)
|
|
145
|
-
|
|
146
|
-
return (
|
|
147
|
-
<WorkItemSheetProvider>
|
|
148
|
-
<div className="min-h-svh bg-[linear-gradient(180deg,_#fff_0%,_#f7f4ff_100%)]">
|
|
149
|
-
<AppBar
|
|
150
|
-
projects={workspace.projects}
|
|
151
|
-
activeProjectId={activeProject.id}
|
|
152
|
-
autoconfig={workspace.autoconfig}
|
|
153
|
-
/>
|
|
154
|
-
|
|
155
|
-
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-4 py-8">
|
|
156
|
-
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
157
|
-
<div>
|
|
158
|
-
<h1 className="font-head text-3xl tracking-tight">
|
|
159
|
-
{headingForView(activeView)}
|
|
160
|
-
</h1>
|
|
161
|
-
<p className="mt-1 text-sm text-muted-foreground">
|
|
162
|
-
{activeProject.name}
|
|
163
|
-
{activeView === "overall"
|
|
164
|
-
? ` · ${epicCount} epic${epicCount === 1 ? "" : "s"}`
|
|
165
|
-
: ""}
|
|
166
|
-
{activeView === "overall" && errorCount > 0
|
|
167
|
-
? ` · ${errorCount} parse issue${errorCount === 1 ? "" : "s"}`
|
|
168
|
-
: ""}
|
|
169
|
-
{activeView === "workitems" && workItemsList
|
|
170
|
-
? ` · ${workItemsList.rows.length} item${workItemsList.rows.length === 1 ? "" : "s"}`
|
|
171
|
-
: ""}
|
|
172
|
-
{activeView === "overall" && selectedEpic
|
|
173
|
-
? ` · selected ${selectedEpic.id}`
|
|
174
|
-
: activeView === "overall" && selectedEpicId
|
|
175
|
-
? ` · epic ${selectedEpicId} not in list`
|
|
176
|
-
: activeView === "overall"
|
|
177
|
-
? " · select an epic to view work items"
|
|
178
|
-
: ""}
|
|
179
|
-
{activeView === "overall" && selectedStory
|
|
180
|
-
? ` · story ${selectedStory.id}`
|
|
181
|
-
: activeView === "overall" && selectedStoryId
|
|
182
|
-
? ` · story ${selectedStoryId} not in list`
|
|
183
|
-
: ""}
|
|
184
|
-
</p>
|
|
185
|
-
</div>
|
|
186
|
-
<ListViewSwitcher
|
|
187
|
-
activeView={activeView}
|
|
188
|
-
selectedEpicId={selectedEpicId}
|
|
189
|
-
selectedStoryId={selectedStory?.id ?? null}
|
|
190
|
-
/>
|
|
191
|
-
</div>
|
|
192
|
-
|
|
193
|
-
<ProjectStatusMetricsStrip metrics={statusMetrics} />
|
|
194
|
-
|
|
195
|
-
{activeView === "overall" ? (
|
|
196
|
-
<>
|
|
197
|
-
<EpicList
|
|
198
|
-
lists={[list]}
|
|
199
|
-
selectedEpicId={selectedEpicId}
|
|
200
|
-
countableCompletions={countableCompletions}
|
|
201
|
-
initialHideCompleted={hideCompletedPrefs.epics}
|
|
202
|
-
/>
|
|
203
|
-
|
|
204
|
-
{epicWorkItems ? (
|
|
205
|
-
<div className="flex flex-col gap-3">
|
|
206
|
-
<div className="flex items-baseline justify-between gap-3">
|
|
207
|
-
<h2 className="font-head text-2xl tracking-tight">
|
|
208
|
-
Work items for{" "}
|
|
209
|
-
{epicWorkItems.epicTitle ?? epicWorkItems.epicId}
|
|
210
|
-
</h2>
|
|
211
|
-
<Link
|
|
212
|
-
href={boardHref({
|
|
213
|
-
view: "overall",
|
|
214
|
-
item: selectedItemId,
|
|
215
|
-
})}
|
|
216
|
-
className="text-sm text-muted-foreground underline-offset-2 hover:underline"
|
|
217
|
-
>
|
|
218
|
-
Clear selection
|
|
219
|
-
</Link>
|
|
220
|
-
</div>
|
|
221
|
-
<p className="text-sm text-muted-foreground">
|
|
222
|
-
{epicWorkItems.rows.length} work item
|
|
223
|
-
{epicWorkItems.rows.length === 1 ? "" : "s"}
|
|
224
|
-
{epicWorkItems.errors.length > 0
|
|
225
|
-
? ` · ${epicWorkItems.errors.length} issue${epicWorkItems.errors.length === 1 ? "" : "s"}`
|
|
226
|
-
: ""}
|
|
227
|
-
{!selectedStory
|
|
228
|
-
? " · select a story to view its sub tasks"
|
|
229
|
-
: ""}
|
|
230
|
-
</p>
|
|
231
|
-
<OverallWorkItemsList
|
|
232
|
-
list={epicWorkItems}
|
|
233
|
-
selectedStoryId={selectedStory?.id ?? null}
|
|
234
|
-
countableCompletions={countableCompletions}
|
|
235
|
-
initialHideCompleted={hideCompletedPrefs.overallWorkItems}
|
|
236
|
-
/>
|
|
237
|
-
</div>
|
|
238
|
-
) : null}
|
|
239
|
-
|
|
240
|
-
{itemList && selectedStory ? (
|
|
241
|
-
<div className="flex flex-col gap-3">
|
|
242
|
-
<div className="flex items-baseline justify-between gap-3">
|
|
243
|
-
<h2 className="font-head text-2xl tracking-tight">
|
|
244
|
-
Sub Tasks for{" "}
|
|
245
|
-
{itemList.storyTitle ?? itemList.storyId}
|
|
246
|
-
</h2>
|
|
247
|
-
<Link
|
|
248
|
-
href={boardHref({
|
|
249
|
-
view: "overall",
|
|
250
|
-
epic: itemList.epicId,
|
|
251
|
-
item: selectedItemId,
|
|
252
|
-
})}
|
|
253
|
-
className="text-sm text-muted-foreground underline-offset-2 hover:underline"
|
|
254
|
-
>
|
|
255
|
-
Clear story
|
|
256
|
-
</Link>
|
|
257
|
-
</div>
|
|
258
|
-
<p className="text-sm text-muted-foreground">
|
|
259
|
-
{itemList.items.length} sub task
|
|
260
|
-
{itemList.items.length === 1 ? "" : "s"}
|
|
261
|
-
{itemList.errors.length > 0
|
|
262
|
-
? ` · ${itemList.errors.length} issue${itemList.errors.length === 1 ? "" : "s"}`
|
|
263
|
-
: ""}
|
|
264
|
-
</p>
|
|
265
|
-
<TaskList
|
|
266
|
-
list={itemList}
|
|
267
|
-
countableCompletions={countableCompletions}
|
|
268
|
-
initialHideCompleted={hideCompletedPrefs.tasks}
|
|
269
|
-
/>
|
|
270
|
-
</div>
|
|
271
|
-
) : null}
|
|
272
|
-
</>
|
|
273
|
-
) : null}
|
|
274
|
-
|
|
275
|
-
{activeView === "workitems" && workItemsList ? (
|
|
276
|
-
<WorkItemsList
|
|
277
|
-
list={workItemsList}
|
|
278
|
-
countableCompletions={countableCompletions}
|
|
279
|
-
initialHideCompleted={hideCompletedPrefs.workItems}
|
|
280
|
-
/>
|
|
281
|
-
) : null}
|
|
282
|
-
</div>
|
|
283
|
-
</div>
|
|
284
|
-
</WorkItemSheetProvider>
|
|
285
|
-
)
|
|
16
|
+
return <BoardScreen searchParams={searchParams} />
|
|
286
17
|
}
|
package/app/page.tsx
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BoardScreen } from "@/components/board/board-screen"
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
import { isStaticRuntime } from "@/lib/taskmark/static-mode"
|
|
5
|
-
import { loadWorkspace } from "@/lib/taskmark/workspace"
|
|
3
|
+
export const dynamic = "force-dynamic"
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
5
|
+
type HomePageProps = {
|
|
6
|
+
searchParams: Promise<{
|
|
7
|
+
view?: string | string[]
|
|
8
|
+
epic?: string | string[]
|
|
9
|
+
story?: string | string[]
|
|
10
|
+
item?: string | string[]
|
|
11
|
+
}>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The board is the default view; BoardScreen sends unbound setups to /setup. */
|
|
15
|
+
export default async function HomePage({ searchParams }: HomePageProps) {
|
|
16
|
+
return <BoardScreen searchParams={searchParams} />
|
|
17
17
|
}
|
package/app/setup/actions.ts
CHANGED
|
@@ -64,7 +64,7 @@ export async function saveMasterFolder(
|
|
|
64
64
|
await setActiveProjectCookie(preferred.id)
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
redirect("/
|
|
67
|
+
redirect("/")
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
export type PickMasterFolderState = {
|
|
@@ -89,7 +89,7 @@ export async function openAddProject(): Promise<void> {
|
|
|
89
89
|
"@/lib/taskmark/autoconfig"
|
|
90
90
|
)
|
|
91
91
|
if (resolveAutoconfigWorkspace()?.projects.length) {
|
|
92
|
-
redirect("/
|
|
92
|
+
redirect("/")
|
|
93
93
|
}
|
|
94
94
|
redirect("/setup?mode=add")
|
|
95
95
|
}
|
|
@@ -100,8 +100,8 @@ export async function selectActiveProject(formData: FormData): Promise<void> {
|
|
|
100
100
|
return
|
|
101
101
|
}
|
|
102
102
|
await setActiveProjectCookie(projectId)
|
|
103
|
-
revalidatePath("/
|
|
104
|
-
redirect("/
|
|
103
|
+
revalidatePath("/")
|
|
104
|
+
redirect("/")
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
/** Optional: clear everything and start over (not shown in app bar by default). */
|
package/app/setup/page.tsx
CHANGED
|
@@ -6,26 +6,34 @@ import { SetupWizard } from "@/components/setup/setup-wizard"
|
|
|
6
6
|
import { resolveAutoconfigWorkspace } from "@/lib/taskmark/autoconfig"
|
|
7
7
|
import { getMasterFoldersCookie } from "@/lib/taskmark/cookies"
|
|
8
8
|
import { isStaticRuntime } from "@/lib/taskmark/static-mode"
|
|
9
|
+
import { workspaceStartPath } from "@/lib/taskmark/workspace-mode"
|
|
9
10
|
|
|
10
11
|
type SetupPageProps = {
|
|
11
12
|
searchParams?: Promise<{ mode?: string }>
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
// Depends on runtime env (workspace vs bound) and cookies — never prerender in
|
|
16
|
+
// server mode, or a build-time redirect gets baked in and loops against `/`.
|
|
17
|
+
// The static build rewrites this to force-static (see scripts/build-static.mjs).
|
|
18
|
+
export const dynamic = "force-dynamic"
|
|
19
|
+
|
|
14
20
|
export default async function SetupPage({ searchParams }: SetupPageProps) {
|
|
15
21
|
if (isStaticRuntime()) {
|
|
16
|
-
redirect("/
|
|
22
|
+
redirect("/")
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
const auto = resolveAutoconfigWorkspace()
|
|
20
26
|
if (auto && auto.projects.length > 0) {
|
|
21
27
|
// Zero-config bind — setup wizard is not needed.
|
|
22
|
-
redirect("/
|
|
28
|
+
redirect("/")
|
|
23
29
|
}
|
|
24
30
|
|
|
25
31
|
const params = searchParams ? await searchParams : {}
|
|
26
32
|
const masters = await getMasterFoldersCookie()
|
|
27
33
|
const hasProjects = masters.length > 0
|
|
28
34
|
const adding = params.mode === "add" && hasProjects
|
|
35
|
+
// First run: offer the directory `taskmark` was launched from as the master.
|
|
36
|
+
const suggestedPath = adding || hasProjects ? "" : workspaceStartPath()
|
|
29
37
|
|
|
30
38
|
return (
|
|
31
39
|
<div className="relative flex min-h-svh flex-col items-center justify-center px-4 py-16">
|
|
@@ -50,13 +58,13 @@ export default async function SetupPage({ searchParams }: SetupPageProps) {
|
|
|
50
58
|
</p>
|
|
51
59
|
</div>
|
|
52
60
|
<SetupWizard
|
|
53
|
-
initialPath=
|
|
61
|
+
initialPath={suggestedPath}
|
|
54
62
|
adding={adding}
|
|
55
63
|
hasExistingProjects={hasProjects}
|
|
56
64
|
/>
|
|
57
65
|
{adding ? (
|
|
58
66
|
<p className="text-sm text-muted-foreground">
|
|
59
|
-
<Link href="/
|
|
67
|
+
<Link href="/" className="underline underline-offset-2">
|
|
60
68
|
Cancel and return to board
|
|
61
69
|
</Link>
|
|
62
70
|
</p>
|
|
@@ -48,6 +48,50 @@ export function resolveBoardAtPath(candidatePath) {
|
|
|
48
48
|
return null
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Multi-git layout: product repo beside a dedicated `<common>-taskmark` board.
|
|
53
|
+
* Prefer `<parentDirBasename>-taskmark`, else a unique `*-taskmark` sibling,
|
|
54
|
+
* else a sibling whose board prefix matches the product repo name.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveSiblingDedicatedBoard(fromDir) {
|
|
57
|
+
const resolved = path.resolve(fromDir)
|
|
58
|
+
const parent = path.dirname(resolved)
|
|
59
|
+
const parentName = path.basename(parent)
|
|
60
|
+
const preferred = path.join(parent, `${parentName}-taskmark`)
|
|
61
|
+
if (
|
|
62
|
+
isDedicatedBoardRepoName(path.basename(preferred)) &&
|
|
63
|
+
hasBoardMarkers(preferred)
|
|
64
|
+
) {
|
|
65
|
+
return preferred
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let entries
|
|
69
|
+
try {
|
|
70
|
+
entries = fs.readdirSync(parent, { withFileTypes: true })
|
|
71
|
+
} catch {
|
|
72
|
+
return null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const boards = []
|
|
76
|
+
for (const entry of entries) {
|
|
77
|
+
if (!entry.isDirectory() || !isDedicatedBoardRepoName(entry.name)) continue
|
|
78
|
+
const full = path.join(parent, entry.name)
|
|
79
|
+
if (hasBoardMarkers(full)) boards.push(full)
|
|
80
|
+
}
|
|
81
|
+
if (boards.length === 0) return null
|
|
82
|
+
if (boards.length === 1) return boards[0]
|
|
83
|
+
|
|
84
|
+
const cwdBase = path.basename(resolved)
|
|
85
|
+
const parts = cwdBase.split("-").filter(Boolean)
|
|
86
|
+
for (let i = parts.length - 1; i >= 1; i--) {
|
|
87
|
+
const prefix = parts.slice(0, i).join("-")
|
|
88
|
+
const hit = boards.find((b) => path.basename(b) === `${prefix}-taskmark`)
|
|
89
|
+
if (hit) return hit
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
|
|
51
95
|
function cwdCandidates() {
|
|
52
96
|
const out = []
|
|
53
97
|
const seen = new Set()
|
|
@@ -74,7 +118,7 @@ function cwdCandidates() {
|
|
|
74
118
|
|
|
75
119
|
/**
|
|
76
120
|
* Same precedence as the Next app autoconfig:
|
|
77
|
-
* TASKMARK_BOARD → TASKMARK_MASTER (as board-or-master) → cwd.
|
|
121
|
+
* TASKMARK_BOARD → TASKMARK_MASTER (as board-or-master) → cwd → sibling *-taskmark.
|
|
78
122
|
* @returns {{ boardPath: string, source: string } | null}
|
|
79
123
|
*/
|
|
80
124
|
export function resolveServeBoard(explicitBoard) {
|
|
@@ -104,5 +148,10 @@ export function resolveServeBoard(explicitBoard) {
|
|
|
104
148
|
if (board) return { boardPath: board, source: "cwd" }
|
|
105
149
|
}
|
|
106
150
|
|
|
151
|
+
for (const candidate of cwdCandidates()) {
|
|
152
|
+
const sibling = resolveSiblingDedicatedBoard(candidate)
|
|
153
|
+
if (sibling) return { boardPath: sibling, source: "sibling" }
|
|
154
|
+
}
|
|
155
|
+
|
|
107
156
|
return null
|
|
108
157
|
}
|