@nexttylabs/echo 0.4.0 → 0.5.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.
Files changed (247) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
  3. package/app/api/admin/backup/route.ts +22 -4
  4. package/app/api/auth/register/handler.ts +1 -2
  5. package/lib/auth/config.ts +0 -7
  6. package/lib/db/migrations/0000_needy_leech.sql +335 -0
  7. package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
  8. package/lib/db/migrations/meta/_journal.json +2 -135
  9. package/lib/db/schema/auth.ts +0 -1
  10. package/lib/db/schema/index.ts +0 -1
  11. package/lib/portal/public-context.tsx +5 -0
  12. package/package.json +20 -1
  13. package/.changeset/README.md +0 -21
  14. package/.changeset/config.json +0 -11
  15. package/.changeset/cozy-ghosts-care.md +0 -5
  16. package/.changeset/sharp-lines-stand.md +0 -5
  17. package/.changeset/sour-doodles-eat.md +0 -5
  18. package/.changeset/tender-moose-shop.md +0 -5
  19. package/.github/pull_request_template.md +0 -13
  20. package/.github/workflows/ci.yml +0 -41
  21. package/.github/workflows/publish.yml +0 -44
  22. package/.github/workflows/release.yml +0 -73
  23. package/AGENTS.md +0 -92
  24. package/Dockerfile +0 -57
  25. package/Makefile +0 -77
  26. package/bun.lock +0 -2503
  27. package/components/portal/project-switcher.tsx +0 -20
  28. package/docker-compose.dev.yml +0 -26
  29. package/docker-compose.yml +0 -98
  30. package/docs/architecture.md +0 -259
  31. package/docs/component-inventory.md +0 -261
  32. package/docs/database-migrations.md +0 -76
  33. package/docs/development-guide.md +0 -209
  34. package/docs/e2e-user-flows.csv +0 -31
  35. package/docs/er-diagram-feedback.mmd +0 -138
  36. package/docs/er-diagram.mmd +0 -281
  37. package/docs/i18n-check-report.md +0 -296
  38. package/docs/index.md +0 -214
  39. package/docs/logic-chain.md +0 -94
  40. package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
  41. package/docs/plans/2026-01-02-user-login-design.md +0 -37
  42. package/docs/plans/2026-01-02-user-login.md +0 -437
  43. package/docs/plans/2026-01-02-user-registration-design.md +0 -47
  44. package/docs/plans/2026-01-02-user-registration.md +0 -628
  45. package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
  46. package/docs/plans/2026-01-03-roles-permissions.md +0 -266
  47. package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
  48. package/docs/plans/2026-01-05-member-removal.md +0 -186
  49. package/docs/plans/2026-01-05-organization-creation.md +0 -374
  50. package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
  51. package/docs/plans/2026-01-05-role-configuration.md +0 -441
  52. package/docs/plans/2026-01-06-file-upload-support.md +0 -804
  53. package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
  54. package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
  55. package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
  56. package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
  57. package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
  58. package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
  59. package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
  60. package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
  61. package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
  62. package/docs/plans/2026-01-09-settings-center-design.md +0 -114
  63. package/docs/plans/2026-01-09-settings-center.md +0 -948
  64. package/docs/plans/2026-01-10-organization-only-design.md +0 -66
  65. package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
  66. package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
  67. package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
  68. package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
  69. package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
  70. package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
  71. package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
  72. package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
  73. package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
  74. package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
  75. package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
  76. package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
  77. package/docs/plans/2026-01-18-changesets-design.md +0 -40
  78. package/docs/product_changes.md +0 -37
  79. package/docs/project-overview.md +0 -159
  80. package/docs/project-scan-report.json +0 -104
  81. package/docs/route-role-visibility.md +0 -51
  82. package/docs/source-tree-analysis.md +0 -150
  83. package/docs/testing/delete-project-manual-tests.md +0 -18
  84. package/docs/user-story-tracking.md +0 -191
  85. package/eslint.config.mjs +0 -19
  86. package/lib/db/migrations/.gitkeep +0 -0
  87. package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
  88. package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
  89. package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
  90. package/lib/db/migrations/0003_add_org_description.sql +0 -1
  91. package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
  92. package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
  93. package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
  94. package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
  95. package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
  96. package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
  97. package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
  98. package/lib/db/migrations/0010_kind_junta.sql +0 -8
  99. package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
  100. package/lib/db/migrations/0012_giant_power_man.sql +0 -24
  101. package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
  102. package/lib/db/migrations/0014_blue_alice.sql +0 -18
  103. package/lib/db/migrations/0015_webhook_tables.sql +0 -41
  104. package/lib/db/migrations/0016_github_integration.sql +0 -30
  105. package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
  106. package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
  107. package/lib/db/migrations/0018_same_spitfire.sql +0 -1
  108. package/lib/db/migrations/0019_jittery_loners.sql +0 -16
  109. package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
  110. package/lib/db/migrations/meta/0001_snapshot.json +0 -553
  111. package/lib/db/migrations/meta/0002_snapshot.json +0 -560
  112. package/lib/db/migrations/meta/0003_snapshot.json +0 -650
  113. package/lib/db/migrations/meta/0004_snapshot.json +0 -852
  114. package/lib/db/migrations/meta/0005_snapshot.json +0 -900
  115. package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
  116. package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
  117. package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
  118. package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
  119. package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
  120. package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
  121. package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
  122. package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
  123. package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
  124. package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
  125. package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
  126. package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
  127. package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
  128. package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
  129. package/lib/db/schema/projects.ts +0 -145
  130. package/lib/db/schema/user-profiles.ts +0 -31
  131. package/lib/validations/projects.ts +0 -49
  132. package/next-env.d.ts +0 -6
  133. package/playwright.config.ts +0 -44
  134. package/proxy.test.ts +0 -131
  135. package/proxy.ts +0 -116
  136. package/scripts/backup-db.sh +0 -57
  137. package/scripts/backup-db.ts +0 -24
  138. package/scripts/generate-openapi.ts +0 -22
  139. package/scripts/migration-helper.ts +0 -39
  140. package/scripts/pre-deploy.ts +0 -75
  141. package/scripts/restore-db.sh +0 -60
  142. package/scripts/rollback.ts +0 -72
  143. package/scripts/seed-tags.ts +0 -48
  144. package/tests/api/feedback-bulk.test.ts +0 -47
  145. package/tests/api/feedback-by-id.test.ts +0 -67
  146. package/tests/api/feedback-comments-route-import.test.ts +0 -26
  147. package/tests/api/feedback-create.test.ts +0 -71
  148. package/tests/api/feedback-delete.test.ts +0 -160
  149. package/tests/api/feedback-filter.test.ts +0 -250
  150. package/tests/api/feedback-list.test.ts +0 -234
  151. package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
  152. package/tests/api/feedback-similar.test.ts +0 -46
  153. package/tests/api/feedback-sort.test.ts +0 -261
  154. package/tests/api/feedback-status-enum.test.ts +0 -49
  155. package/tests/api/feedback-status-filter.test.ts +0 -117
  156. package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
  157. package/tests/api/feedback.test.ts +0 -175
  158. package/tests/api/identify-jwt.test.ts +0 -25
  159. package/tests/api/invitation-accept.test.ts +0 -213
  160. package/tests/api/organization-invitations.test.ts +0 -186
  161. package/tests/api/organization-members-list.test.ts +0 -79
  162. package/tests/api/organization-members.test.ts +0 -340
  163. package/tests/api/organizations.test.ts +0 -149
  164. package/tests/api/register.test.ts +0 -112
  165. package/tests/api/upload.test.ts +0 -103
  166. package/tests/api/vote.test.ts +0 -82
  167. package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
  168. package/tests/app/admin-feedback-list-page.test.tsx +0 -25
  169. package/tests/app/admin-feedback-new-page.test.tsx +0 -25
  170. package/tests/app/health-route-helpers.test.ts +0 -27
  171. package/tests/app/login-page.test.ts +0 -26
  172. package/tests/app/portal-page.test.ts +0 -29
  173. package/tests/app/project-portal-overview.test.tsx +0 -25
  174. package/tests/app/widget-page-import.test.ts +0 -25
  175. package/tests/components/create-post-dialog-defaults.test.ts +0 -43
  176. package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
  177. package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
  178. package/tests/components/feedback/feedback-detail.test.tsx +0 -25
  179. package/tests/components/feedback/feedback-stats.test.tsx +0 -49
  180. package/tests/components/feedback-bulk-actions.test.tsx +0 -39
  181. package/tests/components/feedback-i18n-keys.test.ts +0 -70
  182. package/tests/components/feedback-list-controls-compile.test.ts +0 -25
  183. package/tests/components/feedback-list-controls.test.tsx +0 -204
  184. package/tests/components/feedback-list-item.test.tsx +0 -67
  185. package/tests/components/landing/hero.test.tsx +0 -46
  186. package/tests/components/layout/language-switcher.test.tsx +0 -25
  187. package/tests/components/layout/sidebar.test.tsx +0 -157
  188. package/tests/components/login-form.test.ts +0 -25
  189. package/tests/components/organization-form.test.ts +0 -32
  190. package/tests/components/organization-switcher.test.ts +0 -25
  191. package/tests/components/pagination.test.tsx +0 -43
  192. package/tests/components/portal-overview.test.tsx +0 -25
  193. package/tests/components/profile-form.test.tsx +0 -139
  194. package/tests/components/role-selector.test.ts +0 -31
  195. package/tests/components/status-chart.test.tsx +0 -90
  196. package/tests/e2e/auth.e2e.ts +0 -323
  197. package/tests/e2e/feedback-actions.e2e.ts +0 -471
  198. package/tests/e2e/feedback-attachment.e2e.ts +0 -168
  199. package/tests/e2e/feedback-customer.e2e.ts +0 -226
  200. package/tests/e2e/feedback-management.e2e.ts +0 -565
  201. package/tests/e2e/feedback-submit.e2e.ts +0 -133
  202. package/tests/e2e/feedback-view.e2e.ts +0 -297
  203. package/tests/e2e/fixtures/test-data.ts +0 -235
  204. package/tests/e2e/health-check.e2e.ts +0 -230
  205. package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
  206. package/tests/e2e/helpers/test-utils.ts +0 -298
  207. package/tests/e2e/integration-placeholders.e2e.ts +0 -199
  208. package/tests/e2e/organization.e2e.ts +0 -292
  209. package/tests/e2e/permissions.e2e.ts +0 -424
  210. package/tests/e2e/project-widget.e2e.ts +0 -63
  211. package/tests/feedback/filters.test.ts +0 -29
  212. package/tests/hooks/use-permissions.test.ts +0 -52
  213. package/tests/lib/ai/classifier.test.ts +0 -104
  214. package/tests/lib/ai/duplicate-detector.test.ts +0 -234
  215. package/tests/lib/attachments-schema.test.ts +0 -30
  216. package/tests/lib/auth/session.test.ts +0 -49
  217. package/tests/lib/auth-client.test.ts +0 -37
  218. package/tests/lib/auth-config.test.ts +0 -26
  219. package/tests/lib/feedback-prefill.test.ts +0 -52
  220. package/tests/lib/feedback-processor.test.ts +0 -41
  221. package/tests/lib/feedback-schema.test.ts +0 -33
  222. package/tests/lib/file-validator.test.ts +0 -48
  223. package/tests/lib/get-feedback-by-id.test.ts +0 -37
  224. package/tests/lib/invitations.test.ts +0 -35
  225. package/tests/lib/login-schema.test.ts +0 -36
  226. package/tests/lib/org-context.test.ts +0 -95
  227. package/tests/lib/organization-access.test.ts +0 -44
  228. package/tests/lib/organization-member-role-schema.test.ts +0 -41
  229. package/tests/lib/permissions.test.ts +0 -88
  230. package/tests/lib/portal-analytics.test.ts +0 -25
  231. package/tests/lib/portal-contributors.test.ts +0 -25
  232. package/tests/lib/portal-copy.test.ts +0 -27
  233. package/tests/lib/portal-i18n.test.ts +0 -30
  234. package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
  235. package/tests/lib/portal-modules.test.ts +0 -25
  236. package/tests/lib/portal-seo.test.ts +0 -25
  237. package/tests/lib/portal-sharing.test.ts +0 -25
  238. package/tests/lib/portal-sorting.test.ts +0 -25
  239. package/tests/lib/portal-theme.test.ts +0 -25
  240. package/tests/lib/rate-limit.test.ts +0 -142
  241. package/tests/lib/resolve-locale.test.ts +0 -34
  242. package/tests/lib/services/backup.test.ts +0 -145
  243. package/tests/lib/user-organizations.test.ts +0 -42
  244. package/tests/lib/user-role-schema.test.ts +0 -33
  245. package/tests/lib/user-schema.test.ts +0 -25
  246. package/tests/setup.ts +0 -74
  247. package/vercel.json +0 -4
