2026-03-26 07:41:58 -05:00
|
|
|
import { useMemo, useRef, useState } from "react";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
2026-02-17 10:53:20 -06:00
|
|
|
import { useDialog } from "../context/DialogContext";
|
|
|
|
|
import { useCompany } from "../context/CompanyContext";
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
import { accessApi } from "../api/access";
|
2026-02-17 10:53:20 -06:00
|
|
|
import { projectsApi } from "../api/projects";
|
2026-03-26 07:41:58 -05:00
|
|
|
import { agentsApi } from "../api/agents";
|
2026-02-17 10:53:20 -06:00
|
|
|
import { goalsApi } from "../api/goals";
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
import { assetsApi } from "../api/assets";
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
import { buildMarkdownMentionOptions } from "../lib/company-members";
|
2026-02-17 12:24:48 -06:00
|
|
|
import { queryKeys } from "../lib/queryKeys";
|
2026-02-17 10:53:20 -06:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogContent,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import {
|
|
|
|
|
Popover,
|
|
|
|
|
PopoverContent,
|
|
|
|
|
PopoverTrigger,
|
|
|
|
|
} from "@/components/ui/popover";
|
|
|
|
|
import {
|
|
|
|
|
Maximize2,
|
|
|
|
|
Minimize2,
|
|
|
|
|
Target,
|
|
|
|
|
Calendar,
|
2026-02-20 15:48:42 -06:00
|
|
|
Plus,
|
|
|
|
|
X,
|
2026-03-19 07:17:49 -05:00
|
|
|
HelpCircle,
|
2026-02-17 10:53:20 -06:00
|
|
|
} from "lucide-react";
|
2026-03-19 07:17:49 -05:00
|
|
|
import {
|
|
|
|
|
Tooltip,
|
|
|
|
|
TooltipContent,
|
|
|
|
|
TooltipTrigger,
|
|
|
|
|
} from "@/components/ui/tooltip";
|
2026-03-03 08:45:26 -06:00
|
|
|
import { PROJECT_COLORS } from "@paperclipai/shared";
|
2026-02-17 10:53:20 -06:00
|
|
|
import { cn } from "../lib/utils";
|
2026-03-26 07:41:58 -05:00
|
|
|
import { MarkdownEditor, type MarkdownEditorRef, type MentionOption } from "./MarkdownEditor";
|
2026-02-17 10:53:20 -06:00
|
|
|
import { StatusBadge } from "./StatusBadge";
|
2026-03-02 16:09:07 -06:00
|
|
|
import { ChoosePathButton } from "./PathInstructionsModal";
|
2026-02-17 10:53:20 -06:00
|
|
|
|
|
|
|
|
const projectStatuses = [
|
|
|
|
|
{ value: "backlog", label: "Backlog" },
|
|
|
|
|
{ value: "planned", label: "Planned" },
|
|
|
|
|
{ value: "in_progress", label: "In Progress" },
|
|
|
|
|
{ value: "completed", label: "Completed" },
|
|
|
|
|
{ value: "cancelled", label: "Cancelled" },
|
|
|
|
|
];
|
|
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
export function NewProjectDialog() {
|
2026-02-17 10:53:20 -06:00
|
|
|
const { newProjectOpen, closeNewProject } = useDialog();
|
|
|
|
|
const { selectedCompanyId, selectedCompany } = useCompany();
|
2026-02-17 12:24:48 -06:00
|
|
|
const queryClient = useQueryClient();
|
2026-02-17 10:53:20 -06:00
|
|
|
const [name, setName] = useState("");
|
|
|
|
|
const [description, setDescription] = useState("");
|
|
|
|
|
const [status, setStatus] = useState("planned");
|
2026-02-20 15:48:42 -06:00
|
|
|
const [goalIds, setGoalIds] = useState<string[]>([]);
|
2026-02-17 10:53:20 -06:00
|
|
|
const [targetDate, setTargetDate] = useState("");
|
|
|
|
|
const [expanded, setExpanded] = useState(false);
|
2026-02-25 21:36:06 -06:00
|
|
|
const [workspaceLocalPath, setWorkspaceLocalPath] = useState("");
|
|
|
|
|
const [workspaceRepoUrl, setWorkspaceRepoUrl] = useState("");
|
|
|
|
|
const [workspaceError, setWorkspaceError] = useState<string | null>(null);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
|
|
|
|
const [statusOpen, setStatusOpen] = useState(false);
|
|
|
|
|
const [goalOpen, setGoalOpen] = useState(false);
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const descriptionEditorRef = useRef<MarkdownEditorRef>(null);
|
2026-02-17 10:53:20 -06:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const { data: goals } = useQuery({
|
|
|
|
|
queryKey: queryKeys.goals.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => goalsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId && newProjectOpen,
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-26 07:41:58 -05:00
|
|
|
const { data: agents } = useQuery({
|
|
|
|
|
queryKey: queryKeys.agents.list(selectedCompanyId!),
|
|
|
|
|
queryFn: () => agentsApi.list(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId && newProjectOpen,
|
|
|
|
|
});
|
|
|
|
|
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
const { data: companyMembers } = useQuery({
|
|
|
|
|
queryKey: queryKeys.access.companyUserDirectory(selectedCompanyId!),
|
|
|
|
|
queryFn: () => accessApi.listUserDirectory(selectedCompanyId!),
|
|
|
|
|
enabled: !!selectedCompanyId && newProjectOpen,
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-26 07:41:58 -05:00
|
|
|
const mentionOptions = useMemo<MentionOption[]>(() => {
|
feat: implement multi-user access and invite flows (#3784)
## Thinking Path
> - Paperclip is the control plane for autonomous AI companies.
> - V1 needs to stay local-first while also supporting shared,
authenticated deployments.
> - Human operators need real identities, company membership, invite
flows, profile surfaces, and company-scoped access controls.
> - Agents and operators also need the existing issue, inbox, workspace,
approval, and plugin flows to keep working under those authenticated
boundaries.
> - This branch accumulated the multi-user implementation, follow-up QA
fixes, workspace/runtime refinements, invite UX improvements,
release-branch conflict resolution, and review hardening.
> - This pull request consolidates that branch onto the current `master`
branch as a single reviewable PR.
> - The benefit is a complete multi-user implementation path with tests
and docs carried forward without dropping existing branch work.
## What Changed
- Added authenticated human-user access surfaces: auth/session routes,
company user directory, profile settings, company access/member
management, join requests, and invite management.
- Added invite creation, invite landing, onboarding, logo/branding,
invite grants, deduped join requests, and authenticated multi-user E2E
coverage.
- Tightened company-scoped and instance-admin authorization across
board, plugin, adapter, access, issue, and workspace routes.
- Added profile-image URL validation hardening, avatar preservation on
name-only profile updates, and join-request uniqueness migration cleanup
for pending human requests.
- Added an atomic member role/status/grants update path so Company
Access saves no longer leave partially updated permissions.
- Improved issue chat, inbox, assignee identity rendering,
sidebar/account/company navigation, workspace routing, and execution
workspace reuse behavior for multi-user operation.
- Added and updated server/UI tests covering auth, invites, membership,
issue workspace inheritance, plugin authz, inbox/chat behavior, and
multi-user flows.
- Merged current `public-gh/master` into this branch, resolved all
conflicts, and verified no `pnpm-lock.yaml` change is included in this
PR diff.
## Verification
- `pnpm exec vitest run server/src/__tests__/issues-service.test.ts
ui/src/components/IssueChatThread.test.tsx ui/src/pages/Inbox.test.tsx`
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/plugin-routes-authz.test.ts`
- `pnpm exec vitest run server/src/__tests__/plugin-routes-authz.test.ts
server/src/__tests__/workspace-runtime-service-authz.test.ts
server/src/__tests__/access-validators.test.ts`
- `pnpm exec vitest run
server/src/__tests__/authz-company-access.test.ts
server/src/__tests__/routines-routes.test.ts
server/src/__tests__/sidebar-preferences-routes.test.ts
server/src/__tests__/approval-routes-idempotency.test.ts
server/src/__tests__/openclaw-invite-prompt-route.test.ts
server/src/__tests__/agent-cross-tenant-authz-routes.test.ts
server/src/__tests__/routines-e2e.test.ts`
- `pnpm exec vitest run server/src/__tests__/auth-routes.test.ts
ui/src/pages/CompanyAccess.test.tsx`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/db typecheck && pnpm --filter @paperclipai/server
typecheck`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `pnpm --filter @paperclipai/ui typecheck`
- `pnpm db:generate`
- `npx playwright test --config tests/e2e/playwright.config.ts --list`
- Confirmed branch has no uncommitted changes and is `0` commits behind
`public-gh/master` before PR creation.
- Confirmed no `pnpm-lock.yaml` change is staged or present in the PR
diff.
## Risks
- High review surface area: this PR contains the accumulated multi-user
branch plus follow-up fixes, so reviewers should focus especially on
company-boundary enforcement and authenticated-vs-local deployment
behavior.
- UI behavior changed across invites, inbox, issue chat, access
settings, and sidebar navigation; no browser screenshots are included in
this branch-consolidation PR.
- Plugin install, upgrade, and lifecycle/config mutations now require
instance-admin access, which is intentional but may change expectations
for non-admin board users.
- A join-request dedupe migration rejects duplicate pending human
requests before creating unique indexes; deployments with unusual
historical duplicates should review the migration behavior.
- Company member role/status/grant saves now use a new combined
endpoint; older separate endpoints remain for compatibility.
- Full production build was not run locally in this heartbeat; CI should
cover the full matrix.
## Model Used
- OpenAI Codex coding agent, GPT-5-based model, CLI/tool-use
environment. Exact deployed model identifier and context window were not
exposed by the runtime.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] If this change affects the UI, I have included before/after
screenshots
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] I will address all Greptile and reviewer comments before
requesting merge
Note on screenshots: this is a branch-consolidation PR for an
already-developed multi-user branch, and no browser screenshots were
captured during this heartbeat.
---------
Co-authored-by: dotta <dotta@example.com>
Co-authored-by: Paperclip <noreply@paperclip.ing>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 09:44:19 -05:00
|
|
|
return buildMarkdownMentionOptions({
|
|
|
|
|
agents,
|
|
|
|
|
members: companyMembers?.users,
|
|
|
|
|
});
|
|
|
|
|
}, [agents, companyMembers?.users]);
|
2026-03-26 07:41:58 -05:00
|
|
|
|
2026-02-17 12:24:48 -06:00
|
|
|
const createProject = useMutation({
|
|
|
|
|
mutationFn: (data: Record<string, unknown>) =>
|
|
|
|
|
projectsApi.create(selectedCompanyId!, data),
|
|
|
|
|
});
|
2026-02-17 10:53:20 -06:00
|
|
|
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
const uploadDescriptionImage = useMutation({
|
|
|
|
|
mutationFn: async (file: File) => {
|
|
|
|
|
if (!selectedCompanyId) throw new Error("No company selected");
|
|
|
|
|
return assetsApi.uploadImage(selectedCompanyId, file, "projects/drafts");
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
function reset() {
|
|
|
|
|
setName("");
|
|
|
|
|
setDescription("");
|
|
|
|
|
setStatus("planned");
|
2026-02-20 15:48:42 -06:00
|
|
|
setGoalIds([]);
|
2026-02-17 10:53:20 -06:00
|
|
|
setTargetDate("");
|
|
|
|
|
setExpanded(false);
|
2026-02-25 21:36:06 -06:00
|
|
|
setWorkspaceLocalPath("");
|
|
|
|
|
setWorkspaceRepoUrl("");
|
|
|
|
|
setWorkspaceError(null);
|
2026-02-17 10:53:20 -06:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 21:36:06 -06:00
|
|
|
const isAbsolutePath = (value: string) => value.startsWith("/") || /^[A-Za-z]:[\\/]/.test(value);
|
|
|
|
|
|
2026-04-01 23:21:22 +00:00
|
|
|
const looksLikeRepoUrl = (value: string) => {
|
2026-02-25 21:36:06 -06:00
|
|
|
try {
|
|
|
|
|
const parsed = new URL(value);
|
2026-04-01 21:05:48 +00:00
|
|
|
if (parsed.protocol !== "https:") return false;
|
2026-02-25 21:36:06 -06:00
|
|
|
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
|
|
|
return segments.length >= 2;
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deriveWorkspaceNameFromPath = (value: string) => {
|
|
|
|
|
const normalized = value.trim().replace(/[\\/]+$/, "");
|
|
|
|
|
const segments = normalized.split(/[\\/]/).filter(Boolean);
|
|
|
|
|
return segments[segments.length - 1] ?? "Local folder";
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const deriveWorkspaceNameFromRepo = (value: string) => {
|
|
|
|
|
try {
|
|
|
|
|
const parsed = new URL(value);
|
|
|
|
|
const segments = parsed.pathname.split("/").filter(Boolean);
|
|
|
|
|
const repo = segments[segments.length - 1]?.replace(/\.git$/i, "") ?? "";
|
|
|
|
|
return repo || "GitHub repo";
|
|
|
|
|
} catch {
|
|
|
|
|
return "GitHub repo";
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function handleSubmit() {
|
2026-02-17 10:53:20 -06:00
|
|
|
if (!selectedCompanyId || !name.trim()) return;
|
2026-02-25 21:36:06 -06:00
|
|
|
const localPath = workspaceLocalPath.trim();
|
|
|
|
|
const repoUrl = workspaceRepoUrl.trim();
|
|
|
|
|
|
2026-03-19 07:17:49 -05:00
|
|
|
if (localPath && !isAbsolutePath(localPath)) {
|
2026-02-25 21:36:06 -06:00
|
|
|
setWorkspaceError("Local folder must be a full absolute path.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-04-01 23:21:22 +00:00
|
|
|
if (repoUrl && !looksLikeRepoUrl(repoUrl)) {
|
2026-04-01 20:42:48 +00:00
|
|
|
setWorkspaceError("Repo must use a valid GitHub or GitHub Enterprise repo URL.");
|
2026-02-25 21:36:06 -06:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setWorkspaceError(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const created = await createProject.mutateAsync({
|
|
|
|
|
name: name.trim(),
|
|
|
|
|
description: description.trim() || undefined,
|
|
|
|
|
status,
|
|
|
|
|
color: PROJECT_COLORS[Math.floor(Math.random() * PROJECT_COLORS.length)],
|
|
|
|
|
...(goalIds.length > 0 ? { goalIds } : {}),
|
|
|
|
|
...(targetDate ? { targetDate } : {}),
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-19 07:17:49 -05:00
|
|
|
if (localPath || repoUrl) {
|
|
|
|
|
const workspacePayload: Record<string, unknown> = {
|
|
|
|
|
name: localPath
|
|
|
|
|
? deriveWorkspaceNameFromPath(localPath)
|
|
|
|
|
: deriveWorkspaceNameFromRepo(repoUrl),
|
|
|
|
|
...(localPath ? { cwd: localPath } : {}),
|
|
|
|
|
...(repoUrl ? { repoUrl } : {}),
|
|
|
|
|
};
|
|
|
|
|
await projectsApi.createWorkspace(created.id, workspacePayload);
|
2026-02-25 21:36:06 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.projects.list(selectedCompanyId) });
|
|
|
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.projects.detail(created.id) });
|
|
|
|
|
reset();
|
|
|
|
|
closeNewProject();
|
|
|
|
|
} catch {
|
|
|
|
|
// surface through createProject.isError
|
|
|
|
|
}
|
2026-02-17 10:53:20 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleKeyDown(e: React.KeyboardEvent) {
|
|
|
|
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
handleSubmit();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
const selectedGoals = (goals ?? []).filter((g) => goalIds.includes(g.id));
|
|
|
|
|
const availableGoals = (goals ?? []).filter((g) => !goalIds.includes(g.id));
|
2026-02-17 10:53:20 -06:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog
|
|
|
|
|
open={newProjectOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
reset();
|
|
|
|
|
closeNewProject();
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DialogContent
|
|
|
|
|
showCloseButton={false}
|
|
|
|
|
className={cn("p-0 gap-0", expanded ? "sm:max-w-2xl" : "sm:max-w-lg")}
|
|
|
|
|
onKeyDown={handleKeyDown}
|
|
|
|
|
>
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between px-4 py-2.5 border-b border-border">
|
|
|
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
|
|
|
{selectedCompany && (
|
|
|
|
|
<span className="bg-muted px-1.5 py-0.5 rounded text-xs font-medium">
|
|
|
|
|
{selectedCompany.name.slice(0, 3).toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
<span className="text-muted-foreground/60">›</span>
|
|
|
|
|
<span>New project</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="text-muted-foreground"
|
|
|
|
|
onClick={() => setExpanded(!expanded)}
|
|
|
|
|
>
|
|
|
|
|
{expanded ? <Minimize2 className="h-3.5 w-3.5" /> : <Maximize2 className="h-3.5 w-3.5" />}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="ghost"
|
|
|
|
|
size="icon-xs"
|
|
|
|
|
className="text-muted-foreground"
|
|
|
|
|
onClick={() => { reset(); closeNewProject(); }}
|
|
|
|
|
>
|
|
|
|
|
<span className="text-lg leading-none">×</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Name */}
|
2026-02-23 14:41:21 -06:00
|
|
|
<div className="px-4 pt-4 pb-2 shrink-0">
|
2026-02-17 10:53:20 -06:00
|
|
|
<input
|
2026-02-23 14:41:21 -06:00
|
|
|
className="w-full text-lg font-semibold bg-transparent outline-none placeholder:text-muted-foreground/50"
|
2026-02-17 10:53:20 -06:00
|
|
|
placeholder="Project name"
|
|
|
|
|
value={name}
|
|
|
|
|
onChange={(e) => setName(e.target.value)}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
onKeyDown={(e) => {
|
|
|
|
|
if (e.key === "Tab" && !e.shiftKey) {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
descriptionEditorRef.current?.focus();
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-02-17 10:53:20 -06:00
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Description */}
|
|
|
|
|
<div className="px-4 pb-2">
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
<MarkdownEditor
|
|
|
|
|
ref={descriptionEditorRef}
|
2026-02-17 10:53:20 -06:00
|
|
|
value={description}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
onChange={setDescription}
|
|
|
|
|
placeholder="Add description..."
|
|
|
|
|
bordered={false}
|
2026-03-26 07:41:58 -05:00
|
|
|
mentions={mentionOptions}
|
Add MarkdownEditor component, asset image upload, and rich description editing
Introduce MarkdownEditor built on @mdxeditor/editor with headings,
lists, links, quotes, image upload with drag-and-drop, and themed CSS
integration. Add asset image upload API (routes, service, storage) and
wire image upload into InlineEditor multiline mode, NewIssueDialog,
NewProjectDialog, GoalDetail, IssueDetail, and ProjectDetail
description fields. Tighten prompt template editor styling.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 12:50:45 -06:00
|
|
|
contentClassName={cn("text-sm text-muted-foreground", expanded ? "min-h-[220px]" : "min-h-[120px]")}
|
|
|
|
|
imageUploadHandler={async (file) => {
|
|
|
|
|
const asset = await uploadDescriptionImage.mutateAsync(file);
|
|
|
|
|
return asset.contentPath;
|
|
|
|
|
}}
|
2026-02-17 10:53:20 -06:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-03-19 07:26:28 -05:00
|
|
|
<div className="px-4 pt-3 pb-3 space-y-3 border-t border-border">
|
|
|
|
|
<div>
|
2026-03-19 07:17:49 -05:00
|
|
|
<div className="mb-1 flex items-center gap-1.5">
|
|
|
|
|
<label className="block text-xs text-muted-foreground">Repo URL</label>
|
|
|
|
|
<span className="text-xs text-muted-foreground/50">optional</span>
|
|
|
|
|
<Tooltip delayDuration={300}>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<HelpCircle className="h-3 w-3 text-muted-foreground/50 cursor-help" />
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="top" className="max-w-[240px] text-xs">
|
|
|
|
|
Link a GitHub repository so agents can clone, read, and push code for this project.
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
|
|
|
|
</div>
|
|
|
|
|
<input
|
|
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs outline-none"
|
|
|
|
|
value={workspaceRepoUrl}
|
|
|
|
|
onChange={(e) => { setWorkspaceRepoUrl(e.target.value); setWorkspaceError(null); }}
|
|
|
|
|
placeholder="https://github.com/org/repo"
|
|
|
|
|
/>
|
2026-02-25 21:36:06 -06:00
|
|
|
</div>
|
|
|
|
|
|
2026-03-19 07:26:28 -05:00
|
|
|
<div>
|
2026-03-19 07:17:49 -05:00
|
|
|
<div className="mb-1 flex items-center gap-1.5">
|
|
|
|
|
<label className="block text-xs text-muted-foreground">Local folder</label>
|
|
|
|
|
<span className="text-xs text-muted-foreground/50">optional</span>
|
|
|
|
|
<Tooltip delayDuration={300}>
|
|
|
|
|
<TooltipTrigger asChild>
|
|
|
|
|
<HelpCircle className="h-3 w-3 text-muted-foreground/50 cursor-help" />
|
|
|
|
|
</TooltipTrigger>
|
|
|
|
|
<TooltipContent side="top" className="max-w-[240px] text-xs">
|
|
|
|
|
Set an absolute path on this machine where local agents will read and write files for this project.
|
|
|
|
|
</TooltipContent>
|
|
|
|
|
</Tooltip>
|
2026-02-25 21:36:06 -06:00
|
|
|
</div>
|
2026-03-19 07:17:49 -05:00
|
|
|
<div className="flex items-center gap-2">
|
2026-02-25 21:36:06 -06:00
|
|
|
<input
|
2026-03-19 07:17:49 -05:00
|
|
|
className="w-full rounded border border-border bg-transparent px-2 py-1 text-xs font-mono outline-none"
|
|
|
|
|
value={workspaceLocalPath}
|
|
|
|
|
onChange={(e) => { setWorkspaceLocalPath(e.target.value); setWorkspaceError(null); }}
|
|
|
|
|
placeholder="/absolute/path/to/workspace"
|
2026-02-25 21:36:06 -06:00
|
|
|
/>
|
2026-03-19 07:17:49 -05:00
|
|
|
<ChoosePathButton />
|
2026-02-25 21:36:06 -06:00
|
|
|
</div>
|
2026-03-19 07:17:49 -05:00
|
|
|
</div>
|
|
|
|
|
|
2026-02-25 21:36:06 -06:00
|
|
|
{workspaceError && (
|
|
|
|
|
<p className="text-xs text-destructive">{workspaceError}</p>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
{/* Property chips */}
|
|
|
|
|
<div className="flex items-center gap-1.5 px-4 py-2 border-t border-border flex-wrap">
|
|
|
|
|
{/* Status */}
|
|
|
|
|
<Popover open={statusOpen} onOpenChange={setStatusOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors">
|
|
|
|
|
<StatusBadge status={status} />
|
|
|
|
|
</button>
|
|
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-40 p-1" align="start">
|
|
|
|
|
{projectStatuses.map((s) => (
|
|
|
|
|
<button
|
|
|
|
|
key={s.value}
|
|
|
|
|
className={cn(
|
|
|
|
|
"flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50",
|
|
|
|
|
s.value === status && "bg-accent"
|
|
|
|
|
)}
|
|
|
|
|
onClick={() => { setStatus(s.value); setStatusOpen(false); }}
|
|
|
|
|
>
|
|
|
|
|
{s.label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
|
2026-02-20 15:48:42 -06:00
|
|
|
{selectedGoals.map((goal) => (
|
|
|
|
|
<span
|
|
|
|
|
key={goal.id}
|
|
|
|
|
className="inline-flex items-center gap-1 rounded-md border border-border px-2 py-1 text-xs"
|
|
|
|
|
>
|
|
|
|
|
<Target className="h-3 w-3 text-muted-foreground" />
|
|
|
|
|
<span className="max-w-[160px] truncate">{goal.title}</span>
|
|
|
|
|
<button
|
|
|
|
|
className="text-muted-foreground hover:text-foreground"
|
|
|
|
|
onClick={() => setGoalIds((prev) => prev.filter((id) => id !== goal.id))}
|
|
|
|
|
aria-label={`Remove goal ${goal.title}`}
|
|
|
|
|
type="button"
|
|
|
|
|
>
|
|
|
|
|
<X className="h-3 w-3" />
|
|
|
|
|
</button>
|
|
|
|
|
</span>
|
|
|
|
|
))}
|
|
|
|
|
|
2026-02-17 10:53:20 -06:00
|
|
|
<Popover open={goalOpen} onOpenChange={setGoalOpen}>
|
|
|
|
|
<PopoverTrigger asChild>
|
|
|
|
|
<button
|
2026-02-20 15:48:42 -06:00
|
|
|
className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs hover:bg-accent/50 transition-colors disabled:opacity-60"
|
|
|
|
|
disabled={selectedGoals.length > 0 && availableGoals.length === 0}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
2026-02-20 15:48:42 -06:00
|
|
|
{selectedGoals.length > 0 ? <Plus className="h-3 w-3 text-muted-foreground" /> : <Target className="h-3 w-3 text-muted-foreground" />}
|
|
|
|
|
{selectedGoals.length > 0 ? "+ Goal" : "Goal"}
|
2026-02-17 10:53:20 -06:00
|
|
|
</button>
|
2026-02-20 15:48:42 -06:00
|
|
|
</PopoverTrigger>
|
|
|
|
|
<PopoverContent className="w-56 p-1" align="start">
|
|
|
|
|
{selectedGoals.length === 0 && (
|
|
|
|
|
<button
|
|
|
|
|
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 text-muted-foreground"
|
|
|
|
|
onClick={() => setGoalOpen(false)}
|
|
|
|
|
>
|
|
|
|
|
No goal
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
{availableGoals.map((g) => (
|
2026-02-17 10:53:20 -06:00
|
|
|
<button
|
|
|
|
|
key={g.id}
|
2026-02-20 15:48:42 -06:00
|
|
|
className="flex items-center gap-2 w-full px-2 py-1.5 text-xs rounded hover:bg-accent/50 truncate"
|
|
|
|
|
onClick={() => {
|
|
|
|
|
setGoalIds((prev) => [...prev, g.id]);
|
|
|
|
|
setGoalOpen(false);
|
|
|
|
|
}}
|
2026-02-17 10:53:20 -06:00
|
|
|
>
|
|
|
|
|
{g.title}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
2026-02-20 15:48:42 -06:00
|
|
|
{selectedGoals.length > 0 && availableGoals.length === 0 && (
|
|
|
|
|
<div className="px-2 py-1.5 text-xs text-muted-foreground">
|
|
|
|
|
All goals already selected.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-02-17 10:53:20 -06:00
|
|
|
</PopoverContent>
|
|
|
|
|
</Popover>
|
|
|
|
|
|
|
|
|
|
{/* Target date */}
|
|
|
|
|
<div className="inline-flex items-center gap-1.5 rounded-md border border-border px-2 py-1 text-xs">
|
|
|
|
|
<Calendar className="h-3 w-3 text-muted-foreground" />
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
|
|
|
|
className="bg-transparent outline-none text-xs w-24"
|
|
|
|
|
value={targetDate}
|
|
|
|
|
onChange={(e) => setTargetDate(e.target.value)}
|
|
|
|
|
placeholder="Target date"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
2026-02-25 21:36:06 -06:00
|
|
|
<div className="flex items-center justify-between px-4 py-2.5 border-t border-border">
|
|
|
|
|
{createProject.isError ? (
|
|
|
|
|
<p className="text-xs text-destructive">Failed to create project.</p>
|
|
|
|
|
) : (
|
|
|
|
|
<span />
|
|
|
|
|
)}
|
2026-02-17 10:53:20 -06:00
|
|
|
<Button
|
|
|
|
|
size="sm"
|
2026-02-17 12:24:48 -06:00
|
|
|
disabled={!name.trim() || createProject.isPending}
|
2026-02-17 10:53:20 -06:00
|
|
|
onClick={handleSubmit}
|
|
|
|
|
>
|
2026-03-02 16:44:03 -06:00
|
|
|
{createProject.isPending ? "Creating…" : "Create project"}
|
2026-02-17 10:53:20 -06:00
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|