@nexttylabs/echo 0.4.0 → 0.6.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/CHANGELOG.md +27 -0
- package/app/(dashboard)/admin/feedback/[id]/edit/page.tsx +12 -6
- package/app/(dashboard)/admin/feedback/new/page.tsx +19 -17
- package/app/(dashboard)/admin/layout.tsx +16 -6
- package/app/(dashboard)/layout.tsx +4 -2
- package/app/(dashboard)/settings/api-keys/page.tsx +13 -3
- package/app/(dashboard)/settings/layout.tsx +25 -2
- package/app/(dashboard)/settings/organization/page.tsx +8 -9
- package/app/(public)/[organizationSlug]/roadmap/page.tsx +19 -1
- package/app/api/admin/backup/route.ts +22 -4
- package/app/api/auth/register/handler.ts +1 -2
- package/app/api/feedback/[id]/comments/[commentId]/route.ts +13 -4
- package/app/api/feedback/[id]/reclassify/route.ts +4 -4
- package/app/api/organizations/handler.ts +2 -4
- package/components/settings/settings-sidebar.tsx +4 -4
- package/hooks/use-organization.tsx +116 -0
- package/hooks/use-permissions.ts +24 -11
- package/lib/auth/config.ts +0 -7
- package/lib/auth/organization.ts +20 -0
- package/lib/auth/permissions.ts +10 -0
- package/lib/db/migrations/0000_needy_leech.sql +335 -0
- package/lib/db/migrations/meta/0000_snapshot.json +2186 -1
- package/lib/db/migrations/meta/_journal.json +2 -135
- package/lib/db/schema/auth.ts +0 -1
- package/lib/db/schema/index.ts +0 -1
- package/lib/portal/public-context.tsx +5 -0
- package/package.json +20 -1
- package/.changeset/README.md +0 -21
- package/.changeset/config.json +0 -11
- package/.changeset/cozy-ghosts-care.md +0 -5
- package/.changeset/sharp-lines-stand.md +0 -5
- package/.changeset/sour-doodles-eat.md +0 -5
- package/.changeset/tender-moose-shop.md +0 -5
- package/.github/pull_request_template.md +0 -13
- package/.github/workflows/ci.yml +0 -41
- package/.github/workflows/publish.yml +0 -44
- package/.github/workflows/release.yml +0 -73
- package/AGENTS.md +0 -92
- package/Dockerfile +0 -57
- package/Makefile +0 -77
- package/bun.lock +0 -2503
- package/components/portal/project-switcher.tsx +0 -20
- package/docker-compose.dev.yml +0 -26
- package/docker-compose.yml +0 -98
- package/docs/architecture.md +0 -259
- package/docs/component-inventory.md +0 -261
- package/docs/database-migrations.md +0 -76
- package/docs/development-guide.md +0 -209
- package/docs/e2e-user-flows.csv +0 -31
- package/docs/er-diagram-feedback.mmd +0 -138
- package/docs/er-diagram.mmd +0 -281
- package/docs/i18n-check-report.md +0 -296
- package/docs/index.md +0 -214
- package/docs/logic-chain.md +0 -94
- package/docs/plans/2026-01-02-database-migration-scripts.md +0 -496
- package/docs/plans/2026-01-02-user-login-design.md +0 -37
- package/docs/plans/2026-01-02-user-login.md +0 -437
- package/docs/plans/2026-01-02-user-registration-design.md +0 -47
- package/docs/plans/2026-01-02-user-registration.md +0 -628
- package/docs/plans/2026-01-03-roles-permissions-design.md +0 -20
- package/docs/plans/2026-01-03-roles-permissions.md +0 -266
- package/docs/plans/2026-01-05-authentication-middleware.md +0 -207
- package/docs/plans/2026-01-05-member-removal.md +0 -186
- package/docs/plans/2026-01-05-organization-creation.md +0 -374
- package/docs/plans/2026-01-05-rbac-middleware.md +0 -112
- package/docs/plans/2026-01-05-role-configuration.md +0 -441
- package/docs/plans/2026-01-06-file-upload-support.md +0 -804
- package/docs/plans/2026-01-06-permission-check-hook.md +0 -155
- package/docs/plans/2026-01-06-resource-ownership-check.md +0 -231
- package/docs/plans/2026-01-07-feedback-tracking-link.md +0 -459
- package/docs/plans/2026-01-09-logout-redirect-design.md +0 -52
- package/docs/plans/2026-01-09-phase2-3-plan.md +0 -654
- package/docs/plans/2026-01-09-portal-execution-plan.md +0 -408
- package/docs/plans/2026-01-09-project-delete-feature-design.md +0 -163
- package/docs/plans/2026-01-09-project-delete-implementation.md +0 -451
- package/docs/plans/2026-01-09-project-edit-delete-design.md +0 -52
- package/docs/plans/2026-01-09-settings-center-design.md +0 -114
- package/docs/plans/2026-01-09-settings-center.md +0 -948
- package/docs/plans/2026-01-10-organization-only-design.md +0 -66
- package/docs/plans/2026-01-10-organization-only-implementation.md +0 -433
- package/docs/plans/2026-01-10-portal-settings-restructure-plan.md +0 -18
- package/docs/plans/2026-01-10-project-settings-tabs-design-implementation.md +0 -296
- package/docs/plans/2026-01-14-e2e-playwright-feedback.md +0 -173
- package/docs/plans/2026-01-15-feedback-management-org-context-design.md +0 -82
- package/docs/plans/2026-01-15-feedback-management-org-context-implementation-plan.md +0 -521
- package/docs/plans/2026-01-16-admin-feedback-filters-design.md +0 -75
- package/docs/plans/2026-01-16-admin-feedback-filters-implementation.md +0 -293
- package/docs/plans/2026-01-16-admin-feedback-route-consolidation.md +0 -180
- package/docs/plans/2026-01-16-e2e-test-fixes.md +0 -158
- package/docs/plans/2026-01-17-admin-feedback-filters.md +0 -214
- package/docs/plans/2026-01-17-admin-feedback-improvements.md +0 -453
- package/docs/plans/2026-01-18-changesets-design.md +0 -40
- package/docs/product_changes.md +0 -37
- package/docs/project-overview.md +0 -159
- package/docs/project-scan-report.json +0 -104
- package/docs/route-role-visibility.md +0 -51
- package/docs/source-tree-analysis.md +0 -150
- package/docs/testing/delete-project-manual-tests.md +0 -18
- package/docs/user-story-tracking.md +0 -191
- package/eslint.config.mjs +0 -19
- package/lib/db/migrations/.gitkeep +0 -0
- package/lib/db/migrations/0000_cynical_gladiator.sql +0 -53
- package/lib/db/migrations/0001_wandering_sunfire.sql +0 -27
- package/lib/db/migrations/0002_shallow_speedball.sql +0 -1
- package/lib/db/migrations/0003_add_org_description.sql +0 -1
- package/lib/db/migrations/0003_boring_wild_pack.sql +0 -13
- package/lib/db/migrations/0004_windy_tyrannus.sql +0 -27
- package/lib/db/migrations/0005_perpetual_doorman.sql +0 -5
- package/lib/db/migrations/0006_aberrant_captain_midlands.sql +0 -13
- package/lib/db/migrations/0007_clever_captain_cross.sql +0 -14
- package/lib/db/migrations/0008_sparkling_pandemic.sql +0 -2
- package/lib/db/migrations/0009_happy_black_tom.sql +0 -29
- package/lib/db/migrations/0010_kind_junta.sql +0 -8
- package/lib/db/migrations/0011_mute_squadron_supreme.sql +0 -25
- package/lib/db/migrations/0012_giant_power_man.sql +0 -24
- package/lib/db/migrations/0013_damp_titanium_man.sql +0 -17
- package/lib/db/migrations/0014_blue_alice.sql +0 -18
- package/lib/db/migrations/0015_webhook_tables.sql +0 -41
- package/lib/db/migrations/0016_github_integration.sql +0 -30
- package/lib/db/migrations/0016_overjoyed_ghost_rider.sql +0 -22
- package/lib/db/migrations/0017_slimy_inhumans.sql +0 -6
- package/lib/db/migrations/0018_same_spitfire.sql +0 -1
- package/lib/db/migrations/0019_jittery_loners.sql +0 -16
- package/lib/db/migrations/0019_remove_projects_add_org_settings.sql +0 -14
- package/lib/db/migrations/meta/0001_snapshot.json +0 -553
- package/lib/db/migrations/meta/0002_snapshot.json +0 -560
- package/lib/db/migrations/meta/0003_snapshot.json +0 -650
- package/lib/db/migrations/meta/0004_snapshot.json +0 -852
- package/lib/db/migrations/meta/0005_snapshot.json +0 -900
- package/lib/db/migrations/meta/0006_snapshot.json +0 -1011
- package/lib/db/migrations/meta/0007_snapshot.json +0 -1125
- package/lib/db/migrations/meta/0008_snapshot.json +0 -1146
- package/lib/db/migrations/meta/0009_snapshot.json +0 -1386
- package/lib/db/migrations/meta/0010_snapshot.json +0 -1419
- package/lib/db/migrations/meta/0011_snapshot.json +0 -1615
- package/lib/db/migrations/meta/0012_snapshot.json +0 -1805
- package/lib/db/migrations/meta/0013_snapshot.json +0 -1948
- package/lib/db/migrations/meta/0014_snapshot.json +0 -2082
- package/lib/db/migrations/meta/0015_snapshot.json +0 -2476
- package/lib/db/migrations/meta/0016_snapshot.json +0 -2633
- package/lib/db/migrations/meta/0017_snapshot.json +0 -2680
- package/lib/db/migrations/meta/0018_snapshot.json +0 -2686
- package/lib/db/migrations/meta/0019_snapshot.json +0 -2741
- package/lib/db/schema/projects.ts +0 -145
- package/lib/db/schema/user-profiles.ts +0 -31
- package/lib/validations/projects.ts +0 -49
- package/next-env.d.ts +0 -6
- package/playwright.config.ts +0 -44
- package/proxy.test.ts +0 -131
- package/proxy.ts +0 -116
- package/scripts/backup-db.sh +0 -57
- package/scripts/backup-db.ts +0 -24
- package/scripts/generate-openapi.ts +0 -22
- package/scripts/migration-helper.ts +0 -39
- package/scripts/pre-deploy.ts +0 -75
- package/scripts/restore-db.sh +0 -60
- package/scripts/rollback.ts +0 -72
- package/scripts/seed-tags.ts +0 -48
- package/tests/api/feedback-bulk.test.ts +0 -47
- package/tests/api/feedback-by-id.test.ts +0 -67
- package/tests/api/feedback-comments-route-import.test.ts +0 -26
- package/tests/api/feedback-create.test.ts +0 -71
- package/tests/api/feedback-delete.test.ts +0 -160
- package/tests/api/feedback-filter.test.ts +0 -250
- package/tests/api/feedback-list.test.ts +0 -234
- package/tests/api/feedback-route-assignee-condition.test.ts +0 -32
- package/tests/api/feedback-similar.test.ts +0 -46
- package/tests/api/feedback-sort.test.ts +0 -261
- package/tests/api/feedback-status-enum.test.ts +0 -49
- package/tests/api/feedback-status-filter.test.ts +0 -117
- package/tests/api/feedback-submit-on-behalf.test.ts +0 -269
- package/tests/api/feedback.test.ts +0 -175
- package/tests/api/identify-jwt.test.ts +0 -25
- package/tests/api/invitation-accept.test.ts +0 -213
- package/tests/api/organization-invitations.test.ts +0 -186
- package/tests/api/organization-members-list.test.ts +0 -79
- package/tests/api/organization-members.test.ts +0 -340
- package/tests/api/organizations.test.ts +0 -149
- package/tests/api/register.test.ts +0 -112
- package/tests/api/upload.test.ts +0 -103
- package/tests/api/vote.test.ts +0 -82
- package/tests/app/admin-feedback-detail-page.test.tsx +0 -25
- package/tests/app/admin-feedback-list-page.test.tsx +0 -25
- package/tests/app/admin-feedback-new-page.test.tsx +0 -25
- package/tests/app/health-route-helpers.test.ts +0 -27
- package/tests/app/login-page.test.ts +0 -26
- package/tests/app/portal-page.test.ts +0 -29
- package/tests/app/project-portal-overview.test.tsx +0 -25
- package/tests/app/widget-page-import.test.ts +0 -25
- package/tests/components/create-post-dialog-defaults.test.ts +0 -43
- package/tests/components/feedback/duplicate-suggestions-inline.test.tsx +0 -27
- package/tests/components/feedback/embedded-feedback-form.test.tsx +0 -96
- package/tests/components/feedback/feedback-detail.test.tsx +0 -25
- package/tests/components/feedback/feedback-stats.test.tsx +0 -49
- package/tests/components/feedback-bulk-actions.test.tsx +0 -39
- package/tests/components/feedback-i18n-keys.test.ts +0 -70
- package/tests/components/feedback-list-controls-compile.test.ts +0 -25
- package/tests/components/feedback-list-controls.test.tsx +0 -204
- package/tests/components/feedback-list-item.test.tsx +0 -67
- package/tests/components/landing/hero.test.tsx +0 -46
- package/tests/components/layout/language-switcher.test.tsx +0 -25
- package/tests/components/layout/sidebar.test.tsx +0 -157
- package/tests/components/login-form.test.ts +0 -25
- package/tests/components/organization-form.test.ts +0 -32
- package/tests/components/organization-switcher.test.ts +0 -25
- package/tests/components/pagination.test.tsx +0 -43
- package/tests/components/portal-overview.test.tsx +0 -25
- package/tests/components/profile-form.test.tsx +0 -139
- package/tests/components/role-selector.test.ts +0 -31
- package/tests/components/status-chart.test.tsx +0 -90
- package/tests/e2e/auth.e2e.ts +0 -323
- package/tests/e2e/feedback-actions.e2e.ts +0 -471
- package/tests/e2e/feedback-attachment.e2e.ts +0 -168
- package/tests/e2e/feedback-customer.e2e.ts +0 -226
- package/tests/e2e/feedback-management.e2e.ts +0 -565
- package/tests/e2e/feedback-submit.e2e.ts +0 -133
- package/tests/e2e/feedback-view.e2e.ts +0 -297
- package/tests/e2e/fixtures/test-data.ts +0 -235
- package/tests/e2e/health-check.e2e.ts +0 -230
- package/tests/e2e/helpers/test-utils-helpers.test.ts +0 -43
- package/tests/e2e/helpers/test-utils.ts +0 -298
- package/tests/e2e/integration-placeholders.e2e.ts +0 -199
- package/tests/e2e/organization.e2e.ts +0 -292
- package/tests/e2e/permissions.e2e.ts +0 -424
- package/tests/e2e/project-widget.e2e.ts +0 -63
- package/tests/feedback/filters.test.ts +0 -29
- package/tests/hooks/use-permissions.test.ts +0 -52
- package/tests/lib/ai/classifier.test.ts +0 -104
- package/tests/lib/ai/duplicate-detector.test.ts +0 -234
- package/tests/lib/attachments-schema.test.ts +0 -30
- package/tests/lib/auth/session.test.ts +0 -49
- package/tests/lib/auth-client.test.ts +0 -37
- package/tests/lib/auth-config.test.ts +0 -26
- package/tests/lib/feedback-prefill.test.ts +0 -52
- package/tests/lib/feedback-processor.test.ts +0 -41
- package/tests/lib/feedback-schema.test.ts +0 -33
- package/tests/lib/file-validator.test.ts +0 -48
- package/tests/lib/get-feedback-by-id.test.ts +0 -37
- package/tests/lib/invitations.test.ts +0 -35
- package/tests/lib/login-schema.test.ts +0 -36
- package/tests/lib/org-context.test.ts +0 -95
- package/tests/lib/organization-access.test.ts +0 -44
- package/tests/lib/organization-member-role-schema.test.ts +0 -41
- package/tests/lib/permissions.test.ts +0 -88
- package/tests/lib/portal-analytics.test.ts +0 -25
- package/tests/lib/portal-contributors.test.ts +0 -25
- package/tests/lib/portal-copy.test.ts +0 -27
- package/tests/lib/portal-i18n.test.ts +0 -30
- package/tests/lib/portal-leaderboard-settings.test.ts +0 -25
- package/tests/lib/portal-modules.test.ts +0 -25
- package/tests/lib/portal-seo.test.ts +0 -25
- package/tests/lib/portal-sharing.test.ts +0 -25
- package/tests/lib/portal-sorting.test.ts +0 -25
- package/tests/lib/portal-theme.test.ts +0 -25
- package/tests/lib/rate-limit.test.ts +0 -142
- package/tests/lib/resolve-locale.test.ts +0 -34
- package/tests/lib/services/backup.test.ts +0 -145
- package/tests/lib/user-organizations.test.ts +0 -42
- package/tests/lib/user-role-schema.test.ts +0 -33
- package/tests/lib/user-schema.test.ts +0 -25
- package/tests/setup.ts +0 -74
- 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).
|