@@ -1,521 +0,0 @@
1
- # Feedback Organization Context Implementation Plan
2
-
3
- > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
-
5
- **Goal:** Enforce organization-aware feedback management across all pages and APIs with a unified org context resolver and a dashboard-only org switcher.
6
-
7
- **Architecture:** Add a server-side org context resolver that reads org ID from URL > header > cookie, validates membership, and is used by all feedback routes. Add a dashboard org switcher that writes org selection to cookie and URL.
8
-
9
- **Tech Stack:** Next.js App Router, React 19, TypeScript, Drizzle ORM, Bun test, Next cookies/headers.
10
-
11
- ---
12
-
13
- ### Task 1: Add organization context resolver + tests
14
-
15
- **Files:**
16
- - Create: `lib/auth/org-context.ts`
17
- - Create: `tests/lib/org-context.test.ts`
18
-
19
- **Step 1: Write the failing test**
20
-
21
- ```ts
22
- import { describe, expect, it } from "bun:test";
23
- import { getOrgContext } from "@/lib/auth/org-context";
24
-
25
- const makeRequest = (options: {
26
- query?: Record<string, string>;
27
- headerOrgId?: string;
28
- cookieOrgId?: string;
29
- }) => {
30
- const url = new URL("https://example.com");
31
- if (options.query) {
32
- for (const [key, value] of Object.entries(options.query)) {
33
- url.searchParams.set(key, value);
34
- }
35
- }
36
-
37
- return {
38
- nextUrl: url,
39
- headers: new Headers(
40
- options.headerOrgId ? { "x-organization-id": options.headerOrgId } : undefined,
41
- ),
42
- cookies: {
43
- get: (name: string) => (name === "orgId" && options.cookieOrgId ? { value: options.cookieOrgId } : undefined),
44
- },
45
- } as const;
46
- };
47
-
48
- const makeDb = (hasMember: boolean) => ({
49
- select: () => ({
50
- from: () => ({
51
- where: () => ({
52
- limit: async () => (hasMember ? [{ role: "admin" }] : []),
53
- }),
54
- }),
55
- }),
56
- });
57
-
58
- describe("getOrgContext", () => {
59
- it("prefers query over header and cookie", async () => {
60
- const req = makeRequest({ query: { organizationId: "org_query" }, headerOrgId: "org_header", cookieOrgId: "org_cookie" });
61
- const context = await getOrgContext({ request: req, db: makeDb(true), userId: "user_1" });
62
- expect(context.organizationId).toBe("org_query");
63
- expect(context.source).toBe("query");
64
- });
65
-
66
- it("throws when membership missing", async () => {
67
- const req = makeRequest({ cookieOrgId: "org_cookie" });
68
- await expect(
69
- getOrgContext({ request: req, db: makeDb(false), userId: "user_1", requireMembership: true })
70
- ).rejects.toThrow("Access denied");
71
- });
72
- });
73
- ```
74
-
75
- **Step 2: Run test to verify it fails**
76
-
77
- Run: `bun test tests/lib/org-context.test.ts`
78
- Expected: FAIL with module not found or missing exports.
79
-
80
- **Step 3: Write minimal implementation**
81
-
82
- ```ts
83
- import { assertOrganizationAccess } from "@/lib/auth/organization";
84
- import type { db as database } from "@/lib/db";
85
-
86
- type Database = NonNullable<typeof database>;
87
-
88
- type OrgContextSource = "query" | "header" | "cookie" | "explicit";
89
-
90
- export type OrgContext = {
91
- organizationId: string;
92
- memberRole: string | null;
93
- source: OrgContextSource;
94
- };
95
-
96
- export async function getOrgContext(options: {
97
- request: { nextUrl: URL; headers: Headers; cookies?: { get: (name: string) => { value: string } | undefined } };
98
- db: Pick<Database, "select">;
99
- userId?: string | null;
100
- organizationId?: string | null;
101
- requireMembership?: boolean;
102
- }): Promise<OrgContext> {
103
- const { request, db, userId, organizationId, requireMembership } = options;
104
- const queryOrgId = request.nextUrl.searchParams.get("organizationId");
105
- const headerOrgId = request.headers.get("x-organization-id");
106
- const cookieOrgId = request.cookies?.get("orgId")?.value ?? null;
107
-
108
- const resolved = organizationId || queryOrgId || headerOrgId || cookieOrgId;
109
- const source: OrgContextSource = organizationId
110
- ? "explicit"
111
- : queryOrgId
112
- ? "query"
113
- : headerOrgId
114
- ? "header"
115
- : "cookie";
116
-
117
- if (!resolved) {
118
- throw new Error("Missing organization");
119
- }
120
-
121
- let memberRole: string | null = null;
122
- if (userId) {
123
- const member = await assertOrganizationAccess(db, userId, resolved);
124
- memberRole = member.role;
125
- } else if (requireMembership) {
126
- throw new Error("Access denied");
127
- }
128
-
129
- return { organizationId: resolved, memberRole, source };
130
- }
131
- ```
132
-
133
- **Step 4: Run test to verify it passes**
134
-
135
- Run: `bun test tests/lib/org-context.test.ts`
136
- Expected: PASS
137
-
138
- **Step 5: Commit**
139
-
140
- ```bash
141
- git add lib/auth/org-context.ts tests/lib/org-context.test.ts
142
- git commit -m "feat: add org context resolver"
143
- ```
144
-
145
- ---
146
-
147
- ### Task 2: Add user organization list helper + tests
148
-
149
- **Files:**
150
- - Modify: `lib/auth/organization.ts`
151
- - Create: `tests/lib/user-organizations.test.ts`
152
-
153
- **Step 1: Write the failing test**
154
-
155
- ```ts
156
- import { describe, expect, it } from "bun:test";
157
- import { getUserOrganizations } from "@/lib/auth/organization";
158
-
159
- const makeDb = (rows: Array<{ id: string; name: string; slug: string; role: string }>) => ({
160
- select: () => ({
161
- from: () => ({
162
- innerJoin: () => ({
163
- where: async () => rows,
164
- }),
165
- }),
166
- }),
167
- });
168
-
169
- describe("getUserOrganizations", () => {
170
- it("returns organization list for user", async () => {
171
- const db = makeDb([{ id: "org_1", name: "Org", slug: "org", role: "admin" }]);
172
- const orgs = await getUserOrganizations(db as never, "user_1");
173
- expect(orgs).toHaveLength(1);
174
- expect(orgs[0].id).toBe("org_1");
175
- });
176
- });
177
- ```
178
-
179
- **Step 2: Run test to verify it fails**
180
-
181
- Run: `bun test tests/lib/user-organizations.test.ts`
182
- Expected: FAIL with missing export.
183
-
184
- **Step 3: Write minimal implementation**
185
-
186
- ```ts
187
- export async function getUserOrganizations(
188
- db: Database,
189
- userId: string
190
- ): Promise<UserOrganization[]> {
191
- return db
192
- .select({
193
- id: organizations.id,
194
- name: organizations.name,
195
- slug: organizations.slug,
196
- role: organizationMembers.role,
197
- })
198
- .from(organizationMembers)
199
- .innerJoin(organizations, eq(organizations.id, organizationMembers.organizationId))
200
- .where(eq(organizationMembers.userId, userId));
201
- }
202
- ```
203
-
204
- **Step 4: Run test to verify it passes**
205
-
206
- Run: `bun test tests/lib/user-organizations.test.ts`
207
- Expected: PASS
208
-
209
- **Step 5: Commit**
210
-
211
- ```bash
212
- git add lib/auth/organization.ts tests/lib/user-organizations.test.ts
213
- git commit -m "feat: add user organization list helper"
214
- ```
215
-
216
- ---
217
-
218
- ### Task 3: Add dashboard organization switcher (client component)
219
-
220
- **Files:**
221
- - Create: `components/dashboard/organization-switcher.tsx`
222
- - Modify: `components/dashboard/index.ts`
223
-
224
- **Step 1: Write the failing test**
225
-
226
- ```ts
227
- import { describe, expect, it } from "bun:test";
228
- import { OrganizationSwitcher } from "@/components/dashboard/organization-switcher";
229
-
230
- describe("OrganizationSwitcher", () => {
231
- it("is defined", () => {
232
- expect(OrganizationSwitcher).toBeDefined();
233
- });
234
- });
235
- ```
236
-
237
- **Step 2: Run test to verify it fails**
238
-
239
- Run: `bun test tests/components/organization-switcher.test.ts`
240
- Expected: FAIL (missing file).
241
-
242
- **Step 3: Write minimal implementation**
243
-
244
- ```tsx
245
- "use client";
246
-
247
- import { useState } from "react";
248
- import { useRouter, useSearchParams } from "next/navigation";
249
- import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
250
-
251
- type OrgOption = { id: string; name: string; slug: string; role: string };
252
-
253
- type Props = {
254
- organizations: OrgOption[];
255
- currentOrgId: string;
256
- };
257
-
258
- const COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 30;
259
-
260
- export function OrganizationSwitcher({ organizations, currentOrgId }: Props) {
261
- const [value, setValue] = useState(currentOrgId);
262
- const router = useRouter();
263
- const searchParams = useSearchParams();
264
-
265
- const handleChange = (orgId: string) => {
266
- setValue(orgId);
267
- const params = new URLSearchParams(searchParams);
268
- params.set("organizationId", orgId);
269
- document.cookie = `orgId=${orgId};path=/;max-age=${COOKIE_MAX_AGE_SECONDS};samesite=lax`;
270
- router.push(`/dashboard?${params.toString()}`);
271
- };
272
-
273
- return (
274
- <Select value={value} onValueChange={handleChange}>
275
- <SelectTrigger className="w-[240px]">
276
- <SelectValue placeholder="选择组织" />
277
- </SelectTrigger>
278
- <SelectContent>
279
- {organizations.map((org) => (
280
- <SelectItem key={org.id} value={org.id}>
281
- {org.name}
282
- </SelectItem>
283
- ))}
284
- </SelectContent>
285
- </Select>
286
- );
287
- }
288
- ```
289
-
290
- **Step 4: Run test to verify it passes**
291
-
292
- Run: `bun test tests/components/organization-switcher.test.ts`
293
- Expected: PASS
294
-
295
- **Step 5: Commit**
296
-
297
- ```bash
298
- git add components/dashboard/organization-switcher.tsx components/dashboard/index.ts tests/components/organization-switcher.test.ts
299
- git commit -m "feat: add dashboard org switcher"
300
- ```
301
-
302
- ---
303
-
304
- ### Task 4: Wire org context into dashboard and feedback pages
305
-
306
- **Files:**
307
- - Modify: `app/(dashboard)/dashboard/page.tsx`
308
- - Modify: `app/(dashboard)/feedback/page.tsx`
309
- - Modify: `app/admin/feedback/page.tsx`
310
- - Modify: `app/admin/feedback/[id]/page.tsx`
311
- - Modify: `app/admin/feedback/[id]/edit/page.tsx`
312
-
313
- **Step 1: Write the failing test**
314
-
315
- ```ts
316
- import { describe, expect, it } from "bun:test";
317
- import { getOrgContext } from "@/lib/auth/org-context";
318
-
319
- describe("dashboard org context", () => {
320
- it("uses cookie org when query missing", async () => {
321
- const request = {
322
- nextUrl: new URL("https://example.com/dashboard"),
323
- headers: new Headers(),
324
- cookies: { get: () => ({ value: "org_cookie" }) },
325
- } as const;
326
- const context = await getOrgContext({ request, db: { select: () => ({}) } as never, userId: null });
327
- expect(context.organizationId).toBe("org_cookie");
328
- });
329
- });
330
- ```
331
-
332
- **Step 2: Run test to verify it fails**
333
-
334
- Run: `bun test tests/lib/org-context.test.ts`
335
- Expected: FAIL until implementations are hooked in.
336
-
337
- **Step 3: Write minimal implementation**
338
-
339
- - In `app/(dashboard)/dashboard/page.tsx`, use `getUserOrganizations` to fetch org list and `getOrgContext` to resolve the active org.
340
- - If no `orgId` cookie exists, default to the first org, set cookie, and use that for stats.
341
- - Render `OrganizationSwitcher` (dashboard only) with org list and active org id.
342
- - In `app/(dashboard)/feedback/page.tsx` and `app/admin/feedback/page.tsx`, remove hardcoded org id and resolve using `getOrgContext` with membership required.
343
-
344
- **Step 4: Run tests to verify pass**
345
-
346
- Run: `bun test tests/lib/org-context.test.ts`
347
- Expected: PASS
348
-
349
- **Step 5: Commit**
350
-
351
- ```bash
352
- git add app/(dashboard)/dashboard/page.tsx app/(dashboard)/feedback/page.tsx app/admin/feedback/page.tsx app/admin/feedback/[id]/page.tsx app/admin/feedback/[id]/edit/page.tsx
353
- git commit -m "feat: use org context in dashboard and admin pages"
354
- ```
355
-
356
- ---
357
-
358
- ### Task 5: Enforce org context in feedback APIs (list and similar)
359
-
360
- **Files:**
361
- - Modify: `app/api/feedback/route.ts`
362
- - Modify: `app/api/feedback/similar/route.ts`
363
-
364
- **Step 1: Write the failing test**
365
-
366
- ```ts
367
- import { describe, expect, it } from "bun:test";
368
- import { getOrgContext } from "@/lib/auth/org-context";
369
-
370
- describe("feedback list API org enforcement", () => {
371
- it("throws when organization is missing", async () => {
372
- const request = { nextUrl: new URL("https://example.com/api/feedback"), headers: new Headers() } as const;
373
- await expect(getOrgContext({ request, db: { select: () => ({}) } as never, userId: "user" })).rejects.toThrow("Missing organization");
374
- });
375
- });
376
- ```
377
-
378
- **Step 2: Run test to verify it fails**
379
-
380
- Run: `bun test tests/lib/org-context.test.ts`
381
- Expected: FAIL until API uses resolver consistently.
382
-
383
- **Step 3: Write minimal implementation**
384
-
385
- - Replace manual `organizationId` resolution with `getOrgContext`.
386
- - Require membership for authenticated routes.
387
- - Use `context.organizationId` in feedback list and similar queries.
388
-
389
- **Step 4: Run tests to verify pass**
390
-
391
- Run: `bun test tests/lib/org-context.test.ts`
392
- Expected: PASS
393
-
394
- **Step 5: Commit**
395
-
396
- ```bash
397
- git add app/api/feedback/route.ts app/api/feedback/similar/route.ts
398
- git commit -m "feat: enforce org context in feedback list APIs"
399
- ```
400
-
401
- ---
402
-
403
- ### Task 6: Enforce org context in feedback detail and child APIs
404
-
405
- **Files:**
406
- - Modify: `app/api/feedback/[id]/route.ts`
407
- - Modify: `app/api/feedback/[id]/comments/route.ts`
408
- - Modify: `app/api/feedback/[id]/vote/route.ts`
409
- - Modify: `app/api/feedback/[id]/duplicates/route.ts`
410
- - Modify: `app/api/feedback/[id]/suggest-tags/route.ts`
411
- - Modify: `app/api/feedback/[id]/processing-status/route.ts`
412
- - Modify: `app/api/feedback/[id]/reclassify/route.ts`
413
-
414
- **Step 1: Write the failing test**
415
-
416
- ```ts
417
- import { describe, expect, it } from "bun:test";
418
- import { getOrgContext } from "@/lib/auth/org-context";
419
-
420
- describe("feedback detail API org enforcement", () => {
421
- it("throws when user is not a member", async () => {
422
- const request = { nextUrl: new URL("https://example.com/api/feedback/1"), headers: new Headers(), cookies: { get: () => ({ value: "org_1" }) } } as const;
423
- const db = { select: () => ({ from: () => ({ where: () => ({ limit: async () => [] }) }) }) };
424
- await expect(getOrgContext({ request, db: db as never, userId: "user", requireMembership: true })).rejects.toThrow("Access denied");
425
- });
426
- });
427
- ```
428
-
429
- **Step 2: Run test to verify it fails**
430
-
431
- Run: `bun test tests/lib/org-context.test.ts`
432
- Expected: FAIL until enforced.
433
-
434
- **Step 3: Write minimal implementation**
435
-
436
- - Resolve org context at the start of each route.
437
- - For id-based operations, verify the feedback’s `organizationId` matches the resolved org before returning or mutating.
438
- - If mismatch, return 404.
439
-
440
- **Step 4: Run tests to verify pass**
441
-
442
- Run: `bun test tests/lib/org-context.test.ts`
443
- Expected: PASS
444
-
445
- **Step 5: Commit**
446
-
447
- ```bash
448
- git add app/api/feedback/[id]/route.ts app/api/feedback/[id]/comments/route.ts app/api/feedback/[id]/vote/route.ts app/api/feedback/[id]/duplicates/route.ts app/api/feedback/[id]/suggest-tags/route.ts app/api/feedback/[id]/processing-status/route.ts app/api/feedback/[id]/reclassify/route.ts
449
- git commit -m "feat: enforce org context in feedback detail APIs"
450
- ```
451
-
452
- ---
453
-
454
- ### Task 7: Update API v1 and public routes for org context consistency
455
-
456
- **Files:**
457
- - Modify: `app/api/v1/feedback/route.ts`
458
- - Modify: `app/api/v1/feedback/[id]/route.ts`
459
- - Modify: `app/[organizationSlug]/page.tsx`
460
- - Modify: `app/[organizationSlug]/feedback/[id]/page.tsx`
461
-
462
- **Step 1: Write the failing test**
463
-
464
- ```ts
465
- import { describe, expect, it } from "bun:test";
466
- import { getOrgContext } from "@/lib/auth/org-context";
467
-
468
- describe("public portal org resolution", () => {
469
- it("accepts explicit organization id", async () => {
470
- const request = { nextUrl: new URL("https://example.com"), headers: new Headers() } as const;
471
- const context = await getOrgContext({ request, db: { select: () => ({}) } as never, organizationId: "org_explicit" });
472
- expect(context.organizationId).toBe("org_explicit");
473
- });
474
- });
475
- ```
476
-
477
- **Step 2: Run test to verify it fails**
478
-
479
- Run: `bun test tests/lib/org-context.test.ts`
480
- Expected: FAIL until explicit org support is used.
481
-
482
- **Step 3: Write minimal implementation**
483
-
484
- - Keep API v1 using API key org id, but pass the resolved org explicitly to the resolver (or bypass if not required).
485
- - Ensure portal routes resolve org via slug and pass explicit org id to avoid cookie contamination.
486
-
487
- **Step 4: Run tests to verify pass**
488
-
489
- Run: `bun test tests/lib/org-context.test.ts`
490
- Expected: PASS
491
-
492
- **Step 5: Commit**
493
-
494
- ```bash
495
- git add app/api/v1/feedback/route.ts app/api/v1/feedback/[id]/route.ts app/[organizationSlug]/page.tsx app/[organizationSlug]/feedback/[id]/page.tsx
496
- git commit -m "feat: align org context for v1 and portal routes"
497
- ```
498
-
499
- ---
500
-
501
- ### Task 8: Final verification and cleanup
502
-
503
- **Files:**
504
- - Modify: `docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md`
505
-
506
- **Step 1: Run unit tests**
507
-
508
- Run: `bun test tests/lib/org-context.test.ts tests/lib/user-organizations.test.ts`
509
- Expected: PASS
510
-
511
- **Step 2: Run lint (acknowledge baseline issues)**
512
-
513
- Run: `bun run lint`
514
- Expected: FAIL due to pre-existing lint errors in `tests/e2e/**`.
515
-
516
- **Step 3: Commit**
517
-
518
- ```bash
519
- git add docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md
520
- git commit -m "chore: record verification steps"
521
- ```
@@ -1,75 +0,0 @@
1
- # Admin Feedback Filters & Sorting Design
2
-
3
- Date: 2026-01-16
4
- Owner: Codex
5
- Scope: /admin/feedback list filters, sorting, pagination, assignee candidates, URL-driven state
6
-
7
- ## Goals
8
- - Provide robust filtering/sorting/pagination for admin feedback list.
9
- - Make list state URL-driven for deep linking and refresh persistence.
10
- - Add assignee, hasVotes, hasReplies filters with multi-select support.
11
- - Add organization members API for assignee candidates and cache client-side.
12
-
13
- ## Non-Goals
14
- - Redesign feedback detail/edit pages.
15
- - Add new permissions or RBAC policies beyond existing org access.
16
- - Introduce new analytics or tracking.
17
-
18
- ## Summary
19
- The admin feedback list becomes fully URL-driven. Filters and sorting are encoded in query parameters, and the list fetches data from `/api/feedback` using those parameters. A new `GET /api/organizations/[orgId]/members` endpoint provides candidate assignees (displayName + userId). The client caches member lists in `sessionStorage` by organization to reduce repeated fetches.
20
-
21
- ## User Experience
22
- - Controls bar above list: search, filters, sorting, primary action (e.g. New Feedback/Export).
23
- - Secondary row: selected filter chips + clear all.
24
- - Right side: total count and per-page size.
25
- - Filters are multi-select for `status`, `type`, `priority`, `assignee`, `hasVotes`, `hasReplies`.
26
- - Any filter/sort change resets `page=1`.
27
-
28
- ## URL Parameters
29
- - `query`: text search across title/description.
30
- - `status`: CSV of statuses.
31
- - `type`: CSV of types.
32
- - `priority`: CSV of priorities.
33
- - `assignee`: CSV of userIds plus `unassigned`.
34
- - `hasVotes`: CSV of boolean values (`true,false`) for multi-select behavior.
35
- - `hasReplies`: CSV of boolean values (`true,false`) for multi-select behavior.
36
- - `sortBy`: `createdAt | voteCount | priority | status`.
37
- - `sortOrder`: `asc | desc`.
38
- - `page`: numeric.
39
- - `pageSize`: numeric.
40
-
41
- Example:
42
- `?status=new,planned&assignee=unassigned,usr_123&hasVotes=true&sortBy=createdAt&sortOrder=desc&page=1`
43
-
44
- ## Data Sources
45
- - Feedback list: existing `/api/feedback` with new query parameters and filters.
46
- - Assignees: new `/api/organizations/[orgId]/members`.
47
-
48
- ## API: GET /api/organizations/[orgId]/members
49
- - Auth: current session user must belong to org.
50
- - Returns array:
51
- - `userId` (string)
52
- - `displayName` (string, derived from user name/email fallback)
53
- - `avatarUrl` (optional)
54
-
55
- ## Assignee Cache
56
- - `sessionStorage` key: `org:${organizationId}:members`.
57
- - Read cache first; if stale or missing, fetch and update cache.
58
- - UI includes fixed option `unassigned` at top.
59
-
60
- ## Error Handling
61
- - Feedback fetch errors render inline error with next step.
62
- - Assignee fetch failures fall back to empty list and show warning UI state.
63
-
64
- ## Accessibility
65
- - Icon-only buttons include `aria-label`.
66
- - Dropdowns and chips are keyboard navigable.
67
- - Loading copy uses ellipsis character `…`.
68
-
69
- ## Testing
70
- - Unit: query param parsing and serialization helpers.
71
- - Integration: list fetch updates on filter change; page resets to 1.
72
- - API: members endpoint returns only org members, denies unauthorized.
73
-
74
- ## Open Questions
75
- - Final CTA label and action target (New Feedback vs Export).