@burdenoff/microfe-workspaces 2026.519.1 → 2026.528.1
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/dist/WorkspacesRoot.js +4 -1
- package/dist/WorkspacesRoot.js.map +1 -1
- package/dist/WorkspacesRoutes.js +4 -4
- package/dist/WorkspacesRoutes.js.map +1 -1
- package/dist/components/CreateProjectDialog.js +1 -1
- package/dist/components/CreateProjectDialog.js.map +1 -1
- package/dist/components/CreateWorkspaceDialog.js +2 -2
- package/dist/components/CreateWorkspaceDialog.js.map +1 -1
- package/dist/components/InviteMemberDialog.js +2 -2
- package/dist/components/InviteMemberDialog.js.map +1 -1
- package/dist/components/MemberListItem.js +71 -86
- package/dist/components/MemberListItem.js.map +1 -1
- package/dist/components/ProjectCard.js +12 -10
- package/dist/components/ProjectCard.js.map +1 -1
- package/dist/components/WorkspaceCard.js.map +1 -1
- package/dist/components/WorkspacesErrorBoundary.js +12 -4
- package/dist/components/WorkspacesErrorBoundary.js.map +1 -1
- package/dist/components/tags/ProjectTagsWidget.js +248 -0
- package/dist/components/tags/ProjectTagsWidget.js.map +1 -0
- package/dist/generated/global-operations.js +9 -1
- package/dist/generated/global-operations.js.map +1 -1
- package/dist/hooks/useOrgsWithPermission.js +55 -0
- package/dist/hooks/useOrgsWithPermission.js.map +1 -0
- package/dist/hooks/useWorkspacesEventEmitter.js.map +1 -1
- package/dist/hooks/useWorkspacesPageView.js +6 -4
- package/dist/hooks/useWorkspacesPageView.js.map +1 -1
- package/dist/pages/CreateWorkspacePage.js +173 -149
- package/dist/pages/CreateWorkspacePage.js.map +1 -1
- package/dist/pages/InviteAcceptancePage.js +28 -28
- package/dist/pages/InviteAcceptancePage.js.map +1 -1
- package/dist/pages/MembersListPage.js +380 -352
- package/dist/pages/MembersListPage.js.map +1 -1
- package/dist/pages/ProjectDetailPage.js +96 -97
- package/dist/pages/ProjectDetailPage.js.map +1 -1
- package/dist/pages/ProjectsListPage.js +307 -304
- package/dist/pages/ProjectsListPage.js.map +1 -1
- package/dist/pages/WorkspaceSettingsPage.js +94 -91
- package/dist/pages/WorkspaceSettingsPage.js.map +1 -1
- package/dist/pages/WorkspacesListPage.js +1 -1
- package/dist/pages/WorkspacesListPage.js.map +1 -1
- package/dist/pages/dashboard/DashboardCustomizer.js +68 -68
- package/dist/pages/dashboard/DashboardCustomizer.js.map +1 -1
- package/dist/pages/dashboard/DashboardGrid.js +52 -41
- package/dist/pages/dashboard/DashboardGrid.js.map +1 -1
- package/dist/pages/dashboard/DashboardPage.js +74 -65
- package/dist/pages/dashboard/DashboardPage.js.map +1 -1
- package/dist/pages/dashboard/WidgetWrapper.js +3 -3
- package/dist/pages/dashboard/WidgetWrapper.js.map +1 -1
- package/dist/pages/dashboard/config.js +126 -30
- package/dist/pages/dashboard/config.js.map +1 -1
- package/dist/pages/dashboard/hooks/useDashboardData.js +39 -2
- package/dist/pages/dashboard/hooks/useDashboardData.js.map +1 -1
- package/dist/pages/dashboard/widgets/AgentsWidget.js +109 -0
- package/dist/pages/dashboard/widgets/AgentsWidget.js.map +1 -0
- package/dist/pages/dashboard/widgets/GroupsWidget.js +6 -5
- package/dist/pages/dashboard/widgets/GroupsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/MembersWidget.js +6 -6
- package/dist/pages/dashboard/widgets/MembersWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/ProjectsWidget.js +8 -7
- package/dist/pages/dashboard/widgets/ProjectsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/QuickActionsWidget.js +72 -18
- package/dist/pages/dashboard/widgets/QuickActionsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/RecentActivityWidget.js +3 -3
- package/dist/pages/dashboard/widgets/RecentActivityWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/SessionsWidget.js +78 -0
- package/dist/pages/dashboard/widgets/SessionsWidget.js.map +1 -0
- package/dist/pages/dashboard/widgets/StorageWidget.js +35 -20
- package/dist/pages/dashboard/widgets/StorageWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/TagCloudWidget.js +2 -2
- package/dist/pages/dashboard/widgets/TagCloudWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/TipsWidget.js +81 -22
- package/dist/pages/dashboard/widgets/TipsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.js +6 -5
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/VibesWidget.js +64 -0
- package/dist/pages/dashboard/widgets/VibesWidget.js.map +1 -0
- package/dist/pages/dashboard/widgets/WelcomeWidget.js +5 -5
- package/dist/pages/dashboard/widgets/WelcomeWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.js +3 -3
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.js.map +1 -1
- package/dist/pages/dashboard/widgets/WorkspacesWidget.js +6 -5
- package/dist/pages/dashboard/widgets/WorkspacesWidget.js.map +1 -1
- package/dist/providers/WorkspacesProvider.js +1 -1
- package/dist/providers/WorkspacesProvider.js.map +1 -1
- package/dist/store/workspacesStore.js.map +1 -1
- package/dist/utils/formatters.js +21 -16
- package/dist/utils/formatters.js.map +1 -1
- package/dist/utils/telemetryError.js +1 -1
- package/dist/utils/telemetryError.js.map +1 -1
- package/package.json +7 -7
- package/dist/WorkspacesRoot.d.ts +0 -35
- package/dist/WorkspacesRoot.d.ts.map +0 -1
- package/dist/WorkspacesRoutes.d.ts +0 -10
- package/dist/WorkspacesRoutes.d.ts.map +0 -1
- package/dist/components/CreateProjectDialog.d.ts +0 -6
- package/dist/components/CreateProjectDialog.d.ts.map +0 -1
- package/dist/components/CreateWorkspaceDialog.d.ts +0 -2
- package/dist/components/CreateWorkspaceDialog.d.ts.map +0 -1
- package/dist/components/InviteMemberDialog.d.ts +0 -7
- package/dist/components/InviteMemberDialog.d.ts.map +0 -1
- package/dist/components/MemberListItem.d.ts +0 -11
- package/dist/components/MemberListItem.d.ts.map +0 -1
- package/dist/components/ProjectCard.d.ts +0 -13
- package/dist/components/ProjectCard.d.ts.map +0 -1
- package/dist/components/WorkspaceCard.d.ts +0 -8
- package/dist/components/WorkspaceCard.d.ts.map +0 -1
- package/dist/components/WorkspacesErrorBoundary.d.ts +0 -27
- package/dist/components/WorkspacesErrorBoundary.d.ts.map +0 -1
- package/dist/components/index.d.ts +0 -8
- package/dist/components/index.d.ts.map +0 -1
- package/dist/constants/permissions.d.ts +0 -33
- package/dist/constants/permissions.d.ts.map +0 -1
- package/dist/generated/global-operations.d.ts +0 -279
- package/dist/generated/global-operations.d.ts.map +0 -1
- package/dist/generated/global-types.d.ts +0 -23527
- package/dist/generated/global-types.d.ts.map +0 -1
- package/dist/generated/wspace-operations.d.ts +0 -2300
- package/dist/generated/wspace-operations.d.ts.map +0 -1
- package/dist/generated/wspace-types.d.ts +0 -57851
- package/dist/generated/wspace-types.d.ts.map +0 -1
- package/dist/hooks/index.d.ts +0 -8
- package/dist/hooks/index.d.ts.map +0 -1
- package/dist/hooks/useMemberMutations.d.ts +0 -61
- package/dist/hooks/useMemberMutations.d.ts.map +0 -1
- package/dist/hooks/useProjectMutations.d.ts +0 -26
- package/dist/hooks/useProjectMutations.d.ts.map +0 -1
- package/dist/hooks/useProjects.d.ts +0 -16
- package/dist/hooks/useProjects.d.ts.map +0 -1
- package/dist/hooks/useScopedNavigation.d.ts +0 -4
- package/dist/hooks/useScopedNavigation.d.ts.map +0 -1
- package/dist/hooks/useTenants.d.ts +0 -13
- package/dist/hooks/useTenants.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceActivity.d.ts +0 -12
- package/dist/hooks/useWorkspaceActivity.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceMembers.d.ts +0 -24
- package/dist/hooks/useWorkspaceMembers.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceMutations.d.ts +0 -36
- package/dist/hooks/useWorkspaceMutations.d.ts.map +0 -1
- package/dist/hooks/useWorkspacePermissions.d.ts +0 -29
- package/dist/hooks/useWorkspacePermissions.d.ts.map +0 -1
- package/dist/hooks/useWorkspaceRoles.d.ts +0 -8
- package/dist/hooks/useWorkspaceRoles.d.ts.map +0 -1
- package/dist/hooks/useWorkspaces.d.ts +0 -21
- package/dist/hooks/useWorkspaces.d.ts.map +0 -1
- package/dist/hooks/useWorkspacesEventEmitter.d.ts +0 -18
- package/dist/hooks/useWorkspacesEventEmitter.d.ts.map +0 -1
- package/dist/hooks/useWorkspacesPageView.d.ts +0 -27
- package/dist/hooks/useWorkspacesPageView.d.ts.map +0 -1
- package/dist/index.d.ts +0 -33
- package/dist/index.d.ts.map +0 -1
- package/dist/pages/ActivityListPage.d.ts +0 -2
- package/dist/pages/ActivityListPage.d.ts.map +0 -1
- package/dist/pages/CreateWorkspacePage.d.ts +0 -2
- package/dist/pages/CreateWorkspacePage.d.ts.map +0 -1
- package/dist/pages/InviteAcceptancePage.d.ts +0 -6
- package/dist/pages/InviteAcceptancePage.d.ts.map +0 -1
- package/dist/pages/MembersListPage.d.ts +0 -2
- package/dist/pages/MembersListPage.d.ts.map +0 -1
- package/dist/pages/ProjectDetailPage.d.ts +0 -2
- package/dist/pages/ProjectDetailPage.d.ts.map +0 -1
- package/dist/pages/ProjectsListPage.d.ts +0 -2
- package/dist/pages/ProjectsListPage.d.ts.map +0 -1
- package/dist/pages/WorkspaceDetailPage.d.ts +0 -2
- package/dist/pages/WorkspaceDetailPage.d.ts.map +0 -1
- package/dist/pages/WorkspaceSettingsPage.d.ts +0 -2
- package/dist/pages/WorkspaceSettingsPage.d.ts.map +0 -1
- package/dist/pages/WorkspacesListPage.d.ts +0 -2
- package/dist/pages/WorkspacesListPage.d.ts.map +0 -1
- package/dist/pages/dashboard/DashboardCustomizer.d.ts +0 -12
- package/dist/pages/dashboard/DashboardCustomizer.d.ts.map +0 -1
- package/dist/pages/dashboard/DashboardGrid.d.ts +0 -8
- package/dist/pages/dashboard/DashboardGrid.d.ts.map +0 -1
- package/dist/pages/dashboard/DashboardPage.d.ts +0 -14
- package/dist/pages/dashboard/DashboardPage.d.ts.map +0 -1
- package/dist/pages/dashboard/WidgetWrapper.d.ts +0 -17
- package/dist/pages/dashboard/WidgetWrapper.d.ts.map +0 -1
- package/dist/pages/dashboard/config.d.ts +0 -9
- package/dist/pages/dashboard/config.d.ts.map +0 -1
- package/dist/pages/dashboard/hooks/useDashboardData.d.ts +0 -9
- package/dist/pages/dashboard/hooks/useDashboardData.d.ts.map +0 -1
- package/dist/pages/dashboard/hooks/useDashboardPreferences.d.ts +0 -4
- package/dist/pages/dashboard/hooks/useDashboardPreferences.d.ts.map +0 -1
- package/dist/pages/dashboard/types.d.ts +0 -145
- package/dist/pages/dashboard/types.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/GroupsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/GroupsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/MembersWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/MembersWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/ProjectsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/ProjectsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/QuickActionsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/QuickActionsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/RecentActivityWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/RecentActivityWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/StorageWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/StorageWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/SystemHealthWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/SystemHealthWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/SystemHealthWidget.js +0 -59
- package/dist/pages/dashboard/widgets/SystemHealthWidget.js.map +0 -1
- package/dist/pages/dashboard/widgets/TagCloudWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/TagCloudWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/TipsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/TipsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/UpcomingEventsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/WelcomeWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/WelcomeWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/WorkflowStatsWidget.d.ts.map +0 -1
- package/dist/pages/dashboard/widgets/WorkspacesWidget.d.ts +0 -4
- package/dist/pages/dashboard/widgets/WorkspacesWidget.d.ts.map +0 -1
- package/dist/providers/WorkspacesProvider.d.ts +0 -13
- package/dist/providers/WorkspacesProvider.d.ts.map +0 -1
- package/dist/store/workspacesStore.d.ts +0 -82
- package/dist/store/workspacesStore.d.ts.map +0 -1
- package/dist/types/index.d.ts +0 -280
- package/dist/types/index.d.ts.map +0 -1
- package/dist/utils/formatters.d.ts +0 -60
- package/dist/utils/formatters.d.ts.map +0 -1
- package/dist/utils/i18n.d.ts +0 -13
- package/dist/utils/i18n.d.ts.map +0 -1
- package/dist/utils/index.d.ts +0 -3
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/nativeBridge.d.ts +0 -3
- package/dist/utils/nativeBridge.d.ts.map +0 -1
- package/dist/utils/projectTags.d.ts +0 -6
- package/dist/utils/projectTags.d.ts.map +0 -1
- package/dist/utils/slug.d.ts +0 -13
- package/dist/utils/slug.d.ts.map +0 -1
- package/dist/utils/telemetryError.d.ts +0 -2
- package/dist/utils/telemetryError.d.ts.map +0 -1
- package/dist/utils/tokenHelpers.d.ts +0 -114
- package/dist/utils/tokenHelpers.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProjectsListPage.js","names":[],"sources":["../../src/pages/ProjectsListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { FolderKanban, Plus, Grid3x3, List, Search } from 'lucide-react';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Button,\n Input,\n Card,\n CardContent,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useMyProjects } from '../hooks/useProjects';\nimport {\n useArchiveProject,\n useDeleteProject,\n useUnarchiveProject,\n} from '../hooks/useProjectMutations';\nimport { ProjectCard } from '../components/ProjectCard';\nimport { CreateProjectDialog } from '../components/CreateProjectDialog';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { tWithFallback } from '../utils/i18n';\nimport { normalizeProjectTags } from '../utils/projectTags';\nimport type { ProjectTag, Workspace } from '../types';\n\nexport function ProjectsListPage() {\n const { t } = useI18n();\n\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.projects', { workspaceId, requireParams: true });\n const {\n viewMode,\n setViewMode,\n projectFilters,\n setProjectFilters,\n clearProjectFilters,\n selectedProjectIds,\n toggleProjectSelection,\n clearProjectSelection,\n openCreateProjectDialog,\n isCreateProjectDialogOpen,\n } = useWorkspacesStore();\n\n const [searchValue, setSearchValue] = useState(projectFilters.search || '');\n const [activeTagFilters, setActiveTagFilters] = useState<ProjectTag[]>([]);\n const [matchAllTagFilters, setMatchAllTagFilters] = useState(true);\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setProjectFilters({ search: debouncedSearch });\n }, [debouncedSearch, setProjectFilters]);\n\n useEffect(() => {\n if (activeTagFilters.length === 0) {\n setProjectFilters({\n tagIds: undefined,\n tagHierarchyPaths: undefined,\n matchAllTags: undefined,\n includeDescendants: undefined,\n });\n return;\n }\n\n setProjectFilters({\n tagIds: activeTagFilters.map((tag) => tag.tagId),\n tagHierarchyPaths: activeTagFilters\n .map((tag) => tag.hierarchyPath)\n .filter((tagPath): tagPath is string => typeof tagPath === 'string' && tagPath.length > 0),\n matchAllTags: matchAllTagFilters,\n includeDescendants: true,\n });\n }, [activeTagFilters, matchAllTagFilters, setProjectFilters]);\n\n useEffect(() => {\n return () => clearProjectSelection();\n }, [clearProjectSelection]);\n\n // Membership-scoped query: backend filters by actorId, so no client-side\n // permission gate is needed. Page is shared by `/workspaces/projects` and\n // `/workspaces/:workspaceId/projects`; the latter narrows the list below.\n const PROJECTS_PAGE_SIZE = 500;\n const {\n data: myData,\n isLoading,\n error,\n } = useMyProjects({\n limit: PROJECTS_PAGE_SIZE,\n offset: 0,\n });\n\n const data = useMemo(() => {\n const all = myData?.items ?? [];\n const q = (projectFilters.search ?? '').trim().toLowerCase();\n const tagIds = projectFilters.tagIds ?? [];\n const matchAllTags = projectFilters.matchAllTags ?? true;\n\n const matchesWorkspace = (project: (typeof all)[number]) =>\n !workspaceId || project.workspaceId === workspaceId;\n const matchesSearch = (project: (typeof all)[number]) =>\n !q || project.name.toLowerCase().includes(q) || project.key.toLowerCase().includes(q);\n const matchesStatus = (project: (typeof all)[number]) =>\n !projectFilters.status || project.status === projectFilters.status;\n const matchesArchived = (project: (typeof all)[number]) =>\n typeof projectFilters.archived === 'undefined' ||\n project.archived === projectFilters.archived;\n const matchesTags = (project: (typeof all)[number]) => {\n if (tagIds.length === 0) return true;\n const projectTagIds = new Set(normalizeProjectTags(project.tags).map((t) => t.tagId));\n return matchAllTags\n ? tagIds.every((id) => projectTagIds.has(id))\n : tagIds.some((id) => projectTagIds.has(id));\n };\n\n const filtered = all.filter(\n (project) =>\n matchesWorkspace(project) &&\n matchesSearch(project) &&\n matchesStatus(project) &&\n matchesArchived(project) &&\n matchesTags(project)\n );\n\n // Surface the real backend totals so the \"Showing X of Y\" footer can flag\n // truncation. For narrowed views (workspace/tag filter), the backend total\n // doesn't reflect the post-filter count, so we report the filtered size as\n // total. But we MUST preserve `hasMore` from upstream — if the upstream\n // query was capped (hasMore=true), matches outside the first page are\n // silently hidden, and the footer needs to keep flagging that.\n const apiTotal = myData?.total ?? filtered.length;\n const apiHasMore = myData?.hasMore ?? false;\n const isNarrowed = !!workspaceId || tagIds.length > 0;\n return {\n items: filtered,\n total: isNarrowed ? filtered.length : apiTotal,\n hasMore: apiHasMore,\n };\n }, [myData?.items, myData?.total, myData?.hasMore, projectFilters, workspaceId]);\n\n const { mutateAsync: archiveProject } = useArchiveProject(workspaceId);\n const { mutateAsync: unarchiveProject } = useUnarchiveProject(workspaceId);\n const { mutateAsync: deleteProject } = useDeleteProject(workspaceId);\n\n const selectedProjects = useMemo(\n () => data?.items.filter((project) => selectedProjectIds.has(project.id)) ?? [],\n [data?.items, selectedProjectIds]\n );\n\n const archivableProjects = useMemo(\n () => selectedProjects.filter((project) => !project.archived),\n [selectedProjects]\n );\n\n const unarchivableProjects = useMemo(\n () => selectedProjects.filter((project) => project.archived),\n [selectedProjects]\n );\n\n const allVisibleSelected =\n (data?.items.length ?? 0) > 0 &&\n (data?.items ?? []).every((project) => selectedProjectIds.has(project.id));\n const partiallySelected =\n (data?.items ?? []).some((project) => selectedProjectIds.has(project.id)) &&\n !allVisibleSelected;\n\n const {\n canCreateProject,\n canDeleteProject,\n canArchiveProject,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n );\n }\n\n // No client-side list gate: this page calls `myProjects` which is\n // membership-scoped — the resolver filters by actorId on the backend, so the\n // data set returned IS the access list. If the user is in zero projects, the\n // page renders the empty state below. Per-action checks (canCreateProject,\n // canDeleteProject, canArchiveProject) remain in place to hide buttons the\n // user isn't authorized for on their workspace.\n\n const handleCreateProject = () => {\n openCreateProjectDialog({} as Workspace);\n };\n\n const handleStatusFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.status;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ status: value as 'ACTIVE' | 'INACTIVE' | 'ARCHIVED' });\n }\n };\n\n const handleArchivedFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.archived;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ archived: value === 'true' });\n }\n };\n\n const toggleTagFilter = (tag: ProjectTag) => {\n setActiveTagFilters((current) => {\n const exists = current.some((entry) => entry.tagId === tag.tagId);\n if (exists) {\n return current.filter((entry) => entry.tagId !== tag.tagId);\n }\n\n return [...current, tag];\n });\n };\n\n const clearAllProjectFilters = () => {\n setSearchValue('');\n setActiveTagFilters([]);\n setMatchAllTagFilters(true);\n clearProjectFilters();\n };\n\n const toggleAllProjects = (checked: boolean) => {\n (data?.items ?? []).forEach((project) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n });\n };\n\n const handleBulkArchive = async () => {\n if (archivableProjects.length === 0) return;\n await Promise.allSettled(archivableProjects.map((project) => archiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkUnarchive = async () => {\n if (unarchivableProjects.length === 0) return;\n await Promise.allSettled(unarchivableProjects.map((project) => unarchiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkDelete = async () => {\n if (selectedProjects.length === 0) return;\n void nativeImpact('medium');\n const message =\n tWithFallback(t, 'projectsList.bulkDeleteConfirmPrefix', 'Delete ') +\n selectedProjects.length +\n ' ' +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmProject', 'selected project') +\n (selectedProjects.length === 1\n ? ''\n : tWithFallback(t, 'projectsList.bulkDeleteConfirmPluralSuffix', 's')) +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmSuffix', '? This cannot be undone.');\n const native = await nativeConfirm({\n title: 'Delete projects',\n message,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n await Promise.allSettled(selectedProjects.map((project) => deleteProject(project.id)));\n clearProjectSelection();\n };\n\n return (\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'projectsList.pageTitle', 'Projects')}\n </h1>\n <p className=\"text-muted-foreground\">\n {tWithFallback(t, 'projectsList.pageSubtitle', 'Manage and organize your projects')}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* View mode toggle */}\n <div className=\"flex items-center gap-1 border rounded-lg p-1\">\n <Button\n variant={viewMode === 'grid' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('grid')}\n className=\"h-8 w-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.gridViewAriaLabel',\n 'Show projects in grid view'\n )}\n >\n <Grid3x3 size={16} />\n </Button>\n <Button\n variant={viewMode === 'list' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('list')}\n className=\"h-8 w-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.listViewAriaLabel',\n 'Show projects in list view'\n )}\n >\n <List size={16} />\n </Button>\n </div>\n {canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus className=\"h-4 w-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </span>\n </Button>\n )}\n </div>\n </div>\n\n {/* Filters & Search */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"relative flex-1\">\n <Search\n className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground\"\n size={18}\n />\n <Input\n placeholder={tWithFallback(t, 'projectsList.searchPlaceholder', 'Search projects...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-[3.25rem]\"\n />\n </div>\n <Select value={projectFilters.status || 'all'} onValueChange={handleStatusFilter}>\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByStatus', 'Filter by status')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.statusAll', 'All Statuses')}\n </SelectItem>\n <SelectItem value=\"ACTIVE\">\n {tWithFallback(t, 'projectsList.statusActive', 'Active')}\n </SelectItem>\n <SelectItem value=\"INACTIVE\">\n {tWithFallback(t, 'projectsList.statusInactive', 'Inactive')}\n </SelectItem>\n </SelectContent>\n </Select>\n <Select\n value={\n projectFilters.archived === undefined\n ? 'all'\n : projectFilters.archived\n ? 'true'\n : 'false'\n }\n onValueChange={handleArchivedFilter}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByArchive', 'Filter by archive')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.archiveAll', 'All Projects')}\n </SelectItem>\n <SelectItem value=\"false\">\n {tWithFallback(t, 'projectsList.archiveActiveProjects', 'Active Projects')}\n </SelectItem>\n <SelectItem value=\"true\">\n {tWithFallback(t, 'projectsList.archiveArchived', 'Archived')}\n </SelectItem>\n </SelectContent>\n </Select>\n {(projectFilters.search ||\n projectFilters.status ||\n projectFilters.archived !== undefined ||\n activeTagFilters.length > 0) && (\n <Button variant=\"outline\" onClick={clearAllProjectFilters}>\n {tWithFallback(t, 'projectsList.clearFilters', 'Clear Filters')}\n </Button>\n )}\n </div>\n\n {activeTagFilters.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card px-4 py-3\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-medium\">\n {tWithFallback(t, 'projectsList.activeTagFilters', 'Active tag filters')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'projectsList.activeTagFiltersDescription',\n 'Click a project tag to add or remove it from the current filter set.'\n )}\n </p>\n </div>\n <Select\n value={matchAllTagFilters ? 'all' : 'any'}\n onValueChange={(value) => setMatchAllTagFilters(value === 'all')}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.tagMatchMode', 'Tag match mode')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.tagMatchAll', 'Match all tags')}\n </SelectItem>\n <SelectItem value=\"any\">\n {tWithFallback(t, 'projectsList.tagMatchAny', 'Match any tag')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {activeTagFilters.map((tag) => (\n <TagBadge\n key={tag.tagId}\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => toggleTagFilter(tag)}\n size=\"sm\"\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Error State */}\n {error && (\n <Card className=\"border-destructive\">\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-destructive font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.errorLoading', 'Error loading projects')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {error instanceof Error\n ? error.message\n : tWithFallback(t, 'projectsList.unknownError', 'An unknown error occurred')}\n </p>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <Card key={i}>\n <CardContent className=\"pt-6\">\n <div className=\"space-y-3\">\n <Skeleton className=\"h-12 w-12 rounded-lg\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && data?.items.length === 0 && (\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <div className=\"rounded-full bg-muted p-6 mb-4\">\n <FolderKanban size={48} className=\"text-muted-foreground\" />\n </div>\n <h3 className=\"text-lg font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.noProjectsFound', 'No projects found')}\n </h3>\n <p className=\"text-muted-foreground max-w-sm mb-4\">\n {projectFilters.search\n ? tWithFallback(\n t,\n 'projectsList.adjustSearchOrFilters',\n 'Try adjusting your search or filters'\n )\n : tWithFallback(\n t,\n 'projectsList.createFirstProject',\n 'Create your first project to get started organizing your work'\n )}\n </p>\n {!projectFilters.search && canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus size={16} className=\"mr-2\" />\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Projects Grid */}\n {!isLoading && !error && data && data.items.length > 0 && (\n <>\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-foreground\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllProjects(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'projectsList.selectAllAriaLabel',\n 'Select all projects'\n )}\n className=\"h-4 w-4 rounded border-border text-primary\"\n />\n <span>{tWithFallback(t, 'projectsList.selectAllVisible', 'Select all visible')}</span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-muted-foreground\">\n {selectedProjectIds.size} {tWithFallback(t, 'projectsList.selected', 'selected')}\n </span>\n {selectedProjectIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearProjectSelection}>\n {tWithFallback(t, 'projectsList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {/* Bulk mutations are bound to a single workspaceId via the\n archive/unarchive/delete hooks. On the cross-workspace\n /workspaces/projects route (workspaceId is undefined),\n hide them — selections can span workspaces and the bound\n hook would fail authorization for projects outside the\n caller's context. The per-card actions in ProjectCard\n still work because they resolve per-project. */}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkArchive}\n disabled={archivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.archiveSelected', 'Archive Selected')}\n {archivableProjects.length > 0 ? ` (${archivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkUnarchive}\n disabled={unarchivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.unarchiveSelected', 'Unarchive Selected')}\n {unarchivableProjects.length > 0 ? ` (${unarchivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canDeleteProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkDelete}\n disabled={selectedProjects.length === 0}\n className=\"text-destructive\"\n >\n {tWithFallback(t, 'projectsList.deleteSelected', 'Delete Selected')}\n {selectedProjects.length > 0 ? ` (${selectedProjects.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n\n <div\n className={\n viewMode === 'list'\n ? 'space-y-3'\n : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4'\n }\n >\n {data.items.map((project) => (\n <ProjectCard\n key={project.id}\n project={project}\n selected={selectedProjectIds.has(project.id)}\n canArchive={canArchiveProject}\n canDelete={canDeleteProject}\n onTagClick={toggleTagFilter}\n onToggleSelect={(checked) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n }}\n />\n ))}\n </div>\n\n {/* Pagination Info */}\n {data.total > data.items.length && (\n <div className=\"text-center text-sm text-muted-foreground\">\n {tWithFallback(t, 'projectsList.showingPrefix', 'Showing')} {data.items.length}{' '}\n {tWithFallback(t, 'projectsList.showingOf', 'of')} {data.total}{' '}\n {tWithFallback(t, 'projectsList.showingProjects', 'projects')}\n </div>\n )}\n </>\n )}\n\n {/* Create Project Dialog */}\n {isCreateProjectDialogOpen && <CreateProjectDialog workspaceId={workspaceId} />}\n </div>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkCA,SAAgB,IAAmB;CACjC,IAAM,EAAE,SAAM,IAAS,EAEjB,EAAE,mBAAgB,IAAoC;AAC5D,GAAsB,uBAAuB;EAAE;EAAa,eAAe;EAAM,CAAC;CAClF,IAAM,EACJ,aACA,gBACA,mBACA,sBACA,yBACA,uBACA,2BACA,0BACA,6BACA,kCACE,GAAoB,EAElB,CAAC,GAAa,KAAkB,EAAS,EAAe,UAAU,GAAG,EACrE,CAAC,GAAkB,KAAuB,EAAuB,EAAE,CAAC,EACpE,CAAC,GAAoB,KAAyB,EAAS,GAAK,EAC5D,IAAkB,GAAY,GAAa,IAAI;AA4BrD,CAzBA,QAAgB;AACd,IAAkB,EAAE,QAAQ,GAAiB,CAAC;IAC7C,CAAC,GAAiB,EAAkB,CAAC,EAExC,QAAgB;AACd,MAAI,EAAiB,WAAW,GAAG;AACjC,KAAkB;IAChB,QAAQ,KAAA;IACR,mBAAmB,KAAA;IACnB,cAAc,KAAA;IACd,oBAAoB,KAAA;IACrB,CAAC;AACF;;AAGF,IAAkB;GAChB,QAAQ,EAAiB,KAAK,MAAQ,EAAI,MAAM;GAChD,mBAAmB,EAChB,KAAK,MAAQ,EAAI,cAAc,CAC/B,QAAQ,MAA+B,OAAO,KAAY,YAAY,EAAQ,SAAS,EAAE;GAC5F,cAAc;GACd,oBAAoB;GACrB,CAAC;IACD;EAAC;EAAkB;EAAoB;EAAkB,CAAC,EAE7D,cACe,GAAuB,EACnC,CAAC,EAAsB,CAAC;CAM3B,IAAM,EACJ,MAAM,GACN,cACA,aACE,EAAc;EAChB,OANyB;EAOzB,QAAQ;EACT,CAAC,EAEI,IAAO,QAAc;EACzB,IAAM,IAAM,GAAQ,SAAS,EAAE,EACzB,KAAK,EAAe,UAAU,IAAI,MAAM,CAAC,aAAa,EACtD,IAAS,EAAe,UAAU,EAAE,EACpC,IAAe,EAAe,gBAAgB,IAE9C,KAAoB,MACxB,CAAC,KAAe,EAAQ,gBAAgB,GACpC,KAAiB,MACrB,CAAC,KAAK,EAAQ,KAAK,aAAa,CAAC,SAAS,EAAE,IAAI,EAAQ,IAAI,aAAa,CAAC,SAAS,EAAE,EACjF,KAAiB,MACrB,CAAC,EAAe,UAAU,EAAQ,WAAW,EAAe,QACxD,KAAmB,MAChB,EAAe,aAAa,UACnC,EAAQ,aAAa,EAAe,UAChC,KAAe,MAAkC;AACrD,OAAI,EAAO,WAAW,EAAG,QAAO;GAChC,IAAM,IAAgB,IAAI,IAAI,GAAqB,EAAQ,KAAK,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC;AACrF,UAAO,IACH,EAAO,OAAO,MAAO,EAAc,IAAI,EAAG,CAAC,GAC3C,EAAO,MAAM,MAAO,EAAc,IAAI,EAAG,CAAC;KAG1C,IAAW,EAAI,QAClB,MACC,EAAiB,EAAQ,IACzB,EAAc,EAAQ,IACtB,EAAc,EAAQ,IACtB,EAAgB,EAAQ,IACxB,EAAY,EAAQ,CACvB,EAQK,IAAW,GAAQ,SAAS,EAAS,QACrC,IAAa,GAAQ,WAAW;AAEtC,SAAO;GACL,OAAO;GACP,OAHmB,KAAe,EAAO,SAAS,IAG9B,EAAS,SAAS;GACtC,SAAS;GACV;IACA;EAAC,GAAQ;EAAO,GAAQ;EAAO,GAAQ;EAAS;EAAgB;EAAY,CAAC,EAE1E,EAAE,aAAa,OAAmB,EAAkB,EAAY,EAChE,EAAE,aAAa,OAAqB,EAAoB,EAAY,EACpE,EAAE,aAAa,OAAkB,EAAiB,EAAY,EAE9D,IAAmB,QACjB,GAAM,MAAM,QAAQ,MAAY,EAAmB,IAAI,EAAQ,GAAG,CAAC,IAAI,EAAE,EAC/E,CAAC,GAAM,OAAO,EAAmB,CAClC,EAEK,IAAqB,QACnB,EAAiB,QAAQ,MAAY,CAAC,EAAQ,SAAS,EAC7D,CAAC,EAAiB,CACnB,EAEK,IAAuB,QACrB,EAAiB,QAAQ,MAAY,EAAQ,SAAS,EAC5D,CAAC,EAAiB,CACnB,EAEK,KACH,GAAM,MAAM,UAAU,KAAK,MAC3B,GAAM,SAAS,EAAE,EAAE,OAAO,MAAY,EAAmB,IAAI,EAAQ,GAAG,CAAC,EACtE,MACH,GAAM,SAAS,EAAE,EAAE,MAAM,MAAY,EAAmB,IAAI,EAAQ,GAAG,CAAC,IACzE,CAAC,GAEG,EACJ,sBACA,qBACA,sBACA,WAAW,OACT,GAAyB;AAE7B,KAAI,GACF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD,EAAU,WAAU,cAAe,CAAA,EACnC,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,GAAD,EAAU,WAAU,QAAS,CAAA;IAC7B,kBAAC,GAAD,EAAU,WAAU,QAAS,CAAA;IAC7B,kBAAC,GAAD,EAAU,WAAU,QAAS,CAAA;IACzB;KACF;;CAWV,IAAM,WAA4B;AAChC,KAAwB,EAAE,CAAc;IAGpC,MAAsB,MAAkB;AAC5C,MAAI,MAAU,OAAO;GACnB,IAAM,IAAc,EAAE,GAAG,GAAgB;AAEzC,GADA,OAAO,EAAY,QACnB,EAAkB,EAAY;QAE9B,GAAkB,EAAE,QAAQ,GAA6C,CAAC;IAIxE,MAAwB,MAAkB;AAC9C,MAAI,MAAU,OAAO;GACnB,IAAM,IAAc,EAAE,GAAG,GAAgB;AAEzC,GADA,OAAO,EAAY,UACnB,EAAkB,EAAY;QAE9B,GAAkB,EAAE,UAAU,MAAU,QAAQ,CAAC;IAI/C,KAAmB,MAAoB;AAC3C,KAAqB,MACJ,EAAQ,MAAM,MAAU,EAAM,UAAU,EAAI,MAAM,GAExD,EAAQ,QAAQ,MAAU,EAAM,UAAU,EAAI,MAAM,GAGtD,CAAC,GAAG,GAAS,EAAI,CACxB;IAGE,WAA+B;AAInC,EAHA,EAAe,GAAG,EAClB,EAAoB,EAAE,CAAC,EACvB,EAAsB,GAAK,EAC3B,IAAqB;IAGjB,MAAqB,MAAqB;AAC9C,GAAC,GAAM,SAAS,EAAE,EAAE,SAAS,MAAY;AAEvC,GAAI,MADe,EAAmB,IAAI,EAAQ,GAAG,IAEnD,EAAuB,EAAQ,GAAG;IAEpC;;AAwCJ,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GAEE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eACX,EAAc,GAAG,0BAA0B,WAAW;KACpD,CAAA,EACL,kBAAC,KAAD;KAAG,WAAU;eACV,EAAc,GAAG,6BAA6B,oCAAoC;KACjF,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CAEE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,GAAD;OACE,SAAS,MAAa,SAAS,cAAc;OAC7C,MAAK;OACL,eAAe,EAAY,OAAO;OAClC,WAAU;OACV,cAAY,EACV,GACA,kCACA,6BACD;iBAED,kBAAC,IAAD,EAAS,MAAM,IAAM,CAAA;OACd,CAAA,EACT,kBAAC,GAAD;OACE,SAAS,MAAa,SAAS,cAAc;OAC7C,MAAK;OACL,eAAe,EAAY,OAAO;OAClC,WAAU;OACV,cAAY,EACV,GACA,kCACA,6BACD;iBAED,kBAAC,IAAD,EAAM,MAAM,IAAM,CAAA;OACX,CAAA,CACL;SACL,MACC,kBAAC,GAAD;MAAQ,SAAS;gBAAjB,CACE,kBAAC,GAAD,EAAM,WAAU,mBAAoB,CAAA,EACpC,kBAAC,QAAD;OAAM,WAAU;iBACb,EAAc,GAAG,8BAA8B,iBAAiB;OAC5D,CAAA,CACA;QAEP;OACF;;GAGN,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,IAAD;OACE,WAAU;OACV,MAAM;OACN,CAAA,EACF,kBAAC,IAAD;OACE,aAAa,EAAc,GAAG,kCAAkC,qBAAqB;OACrF,OAAO;OACP,WAAW,MAAM,EAAe,EAAE,OAAO,MAAM;OAC/C,WAAU;OACV,CAAA,CACE;;KACN,kBAAC,GAAD;MAAQ,OAAO,EAAe,UAAU;MAAO,eAAe;gBAA9D,CACE,kBAAC,GAAD;OAAe,WAAU;iBACvB,kBAAC,GAAD,EACE,aAAa,EAAc,GAAG,+BAA+B,mBAAmB,EAChF,CAAA;OACY,CAAA,EAChB,kBAAC,GAAD,EAAA,UAAA;OACE,kBAAC,GAAD;QAAY,OAAM;kBACf,EAAc,GAAG,0BAA0B,eAAe;QAChD,CAAA;OACb,kBAAC,GAAD;QAAY,OAAM;kBACf,EAAc,GAAG,6BAA6B,SAAS;QAC7C,CAAA;OACb,kBAAC,GAAD;QAAY,OAAM;kBACf,EAAc,GAAG,+BAA+B,WAAW;QACjD,CAAA;OACC,EAAA,CAAA,CACT;;KACT,kBAAC,GAAD;MACE,OACE,EAAe,aAAa,KAAA,IACxB,QACA,EAAe,WACb,SACA;MAER,eAAe;gBARjB,CAUE,kBAAC,GAAD;OAAe,WAAU;iBACvB,kBAAC,GAAD,EACE,aAAa,EAAc,GAAG,gCAAgC,oBAAoB,EAClF,CAAA;OACY,CAAA,EAChB,kBAAC,GAAD,EAAA,UAAA;OACE,kBAAC,GAAD;QAAY,OAAM;kBACf,EAAc,GAAG,2BAA2B,eAAe;QACjD,CAAA;OACb,kBAAC,GAAD;QAAY,OAAM;kBACf,EAAc,GAAG,sCAAsC,kBAAkB;QAC/D,CAAA;OACb,kBAAC,GAAD;QAAY,OAAM;kBACf,EAAc,GAAG,gCAAgC,WAAW;QAClD,CAAA;OACC,EAAA,CAAA,CACT;;MACP,EAAe,UACf,EAAe,UACf,EAAe,aAAa,KAAA,KAC5B,EAAiB,SAAS,MAC1B,kBAAC,GAAD;MAAQ,SAAQ;MAAU,SAAS;gBAChC,EAAc,GAAG,6BAA6B,gBAAgB;MACxD,CAAA;KAEP;;GAEL,EAAiB,SAAS,KACzB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAc,GAAG,iCAAiC,qBAAqB;MACtE,CAAA,EACJ,kBAAC,KAAD;MAAG,WAAU;gBACV,EACC,GACA,4CACA,uEACD;MACC,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,GAAD;MACE,OAAO,IAAqB,QAAQ;MACpC,gBAAgB,MAAU,EAAsB,MAAU,MAAM;gBAFlE,CAIE,kBAAC,GAAD;OAAe,WAAU;iBACvB,kBAAC,GAAD,EACE,aAAa,EAAc,GAAG,6BAA6B,iBAAiB,EAC5E,CAAA;OACY,CAAA,EAChB,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,GAAD;OAAY,OAAM;iBACf,EAAc,GAAG,4BAA4B,iBAAiB;OACpD,CAAA,EACb,kBAAC,GAAD;OAAY,OAAM;iBACf,EAAc,GAAG,4BAA4B,gBAAgB;OACnD,CAAA,CACC,EAAA,CAAA,CACT;QACL;QAEN,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAiB,KAAK,MACrB,kBAAC,IAAD;MAEE,IAAI,EAAI;MACR,OAAO,EAAI;MACX,OAAO,EAAI,SAAS,KAAA;MACpB,MAAM,EAAI,QAAQ,KAAA;MAClB,aAAa,EAAI,eAAe,KAAA;MAChC,WAAA;MACA,gBAAgB,EAAgB,EAAI;MACpC,MAAK;MACL,EATK,EAAI,MAST,CACF;KACE,CAAA,CACF;;GAIP,KACC,kBAAC,GAAD;IAAM,WAAU;cACd,kBAAC,GAAD;KAAa,WAAU;eACrB,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAc,GAAG,6BAA6B,yBAAyB;OACtE,CAAA,EACJ,kBAAC,KAAD;OAAG,WAAU;iBACV,aAAiB,QACd,EAAM,UACN,EAAc,GAAG,6BAA6B,4BAA4B;OAC5E,CAAA,CACA;;KACM,CAAA;IACT,CAAA;GAIR,KACC,kBAAC,OAAD;IAAK,WAAU;cACZ,MAAM,KAAK,EAAE,QAAQ,GAAG,CAAC,CAAC,KAAK,GAAG,MACjC,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;KAAa,WAAU;eACrB,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,GAAD,EAAU,WAAU,wBAAyB,CAAA;OAC7C,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA;OAClC,kBAAC,GAAD,EAAU,WAAU,aAAc,CAAA;OAC9B;;KACM,CAAA,EACT,EARI,EAQJ,CACP;IACE,CAAA;GAIP,CAAC,KAAa,CAAC,KAAS,GAAM,MAAM,WAAW,KAC9C,kBAAC,GAAD,EAAA,UACE,kBAAC,GAAD;IAAa,WAAU;cACrB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,IAAD;QAAc,MAAM;QAAI,WAAU;QAA0B,CAAA;OACxD,CAAA;MACN,kBAAC,MAAD;OAAI,WAAU;iBACX,EAAc,GAAG,gCAAgC,oBAAoB;OACnE,CAAA;MACL,kBAAC,KAAD;OAAG,WAAU;iBACV,EAAe,SACZ,EACE,GACA,sCACA,uCACD,GACD,EACE,GACA,mCACA,gEACD;OACH,CAAA;MACH,CAAC,EAAe,UAAU,MACzB,kBAAC,GAAD;OAAQ,SAAS;iBAAjB,CACE,kBAAC,GAAD;QAAM,MAAM;QAAI,WAAU;QAAS,CAAA,EAClC,EAAc,GAAG,8BAA8B,iBAAiB,CAC1D;;MAEP;;IACM,CAAA,EACT,CAAA;GAIR,CAAC,KAAa,CAAC,KAAS,KAAQ,EAAK,MAAM,SAAS,KACnD,kBAAA,IAAA,EAAA,UAAA;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,SAAD;MAAO,WAAU;gBAAjB,CACE,kBAAC,SAAD;OACE,MAAK;OACL,SAAS;OACT,MAAM,MAAU;AACd,QAAI,MACF,EAAM,gBAAgB,MAAqB,CAAC;;OAGhD,WAAW,MAAU,GAAkB,EAAM,OAAO,QAAQ;OAC5D,cAAY,EACV,GACA,mCACA,sBACD;OACD,WAAU;OACV,CAAA,EACF,kBAAC,QAAD,EAAA,UAAO,EAAc,GAAG,iCAAiC,qBAAqB,EAAQ,CAAA,CAChF;SAER,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,QAAD;QAAM,WAAU;kBAAhB;SACG,EAAmB;SAAK;SAAE,EAAc,GAAG,yBAAyB,WAAW;SAC3E;;OACN,EAAmB,OAAO,KACzB,kBAAC,GAAD;QAAQ,SAAQ;QAAQ,MAAK;QAAK,SAAS;kBACxC,EAAc,GAAG,+BAA+B,kBAAkB;QAC5D,CAAA;OASV,KAAe,KACd,kBAAC,GAAD;QACE,SAAQ;QACR,MAAK;QACL,SAtUU,YAAY;AAChC,WAAmB,WAAW,MAClC,MAAM,QAAQ,WAAW,EAAmB,KAAK,MAAY,GAAe,EAAQ,GAAG,CAAC,CAAC,EACzF,GAAuB;;QAoUT,UAAU,EAAmB,WAAW;kBAJ1C,CAMG,EAAc,GAAG,gCAAgC,mBAAmB,EACpE,EAAmB,SAAS,IAAI,KAAK,EAAmB,OAAO,KAAK,GAC9D;;OAEV,KAAe,KACd,kBAAC,GAAD;QACE,SAAQ;QACR,MAAK;QACL,SA3UY,YAAY;AAClC,WAAqB,WAAW,MACpC,MAAM,QAAQ,WAAW,EAAqB,KAAK,MAAY,GAAiB,EAAQ,GAAG,CAAC,CAAC,EAC7F,GAAuB;;QAyUT,UAAU,EAAqB,WAAW;kBAJ5C,CAMG,EAAc,GAAG,kCAAkC,qBAAqB,EACxE,EAAqB,SAAS,IAAI,KAAK,EAAqB,OAAO,KAAK,GAClE;;OAEV,KAAe,KACd,kBAAC,GAAD;QACE,SAAQ;QACR,MAAK;QACL,SAhVS,YAAY;AACnC,aAAI,EAAiB,WAAW,EAAG;AAC9B,WAAa,SAAS;SAC3B,IAAM,IACJ,EAAc,GAAG,wCAAwC,UAAU,GACnE,EAAiB,SACjB,MACA,EAAc,GAAG,yCAAyC,mBAAmB,IAC5E,EAAiB,WAAW,IACzB,KACA,EAAc,GAAG,8CAA8C,IAAI,IACvE,EAAc,GAAG,wCAAwC,2BAA2B,EAChF,IAAS,MAAM,EAAc;UACjC,OAAO;UACP;UACA,eAAe;UACf,mBAAmB;UACpB,CAAC;AACgB,gBAAW,OAAO,OAAO,QAAQ,EAAQ,GAAG,OAG9D,MAAM,QAAQ,WAAW,EAAiB,KAAK,MAAY,GAAc,EAAQ,GAAG,CAAC,CAAC,EACtF,GAAuB;;QA2TT,UAAU,EAAiB,WAAW;QACtC,WAAU;kBALZ,CAOG,EAAc,GAAG,+BAA+B,kBAAkB,EAClE,EAAiB,SAAS,IAAI,KAAK,EAAiB,OAAO,KAAK,GAC1D;;OAEP;QACF;;IAEN,kBAAC,OAAD;KACE,WACE,MAAa,SACT,cACA;eAGL,EAAK,MAAM,KAAK,MACf,kBAAC,GAAD;MAEW;MACT,UAAU,EAAmB,IAAI,EAAQ,GAAG;MAC5C,YAAY;MACZ,WAAW;MACX,YAAY;MACZ,iBAAiB,MAAY;AAE3B,OAAI,MADe,EAAmB,IAAI,EAAQ,GAAG,IAEnD,EAAuB,EAAQ,GAAG;;MAGtC,EAZK,EAAQ,GAYb,CACF;KACE,CAAA;IAGL,EAAK,QAAQ,EAAK,MAAM,UACvB,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,EAAc,GAAG,8BAA8B,UAAU;MAAC;MAAE,EAAK,MAAM;MAAQ;MAC/E,EAAc,GAAG,0BAA0B,KAAK;MAAC;MAAE,EAAK;MAAO;MAC/D,EAAc,GAAG,gCAAgC,WAAW;MACzD;;IAEP,EAAA,CAAA;GAIJ,MAA6B,kBAAC,GAAD,EAAkC,gBAAe,CAAA;GAC3E"}
|
|
1
|
+
{"version":3,"file":"ProjectsListPage.js","names":[],"sources":["../../src/pages/ProjectsListPage.tsx"],"sourcesContent":["import { useEffect, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { FolderKanban, Plus, Grid3x3, List, Search } from 'lucide-react';\nimport { TagBadge } from '@burdenoff/fe-libs/tag';\nimport {\n Button,\n Input,\n Card,\n CardContent,\n PageContainer,\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n Skeleton,\n} from '@burdenoff/fe-libs/ui';\nimport { useDebounce } from '@burdenoff/fe-libs/shared/hooks';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useWorkspacesStore } from '../store/workspacesStore';\nimport { nativeConfirm, nativeImpact } from '../utils/nativeBridge';\nimport { useMyProjects } from '../hooks/useProjects';\nimport {\n useArchiveProject,\n useDeleteProject,\n useUnarchiveProject,\n} from '../hooks/useProjectMutations';\nimport { ProjectCard } from '../components/ProjectCard';\nimport { CreateProjectDialog } from '../components/CreateProjectDialog';\nimport { useWorkspacePermissions } from '../hooks/useWorkspacePermissions';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\nimport { tWithFallback } from '../utils/i18n';\nimport { normalizeProjectTags } from '../utils/projectTags';\nimport type { ProjectTag, Workspace } from '../types';\n\nexport function ProjectsListPage() {\n const { t } = useI18n();\n\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.projects', { workspaceId, requireParams: true });\n const {\n viewMode,\n setViewMode,\n projectFilters,\n setProjectFilters,\n clearProjectFilters,\n selectedProjectIds,\n toggleProjectSelection,\n clearProjectSelection,\n openCreateProjectDialog,\n isCreateProjectDialogOpen,\n } = useWorkspacesStore();\n\n const [searchValue, setSearchValue] = useState(projectFilters.search || '');\n const [activeTagFilters, setActiveTagFilters] = useState<ProjectTag[]>([]);\n const [matchAllTagFilters, setMatchAllTagFilters] = useState(true);\n const debouncedSearch = useDebounce(searchValue, 500);\n\n // Update filters when debounced search changes\n useEffect(() => {\n setProjectFilters({ search: debouncedSearch });\n }, [debouncedSearch, setProjectFilters]);\n\n useEffect(() => {\n if (activeTagFilters.length === 0) {\n setProjectFilters({\n tagIds: undefined,\n tagHierarchyPaths: undefined,\n matchAllTags: undefined,\n includeDescendants: undefined,\n });\n return;\n }\n\n setProjectFilters({\n tagIds: activeTagFilters.map((tag) => tag.tagId),\n tagHierarchyPaths: activeTagFilters\n .map((tag) => tag.hierarchyPath)\n .filter((tagPath): tagPath is string => typeof tagPath === 'string' && tagPath.length > 0),\n matchAllTags: matchAllTagFilters,\n includeDescendants: true,\n });\n }, [activeTagFilters, matchAllTagFilters, setProjectFilters]);\n\n useEffect(() => {\n return () => clearProjectSelection();\n }, [clearProjectSelection]);\n\n // Membership-scoped query: backend filters by actorId, so no client-side\n // permission gate is needed. Page is shared by `/workspaces/projects` and\n // `/workspaces/:workspaceId/projects`; the latter narrows the list below.\n const PROJECTS_PAGE_SIZE = 500;\n const {\n data: myData,\n isLoading,\n error,\n } = useMyProjects({\n limit: PROJECTS_PAGE_SIZE,\n offset: 0,\n });\n\n const data = useMemo(() => {\n const all = myData?.items ?? [];\n const q = (projectFilters.search ?? '').trim().toLowerCase();\n const tagIds = projectFilters.tagIds ?? [];\n const matchAllTags = projectFilters.matchAllTags ?? true;\n\n const matchesWorkspace = (project: (typeof all)[number]) =>\n !workspaceId || project.workspaceId === workspaceId;\n const matchesSearch = (project: (typeof all)[number]) =>\n !q || project.name.toLowerCase().includes(q) || project.key.toLowerCase().includes(q);\n const matchesStatus = (project: (typeof all)[number]) =>\n !projectFilters.status || project.status === projectFilters.status;\n const matchesArchived = (project: (typeof all)[number]) =>\n typeof projectFilters.archived === 'undefined' ||\n project.archived === projectFilters.archived;\n const matchesTags = (project: (typeof all)[number]) => {\n if (tagIds.length === 0) return true;\n const projectTagIds = new Set(normalizeProjectTags(project.tags).map((t) => t.tagId));\n return matchAllTags\n ? tagIds.every((id) => projectTagIds.has(id))\n : tagIds.some((id) => projectTagIds.has(id));\n };\n\n const filtered = all.filter(\n (project) =>\n matchesWorkspace(project) &&\n matchesSearch(project) &&\n matchesStatus(project) &&\n matchesArchived(project) &&\n matchesTags(project)\n );\n\n // Surface the real backend totals so the \"Showing X of Y\" footer can flag\n // truncation. For narrowed views (workspace/tag filter), the backend total\n // doesn't reflect the post-filter count, so we report the filtered size as\n // total. But we MUST preserve `hasMore` from upstream — if the upstream\n // query was capped (hasMore=true), matches outside the first page are\n // silently hidden, and the footer needs to keep flagging that.\n const apiTotal = myData?.total ?? filtered.length;\n const apiHasMore = myData?.hasMore ?? false;\n const isNarrowed = !!workspaceId || tagIds.length > 0;\n return {\n items: filtered,\n total: isNarrowed ? filtered.length : apiTotal,\n hasMore: apiHasMore,\n };\n }, [myData?.items, myData?.total, myData?.hasMore, projectFilters, workspaceId]);\n\n const { mutateAsync: archiveProject } = useArchiveProject(workspaceId);\n const { mutateAsync: unarchiveProject } = useUnarchiveProject(workspaceId);\n const { mutateAsync: deleteProject } = useDeleteProject(workspaceId);\n\n const selectedProjects = useMemo(\n () => data?.items.filter((project) => selectedProjectIds.has(project.id)) ?? [],\n [data?.items, selectedProjectIds]\n );\n\n const archivableProjects = useMemo(\n () => selectedProjects.filter((project) => !project.archived),\n [selectedProjects]\n );\n\n const unarchivableProjects = useMemo(\n () => selectedProjects.filter((project) => project.archived),\n [selectedProjects]\n );\n\n const allVisibleSelected =\n (data?.items.length ?? 0) > 0 &&\n (data?.items ?? []).every((project) => selectedProjectIds.has(project.id));\n const partiallySelected =\n (data?.items ?? []).some((project) => selectedProjectIds.has(project.id)) &&\n !allVisibleSelected;\n\n const {\n canCreateProject,\n canDeleteProject,\n canArchiveProject,\n isLoading: permissionsLoading,\n } = useWorkspacePermissions();\n\n if (permissionsLoading) {\n return (\n <div className=\"space-y-4 p-6\">\n <Skeleton className=\"h-10 w-1/3\" />\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4\">\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n <Skeleton className=\"h-32\" />\n </div>\n </div>\n );\n }\n\n // No client-side list gate: this page calls `myProjects` which is\n // membership-scoped — the resolver filters by actorId on the backend, so the\n // data set returned IS the access list. If the user is in zero projects, the\n // page renders the empty state below. Per-action checks (canCreateProject,\n // canDeleteProject, canArchiveProject) remain in place to hide buttons the\n // user isn't authorized for on their workspace.\n\n const handleCreateProject = () => {\n openCreateProjectDialog({} as Workspace);\n };\n\n const handleStatusFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.status;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ status: value as 'ACTIVE' | 'INACTIVE' | 'ARCHIVED' });\n }\n };\n\n const handleArchivedFilter = (value: string) => {\n if (value === 'all') {\n const nextFilters = { ...projectFilters };\n delete nextFilters.archived;\n setProjectFilters(nextFilters);\n } else {\n setProjectFilters({ archived: value === 'true' });\n }\n };\n\n const toggleTagFilter = (tag: ProjectTag) => {\n setActiveTagFilters((current) => {\n const exists = current.some((entry) => entry.tagId === tag.tagId);\n if (exists) {\n return current.filter((entry) => entry.tagId !== tag.tagId);\n }\n\n return [...current, tag];\n });\n };\n\n const clearAllProjectFilters = () => {\n setSearchValue('');\n setActiveTagFilters([]);\n setMatchAllTagFilters(true);\n clearProjectFilters();\n };\n\n const toggleAllProjects = (checked: boolean) => {\n (data?.items ?? []).forEach((project) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n });\n };\n\n const handleBulkArchive = async () => {\n if (archivableProjects.length === 0) return;\n await Promise.allSettled(archivableProjects.map((project) => archiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkUnarchive = async () => {\n if (unarchivableProjects.length === 0) return;\n await Promise.allSettled(unarchivableProjects.map((project) => unarchiveProject(project.id)));\n clearProjectSelection();\n };\n\n const handleBulkDelete = async () => {\n if (selectedProjects.length === 0) return;\n void nativeImpact('medium');\n const message =\n tWithFallback(t, 'projectsList.bulkDeleteConfirmPrefix', 'Delete ') +\n selectedProjects.length +\n ' ' +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmProject', 'selected project') +\n (selectedProjects.length === 1\n ? ''\n : tWithFallback(t, 'projectsList.bulkDeleteConfirmPluralSuffix', 's')) +\n tWithFallback(t, 'projectsList.bulkDeleteConfirmSuffix', '? This cannot be undone.');\n const native = await nativeConfirm({\n title: 'Delete projects',\n message,\n okButtonTitle: 'Delete',\n cancelButtonTitle: 'Cancel',\n });\n const confirmed = native === null ? window.confirm(message) : native;\n if (!confirmed) return;\n\n await Promise.allSettled(selectedProjects.map((project) => deleteProject(project.id)));\n clearProjectSelection();\n };\n\n return (\n <PageContainer padded={false}>\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n {/* Header */}\n <div className=\"flex flex-col sm:flex-row sm:items-center justify-between gap-4\">\n <div>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'projectsList.pageTitle', 'Projects')}\n </h1>\n <p className=\"text-muted-foreground\">\n {tWithFallback(t, 'projectsList.pageSubtitle', 'Manage and organize your projects')}\n </p>\n </div>\n <div className=\"flex items-center gap-2\">\n {/* View mode toggle */}\n <div className=\"flex items-center gap-1 border rounded-lg p-1\">\n <Button\n variant={viewMode === 'grid' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('grid')}\n className=\"size-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.gridViewAriaLabel',\n 'Show projects in grid view'\n )}\n >\n <Grid3x3 size={16} />\n </Button>\n <Button\n variant={viewMode === 'list' ? 'secondary' : 'ghost'}\n size=\"sm\"\n onClick={() => setViewMode('list')}\n className=\"size-8 p-0\"\n aria-label={tWithFallback(\n t,\n 'projectsList.listViewAriaLabel',\n 'Show projects in list view'\n )}\n >\n <List size={16} />\n </Button>\n </div>\n {canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus className=\"size-4 sm:mr-2\" />\n <span className=\"hidden sm:inline\">\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </span>\n </Button>\n )}\n </div>\n </div>\n\n {/* Filters & Search */}\n <div className=\"flex flex-col sm:flex-row gap-3\">\n <div className=\"relative flex-1\">\n <Search\n className=\"absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground\"\n size={18}\n />\n <Input\n placeholder={tWithFallback(t, 'projectsList.searchPlaceholder', 'Search projects...')}\n value={searchValue}\n onChange={(e) => setSearchValue(e.target.value)}\n className=\"pl-[3.25rem]\"\n />\n </div>\n <Select value={projectFilters.status || 'all'} onValueChange={handleStatusFilter}>\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByStatus', 'Filter by status')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.statusAll', 'All Statuses')}\n </SelectItem>\n <SelectItem value=\"ACTIVE\">\n {tWithFallback(t, 'projectsList.statusActive', 'Active')}\n </SelectItem>\n <SelectItem value=\"INACTIVE\">\n {tWithFallback(t, 'projectsList.statusInactive', 'Inactive')}\n </SelectItem>\n </SelectContent>\n </Select>\n <Select\n value={\n projectFilters.archived === undefined\n ? 'all'\n : projectFilters.archived\n ? 'true'\n : 'false'\n }\n onValueChange={handleArchivedFilter}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.filterByArchive', 'Filter by archive')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.archiveAll', 'All Projects')}\n </SelectItem>\n <SelectItem value=\"false\">\n {tWithFallback(t, 'projectsList.archiveActiveProjects', 'Active Projects')}\n </SelectItem>\n <SelectItem value=\"true\">\n {tWithFallback(t, 'projectsList.archiveArchived', 'Archived')}\n </SelectItem>\n </SelectContent>\n </Select>\n {(projectFilters.search ||\n projectFilters.status ||\n projectFilters.archived !== undefined ||\n activeTagFilters.length > 0) && (\n <Button variant=\"outline\" onClick={clearAllProjectFilters}>\n {tWithFallback(t, 'projectsList.clearFilters', 'Clear Filters')}\n </Button>\n )}\n </div>\n\n {activeTagFilters.length > 0 && (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card px-4 py-3\">\n <div className=\"flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between\">\n <div>\n <p className=\"text-sm font-medium\">\n {tWithFallback(t, 'projectsList.activeTagFilters', 'Active tag filters')}\n </p>\n <p className=\"text-xs text-muted-foreground\">\n {tWithFallback(\n t,\n 'projectsList.activeTagFiltersDescription',\n 'Click a project tag to add or remove it from the current filter set.'\n )}\n </p>\n </div>\n <Select\n value={matchAllTagFilters ? 'all' : 'any'}\n onValueChange={(value) => setMatchAllTagFilters(value === 'all')}\n >\n <SelectTrigger className=\"w-full sm:w-[180px]\">\n <SelectValue\n placeholder={tWithFallback(t, 'projectsList.tagMatchMode', 'Tag match mode')}\n />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"all\">\n {tWithFallback(t, 'projectsList.tagMatchAll', 'Match all tags')}\n </SelectItem>\n <SelectItem value=\"any\">\n {tWithFallback(t, 'projectsList.tagMatchAny', 'Match any tag')}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"flex flex-wrap gap-2\">\n {activeTagFilters.map((tag) => (\n <TagBadge\n key={tag.tagId}\n id={tag.tagId}\n label={tag.label}\n color={tag.color ?? undefined}\n icon={tag.icon ?? undefined}\n description={tag.description ?? undefined}\n removable\n onRemove={() => toggleTagFilter(tag)}\n size=\"sm\"\n />\n ))}\n </div>\n </div>\n )}\n\n {/* Error State */}\n {error && (\n <Card className=\"border-destructive\">\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-8 text-center\">\n <p className=\"text-destructive font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.errorLoading', 'Error loading projects')}\n </p>\n <p className=\"text-sm text-muted-foreground\">\n {error instanceof Error\n ? error.message\n : tWithFallback(t, 'projectsList.unknownError', 'An unknown error occurred')}\n </p>\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Loading State */}\n {isLoading && (\n <div className=\"grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4\">\n {Array.from({ length: 6 }).map((_, i) => (\n <Card key={i}>\n <CardContent className=\"pt-6\">\n <div className=\"space-y-3\">\n <Skeleton className=\"size-12 rounded-lg\" />\n <Skeleton className=\"h-4 w-3/4\" />\n <Skeleton className=\"h-4 w-1/2\" />\n </div>\n </CardContent>\n </Card>\n ))}\n </div>\n )}\n\n {/* Empty State */}\n {!isLoading && !error && data?.items.length === 0 && (\n <Card>\n <CardContent className=\"pt-6\">\n <div className=\"flex flex-col items-center justify-center py-12 text-center\">\n <div className=\"rounded-full bg-muted p-6 mb-4\">\n <FolderKanban size={48} className=\"text-muted-foreground\" />\n </div>\n <h3 className=\"text-lg font-semibold mb-2\">\n {tWithFallback(t, 'projectsList.noProjectsFound', 'No projects found')}\n </h3>\n <p className=\"text-muted-foreground max-w-sm mb-4\">\n {projectFilters.search\n ? tWithFallback(\n t,\n 'projectsList.adjustSearchOrFilters',\n 'Try adjusting your search or filters'\n )\n : tWithFallback(\n t,\n 'projectsList.createFirstProject',\n 'Create your first project to get started organizing your work'\n )}\n </p>\n {!projectFilters.search && canCreateProject && (\n <Button onClick={handleCreateProject}>\n <Plus size={16} className=\"mr-2\" />\n {tWithFallback(t, 'projectsList.createProject', 'Create Project')}\n </Button>\n )}\n </div>\n </CardContent>\n </Card>\n )}\n\n {/* Projects Grid */}\n {!isLoading && !error && data && data.items.length > 0 && (\n <>\n <div className=\"flex flex-col gap-3 rounded-lg border bg-card p-4 sm:flex-row sm:items-center sm:justify-between\">\n <label className=\"flex items-center gap-2 text-sm text-foreground\">\n <input\n type=\"checkbox\"\n checked={allVisibleSelected}\n ref={(input) => {\n if (input) {\n input.indeterminate = partiallySelected && !allVisibleSelected;\n }\n }}\n onChange={(event) => toggleAllProjects(event.target.checked)}\n aria-label={tWithFallback(\n t,\n 'projectsList.selectAllAriaLabel',\n 'Select all projects'\n )}\n className=\"size-4 rounded border-border text-primary\"\n />\n <span>\n {tWithFallback(t, 'projectsList.selectAllVisible', 'Select all visible')}\n </span>\n </label>\n\n <div className=\"flex flex-wrap items-center gap-2\">\n <span className=\"text-sm text-muted-foreground\">\n {selectedProjectIds.size} {tWithFallback(t, 'projectsList.selected', 'selected')}\n </span>\n {selectedProjectIds.size > 0 && (\n <Button variant=\"ghost\" size=\"sm\" onClick={clearProjectSelection}>\n {tWithFallback(t, 'projectsList.clearSelection', 'Clear selection')}\n </Button>\n )}\n {/* Bulk mutations are bound to a single workspaceId via the\n archive/unarchive/delete hooks. On the cross-workspace\n /workspaces/projects route (workspaceId is undefined),\n hide them — selections can span workspaces and the bound\n hook would fail authorization for projects outside the\n caller's context. The per-card actions in ProjectCard\n still work because they resolve per-project. */}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkArchive}\n disabled={archivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.archiveSelected', 'Archive Selected')}\n {archivableProjects.length > 0 ? ` (${archivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canArchiveProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkUnarchive}\n disabled={unarchivableProjects.length === 0}\n >\n {tWithFallback(t, 'projectsList.unarchiveSelected', 'Unarchive Selected')}\n {unarchivableProjects.length > 0 ? ` (${unarchivableProjects.length})` : ''}\n </Button>\n )}\n {workspaceId && canDeleteProject && (\n <Button\n variant=\"outline\"\n size=\"sm\"\n onClick={handleBulkDelete}\n disabled={selectedProjects.length === 0}\n className=\"text-destructive\"\n >\n {tWithFallback(t, 'projectsList.deleteSelected', 'Delete Selected')}\n {selectedProjects.length > 0 ? ` (${selectedProjects.length})` : ''}\n </Button>\n )}\n </div>\n </div>\n\n <div\n className={\n viewMode === 'list'\n ? 'space-y-3'\n : 'grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4'\n }\n >\n {data.items.map((project) => (\n <ProjectCard\n key={project.id}\n project={project}\n selected={selectedProjectIds.has(project.id)}\n canArchive={canArchiveProject}\n canDelete={canDeleteProject}\n onTagClick={toggleTagFilter}\n onToggleSelect={(checked) => {\n const isSelected = selectedProjectIds.has(project.id);\n if (checked !== isSelected) {\n toggleProjectSelection(project.id);\n }\n }}\n />\n ))}\n </div>\n\n {/* Pagination Info */}\n {data.total > data.items.length && (\n <div className=\"text-center text-sm text-muted-foreground\">\n {tWithFallback(t, 'projectsList.showingPrefix', 'Showing')} {data.items.length}{' '}\n {tWithFallback(t, 'projectsList.showingOf', 'of')} {data.total}{' '}\n {tWithFallback(t, 'projectsList.showingProjects', 'projects')}\n </div>\n )}\n </>\n )}\n\n {/* Create Project Dialog */}\n {isCreateProjectDialogOpen && <CreateProjectDialog workspaceId={workspaceId} />}\n </div>\n </PageContainer>\n );\n}\n"],"mappings":"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"}
|
|
@@ -8,30 +8,30 @@ import { useUpdateWorkspace as s } from "../hooks/useWorkspaceMutations.js";
|
|
|
8
8
|
import { useMemo as c } from "react";
|
|
9
9
|
import { ArrowLeft as l, Save as u } from "lucide-react";
|
|
10
10
|
import { jsx as d, jsxs as f } from "react/jsx-runtime";
|
|
11
|
-
import { Button as p, Card as m, CardContent as h, CardHeader as g, CardTitle as _ } from "@burdenoff/fe-libs/ui";
|
|
12
|
-
import { useNavigate as
|
|
13
|
-
import { AccessDenied as
|
|
14
|
-
import { useI18n as
|
|
15
|
-
import { Form as
|
|
16
|
-
import { TagsWidget as
|
|
11
|
+
import { Button as p, Card as m, CardContent as h, CardHeader as g, CardTitle as _, OptionalFields as v } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { useNavigate as y, useParams as b } from "react-router-dom";
|
|
13
|
+
import { AccessDenied as x, PermissionGate as S } from "@burdenoff/fe-libs/shared/components";
|
|
14
|
+
import { useI18n as C } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
15
|
+
import { Form as w, FormControl as T, FormField as E, FormItem as D, FormLabel as O, FormMessage as k, Input as A, z as j } from "@burdenoff/fe-libs/form";
|
|
16
|
+
import { TagsWidget as M } from "@burdenoff/fe-libs/tag";
|
|
17
17
|
//#region src/pages/WorkspaceSettingsPage.tsx
|
|
18
|
-
var
|
|
19
|
-
name:
|
|
20
|
-
contactEmail:
|
|
21
|
-
domain:
|
|
22
|
-
website:
|
|
18
|
+
var N = j.object({
|
|
19
|
+
name: j.string().min(2).max(100),
|
|
20
|
+
contactEmail: j.string().email().optional().or(j.literal("")),
|
|
21
|
+
domain: j.string().optional(),
|
|
22
|
+
website: j.string().url().optional().or(j.literal(""))
|
|
23
23
|
});
|
|
24
|
-
function
|
|
25
|
-
let { t:
|
|
24
|
+
function P() {
|
|
25
|
+
let { t: j } = C(), { workspaceId: P } = b();
|
|
26
26
|
o("workspaces.settings", {
|
|
27
|
-
workspaceId:
|
|
27
|
+
workspaceId: P,
|
|
28
28
|
requireParams: !0
|
|
29
29
|
});
|
|
30
|
-
let
|
|
31
|
-
variables: { input: { workspaceId:
|
|
32
|
-
skip: !
|
|
30
|
+
let F = y(), { basePath: I = "/" } = e(), { data: L, isLoading: R } = r(P || ""), { mutate: z, isPending: B } = s(), { data: V, refetch: H } = t({
|
|
31
|
+
variables: { input: { workspaceId: P ?? "" } },
|
|
32
|
+
skip: !P,
|
|
33
33
|
fetchPolicy: "cache-and-network"
|
|
34
|
-
}), [
|
|
34
|
+
}), [U, { loading: W }] = n(), G = c(() => (V?.getWorkspaceTags ?? []).flatMap((e) => e.tag ? [{
|
|
35
35
|
id: e.tagId,
|
|
36
36
|
key: e.tag.key,
|
|
37
37
|
label: e.tag.label,
|
|
@@ -42,32 +42,32 @@ function N() {
|
|
|
42
42
|
color: e.tag.color ?? null,
|
|
43
43
|
icon: e.tag.icon ?? null,
|
|
44
44
|
description: e.tag.description ?? null
|
|
45
|
-
}] : []), [
|
|
46
|
-
|
|
47
|
-
workspaceId:
|
|
45
|
+
}] : []), [V?.getWorkspaceTags]), K = async (e) => {
|
|
46
|
+
P && (await U({ variables: { input: {
|
|
47
|
+
workspaceId: P,
|
|
48
48
|
tagIds: e.map((e) => e.id)
|
|
49
|
-
} } }), await
|
|
49
|
+
} } }), await H());
|
|
50
50
|
};
|
|
51
|
-
return
|
|
51
|
+
return R ? /* @__PURE__ */ d("div", {
|
|
52
52
|
className: "p-6",
|
|
53
|
-
children: a(
|
|
54
|
-
}) :
|
|
53
|
+
children: a(j, "workspaceSettings.loading", "Loading...")
|
|
54
|
+
}) : L ? /* @__PURE__ */ d(S, {
|
|
55
55
|
permission: i.WORKSPACE_UPDATE,
|
|
56
56
|
fallback: /* @__PURE__ */ f("div", {
|
|
57
57
|
className: "p-6",
|
|
58
58
|
children: [/* @__PURE__ */ f(p, {
|
|
59
59
|
variant: "ghost",
|
|
60
|
-
onClick: () =>
|
|
60
|
+
onClick: () => F(`${I}/${P}`),
|
|
61
61
|
className: "gap-2 mb-6 -ml-2",
|
|
62
|
-
children: [/* @__PURE__ */ d(l, { size: 20 }), a(
|
|
63
|
-
}), /* @__PURE__ */ d(
|
|
62
|
+
children: [/* @__PURE__ */ d(l, { size: 20 }), a(j, "workspaceSettings.backToWorkspace", "Back to Workspace")]
|
|
63
|
+
}), /* @__PURE__ */ d(x, {
|
|
64
64
|
variant: "card",
|
|
65
|
-
title: a(
|
|
66
|
-
description: a(
|
|
67
|
-
details: a(
|
|
65
|
+
title: a(j, "workspaceSettings.permissionRequired", "Permission Required"),
|
|
66
|
+
description: a(j, "workspaceSettings.permissionRequiredDesc", "You don't have permission to edit workspace settings."),
|
|
67
|
+
details: a(j, "workspaceSettings.permissionRequiredDetails", "Required: workspace:update"),
|
|
68
68
|
primaryAction: {
|
|
69
|
-
label: a(
|
|
70
|
-
onClick: () =>
|
|
69
|
+
label: a(j, "workspaceSettings.goBack", "Go Back"),
|
|
70
|
+
onClick: () => F(`${I}/${P}`)
|
|
71
71
|
}
|
|
72
72
|
})]
|
|
73
73
|
}),
|
|
@@ -77,24 +77,24 @@ function N() {
|
|
|
77
77
|
/* @__PURE__ */ f("div", { children: [
|
|
78
78
|
/* @__PURE__ */ f(p, {
|
|
79
79
|
variant: "ghost",
|
|
80
|
-
onClick: () =>
|
|
80
|
+
onClick: () => F(`${I}/${P}`),
|
|
81
81
|
className: "gap-2 mb-2 -ml-2",
|
|
82
|
-
children: [/* @__PURE__ */ d(l, { size: 20 }), a(
|
|
82
|
+
children: [/* @__PURE__ */ d(l, { size: 20 }), a(j, "workspaceSettings.backToWorkspace", "Back to Workspace")]
|
|
83
83
|
}),
|
|
84
84
|
/* @__PURE__ */ d("h1", {
|
|
85
85
|
className: "text-2xl sm:text-3xl font-bold",
|
|
86
|
-
children: a(
|
|
86
|
+
children: a(j, "workspaceSettings.title", "Workspace Settings")
|
|
87
87
|
}),
|
|
88
88
|
/* @__PURE__ */ d("p", {
|
|
89
89
|
className: "text-muted-foreground",
|
|
90
|
-
children: a(
|
|
90
|
+
children: a(j, "workspaceSettings.subtitle", "Manage workspace configuration")
|
|
91
91
|
})
|
|
92
92
|
] }),
|
|
93
|
-
/* @__PURE__ */ f(m, { children: [/* @__PURE__ */ d(g, { children: /* @__PURE__ */ d(_, { children: a(
|
|
94
|
-
schema:
|
|
93
|
+
/* @__PURE__ */ f(m, { children: [/* @__PURE__ */ d(g, { children: /* @__PURE__ */ d(_, { children: a(j, "workspaceSettings.generalSettings", "General Settings") }) }), /* @__PURE__ */ d(h, { children: /* @__PURE__ */ d(w, {
|
|
94
|
+
schema: N,
|
|
95
95
|
onSubmit: (e) => {
|
|
96
|
-
|
|
97
|
-
id:
|
|
96
|
+
P && z({
|
|
97
|
+
id: P,
|
|
98
98
|
input: {
|
|
99
99
|
...e,
|
|
100
100
|
contactEmail: e.contactEmail || void 0,
|
|
@@ -102,101 +102,104 @@ function N() {
|
|
|
102
102
|
website: e.website || void 0
|
|
103
103
|
}
|
|
104
104
|
}, { onSuccess: () => {
|
|
105
|
-
|
|
105
|
+
F(`${I}/${P}`);
|
|
106
106
|
} });
|
|
107
107
|
},
|
|
108
108
|
formOptions: { defaultValues: {
|
|
109
|
-
name:
|
|
110
|
-
contactEmail:
|
|
111
|
-
domain:
|
|
112
|
-
website:
|
|
109
|
+
name: L?.name || "",
|
|
110
|
+
contactEmail: L?.contactEmail || "",
|
|
111
|
+
domain: L?.domain || "",
|
|
112
|
+
website: L?.website || ""
|
|
113
113
|
} },
|
|
114
114
|
children: (e) => /* @__PURE__ */ f("div", {
|
|
115
115
|
className: "space-y-4",
|
|
116
116
|
children: [
|
|
117
|
-
/* @__PURE__ */ d(
|
|
117
|
+
/* @__PURE__ */ d(E, {
|
|
118
118
|
control: e.control,
|
|
119
119
|
name: "name",
|
|
120
|
-
render: ({ field: e }) => /* @__PURE__ */ f(
|
|
121
|
-
/* @__PURE__ */ d(
|
|
122
|
-
/* @__PURE__ */ d(
|
|
123
|
-
/* @__PURE__ */ d(
|
|
120
|
+
render: ({ field: e }) => /* @__PURE__ */ f(D, { children: [
|
|
121
|
+
/* @__PURE__ */ d(O, { children: a(j, "workspaceSettings.workspaceName", "Workspace Name") }),
|
|
122
|
+
/* @__PURE__ */ d(T, { children: /* @__PURE__ */ d(A, { ...e }) }),
|
|
123
|
+
/* @__PURE__ */ d(k, {})
|
|
124
124
|
] })
|
|
125
125
|
}),
|
|
126
|
-
/* @__PURE__ */ d(
|
|
126
|
+
/* @__PURE__ */ d(E, {
|
|
127
127
|
control: e.control,
|
|
128
128
|
name: "contactEmail",
|
|
129
|
-
render: ({ field: e }) => /* @__PURE__ */ f(
|
|
130
|
-
/* @__PURE__ */ d(
|
|
131
|
-
/* @__PURE__ */ d(
|
|
129
|
+
render: ({ field: e }) => /* @__PURE__ */ f(D, { children: [
|
|
130
|
+
/* @__PURE__ */ d(O, { children: a(j, "workspaceSettings.contactEmail", "Contact Email") }),
|
|
131
|
+
/* @__PURE__ */ d(T, { children: /* @__PURE__ */ d(A, {
|
|
132
132
|
type: "email",
|
|
133
133
|
...e
|
|
134
134
|
}) }),
|
|
135
|
-
/* @__PURE__ */ d(
|
|
135
|
+
/* @__PURE__ */ d(k, {})
|
|
136
136
|
] })
|
|
137
137
|
}),
|
|
138
|
-
/* @__PURE__ */
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
/* @__PURE__ */
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
138
|
+
/* @__PURE__ */ f(v, {
|
|
139
|
+
title: a(j, "workspaceSettings.advancedTitle", "Advanced settings"),
|
|
140
|
+
description: a(j, "workspaceSettings.advancedDescription", "Custom domain + website. Most workspaces leave these blank."),
|
|
141
|
+
children: [/* @__PURE__ */ d(E, {
|
|
142
|
+
control: e.control,
|
|
143
|
+
name: "domain",
|
|
144
|
+
render: ({ field: e }) => /* @__PURE__ */ f(D, { children: [
|
|
145
|
+
/* @__PURE__ */ d(O, { children: a(j, "workspaceSettings.domain", "Domain") }),
|
|
146
|
+
/* @__PURE__ */ d(T, { children: /* @__PURE__ */ d(A, { ...e }) }),
|
|
147
|
+
/* @__PURE__ */ d(k, {})
|
|
148
|
+
] })
|
|
149
|
+
}), /* @__PURE__ */ d(E, {
|
|
150
|
+
control: e.control,
|
|
151
|
+
name: "website",
|
|
152
|
+
render: ({ field: e }) => /* @__PURE__ */ f(D, { children: [
|
|
153
|
+
/* @__PURE__ */ d(O, { children: a(j, "workspaceSettings.website", "Website") }),
|
|
154
|
+
/* @__PURE__ */ d(T, { children: /* @__PURE__ */ d(A, {
|
|
155
|
+
type: "url",
|
|
156
|
+
...e
|
|
157
|
+
}) }),
|
|
158
|
+
/* @__PURE__ */ d(k, {})
|
|
159
|
+
] })
|
|
160
|
+
})]
|
|
158
161
|
}),
|
|
159
162
|
/* @__PURE__ */ f("div", {
|
|
160
163
|
className: "flex gap-2",
|
|
161
164
|
children: [/* @__PURE__ */ f(p, {
|
|
162
165
|
type: "submit",
|
|
163
|
-
disabled:
|
|
166
|
+
disabled: B,
|
|
164
167
|
className: "gap-2",
|
|
165
|
-
children: [/* @__PURE__ */ d(u, { size: 20 }),
|
|
168
|
+
children: [/* @__PURE__ */ d(u, { size: 20 }), B ? a(j, "workspaceSettings.saving", "Saving...") : a(j, "workspaceSettings.saveChanges", "Save Changes")]
|
|
166
169
|
}), /* @__PURE__ */ d(p, {
|
|
167
170
|
type: "button",
|
|
168
171
|
variant: "outline",
|
|
169
|
-
onClick: () =>
|
|
170
|
-
children: a(
|
|
172
|
+
onClick: () => F(`${I}/${P}`),
|
|
173
|
+
children: a(j, "workspaceSettings.cancel", "Cancel")
|
|
171
174
|
})]
|
|
172
175
|
})
|
|
173
176
|
]
|
|
174
177
|
})
|
|
175
178
|
}) })] }),
|
|
176
|
-
|
|
179
|
+
P && /* @__PURE__ */ f(m, { children: [/* @__PURE__ */ d(g, { children: /* @__PURE__ */ d(_, { children: a(j, "workspaceSettings.tags", "Tags") }) }), /* @__PURE__ */ f(h, {
|
|
177
180
|
className: "space-y-3",
|
|
178
181
|
children: [/* @__PURE__ */ d("p", {
|
|
179
182
|
className: "text-sm text-muted-foreground",
|
|
180
|
-
children: a(
|
|
181
|
-
}), /* @__PURE__ */ d(
|
|
183
|
+
children: a(j, "workspaceSettings.tagsDescription", "Use universal tags to classify and filter this workspace.")
|
|
184
|
+
}), /* @__PURE__ */ d(M, {
|
|
182
185
|
entityType: "Workspace",
|
|
183
|
-
entityId:
|
|
184
|
-
workspaceId:
|
|
185
|
-
tags:
|
|
186
|
-
onAdd: (e) =>
|
|
187
|
-
onRemove: (e) => G
|
|
186
|
+
entityId: P,
|
|
187
|
+
workspaceId: P,
|
|
188
|
+
tags: G,
|
|
189
|
+
onAdd: (e) => K([...G, e]),
|
|
190
|
+
onRemove: (e) => K(G.filter((t) => t.id !== e)),
|
|
188
191
|
syncGraphNotifications: !1,
|
|
189
|
-
className:
|
|
192
|
+
className: W ? "pointer-events-none opacity-75" : void 0
|
|
190
193
|
})]
|
|
191
194
|
})] })
|
|
192
195
|
]
|
|
193
196
|
})
|
|
194
197
|
}) : /* @__PURE__ */ d("div", {
|
|
195
198
|
className: "p-6",
|
|
196
|
-
children: a(
|
|
199
|
+
children: a(j, "workspaceSettings.notFound", "Workspace not found")
|
|
197
200
|
});
|
|
198
201
|
}
|
|
199
202
|
//#endregion
|
|
200
|
-
export {
|
|
203
|
+
export { P as WorkspaceSettingsPage };
|
|
201
204
|
|
|
202
205
|
//# sourceMappingURL=WorkspaceSettingsPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"WorkspaceSettingsPage.js","names":[],"sources":["../../src/pages/WorkspaceSettingsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { ArrowLeft, Save } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { type TagItem, TagsWidget } from '@burdenoff/fe-libs/tag';\nimport { tWithFallback } from '../utils/i18n';\nimport { Button, Card, CardHeader, CardTitle, CardContent } from '@burdenoff/fe-libs/ui';\nimport {\n useGetWorkspaceTagsQuery,\n useReplaceWorkspaceTagsMutation,\n type WorkspaceTagAssignmentFieldsFragment,\n} from '../generated/wspace-operations';\nimport {\n Form,\n FormField,\n FormItem,\n FormLabel,\n FormControl,\n FormMessage,\n Input,\n z,\n} from '@burdenoff/fe-libs/form';\nimport { PermissionGate, AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { WORKSPACE_PERMISSIONS } from '../constants/permissions';\nimport { useWorkspace } from '../hooks/useWorkspaces';\nimport { useUpdateWorkspace } from '../hooks/useWorkspaceMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\n\nconst updateWorkspaceSchema = z.object({\n name: z.string().min(2).max(100),\n contactEmail: z.string().email().optional().or(z.literal('')),\n domain: z.string().optional(),\n website: z.string().url().optional().or(z.literal('')),\n});\n\ntype UpdateWorkspaceFormData = z.infer<typeof updateWorkspaceSchema>;\n\nexport function WorkspaceSettingsPage() {\n const { t } = useI18n();\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.settings', { workspaceId, requireParams: true });\n const navigate = useNavigate();\n const { basePath = '/' } = useWorkspacesContext();\n const { data: workspace, isLoading } = useWorkspace(workspaceId || '');\n const { mutate: updateWorkspace, isPending } = useUpdateWorkspace();\n\n // Workspace tags (universal-tags assignments) — backend exposed via\n // wspace-workspace-svc workspace-tags.gql; this UI is the canonical\n // editor for them. Identical pattern to ProjectDetailPage.\n const { data: tagsData, refetch: refetchTags } = useGetWorkspaceTagsQuery({\n variables: { input: { workspaceId: workspaceId ?? '' } },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n const [replaceWorkspaceTags, { loading: tagsUpdating }] = useReplaceWorkspaceTagsMutation();\n // The widget keys tags by `id` for add/remove. We store the\n // assignment's tagId there so the round-trip back to\n // replaceWorkspaceTags has the right value to send. Display fields\n // come from the embedded `tag: UniversalTag` resolved by the\n // assignment — assignments without a hydrated tag (deleted or\n // unresolved) are dropped so the widget never renders empty badges.\n const widgetTags = useMemo<TagItem[]>(() => {\n const assignments = tagsData?.getWorkspaceTags ?? [];\n return assignments.flatMap((a: WorkspaceTagAssignmentFieldsFragment) => {\n if (!a.tag) return [];\n return [\n {\n id: a.tagId,\n key: a.tag.key,\n label: a.tag.label,\n value: a.tag.value ?? null,\n vocabularyId: a.tag.vocabularyId ?? null,\n parentId: a.tag.parentId ?? null,\n hierarchyPath: a.tag.hierarchyPath ?? null,\n color: a.tag.color ?? null,\n icon: a.tag.icon ?? null,\n description: a.tag.description ?? null,\n } satisfies TagItem,\n ];\n });\n }, [tagsData?.getWorkspaceTags]);\n\n const persistTags = async (next: TagItem[]) => {\n if (!workspaceId) return;\n await replaceWorkspaceTags({\n variables: {\n input: {\n workspaceId,\n tagIds: next.map((t) => t.id),\n },\n },\n });\n await refetchTags();\n };\n\n const handleAddTag = (tag: TagItem) => persistTags([...widgetTags, tag]);\n const handleRemoveTag = (tagId: string) => persistTags(widgetTags.filter((t) => t.id !== tagId));\n\n const handleSubmit = (data: UpdateWorkspaceFormData) => {\n if (!workspaceId) return;\n\n updateWorkspace(\n {\n id: workspaceId,\n input: {\n ...data,\n contactEmail: data.contactEmail || undefined,\n domain: data.domain || undefined,\n website: data.website || undefined,\n },\n },\n {\n onSuccess: () => {\n navigate(`${basePath}/${workspaceId}`);\n },\n }\n );\n };\n\n if (isLoading) {\n return <div className=\"p-6\">{tWithFallback(t, 'workspaceSettings.loading', 'Loading...')}</div>;\n }\n\n if (!workspace) {\n return (\n <div className=\"p-6\">\n {tWithFallback(t, 'workspaceSettings.notFound', 'Workspace not found')}\n </div>\n );\n }\n\n return (\n <PermissionGate\n permission={WORKSPACE_PERMISSIONS.WORKSPACE_UPDATE}\n fallback={\n <div className=\"p-6\">\n <Button\n variant=\"ghost\"\n onClick={() => navigate(`${basePath}/${workspaceId}`)}\n className=\"gap-2 mb-6 -ml-2\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'workspaceSettings.backToWorkspace', 'Back to Workspace')}\n </Button>\n <AccessDenied\n variant=\"card\"\n title={tWithFallback(t, 'workspaceSettings.permissionRequired', 'Permission Required')}\n description={tWithFallback(\n t,\n 'workspaceSettings.permissionRequiredDesc',\n \"You don't have permission to edit workspace settings.\"\n )}\n details={tWithFallback(\n t,\n 'workspaceSettings.permissionRequiredDetails',\n 'Required: workspace:update'\n )}\n primaryAction={{\n label: tWithFallback(t, 'workspaceSettings.goBack', 'Go Back'),\n onClick: () => navigate(`${basePath}/${workspaceId}`),\n }}\n />\n </div>\n }\n >\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <Button\n variant=\"ghost\"\n onClick={() => navigate(`${basePath}/${workspaceId}`)}\n className=\"gap-2 mb-2 -ml-2\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'workspaceSettings.backToWorkspace', 'Back to Workspace')}\n </Button>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'workspaceSettings.title', 'Workspace Settings')}\n </h1>\n <p className=\"text-muted-foreground\">\n {tWithFallback(t, 'workspaceSettings.subtitle', 'Manage workspace configuration')}\n </p>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>\n {tWithFallback(t, 'workspaceSettings.generalSettings', 'General Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Form\n schema={updateWorkspaceSchema}\n onSubmit={handleSubmit}\n formOptions={{\n defaultValues: {\n name: workspace?.name || '',\n contactEmail: workspace?.contactEmail || '',\n domain: workspace?.domain || '',\n website: workspace?.website || '',\n },\n }}\n >\n {(form) => (\n <div className=\"space-y-4\">\n <FormField\n control={form.control}\n name=\"name\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.workspaceName', 'Workspace Name')}\n </FormLabel>\n <FormControl>\n <Input {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"contactEmail\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.contactEmail', 'Contact Email')}\n </FormLabel>\n <FormControl>\n <Input type=\"email\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"domain\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.domain', 'Domain')}\n </FormLabel>\n <FormControl>\n <Input {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"website\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.website', 'Website')}\n </FormLabel>\n <FormControl>\n <Input type=\"url\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <div className=\"flex gap-2\">\n <Button type=\"submit\" disabled={isPending} className=\"gap-2\">\n <Save size={20} />\n {isPending\n ? tWithFallback(t, 'workspaceSettings.saving', 'Saving...')\n : tWithFallback(t, 'workspaceSettings.saveChanges', 'Save Changes')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`${basePath}/${workspaceId}`)}\n >\n {tWithFallback(t, 'workspaceSettings.cancel', 'Cancel')}\n </Button>\n </div>\n </div>\n )}\n </Form>\n </CardContent>\n </Card>\n\n {workspaceId && (\n <Card>\n <CardHeader>\n <CardTitle>{tWithFallback(t, 'workspaceSettings.tags', 'Tags')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'workspaceSettings.tagsDescription',\n 'Use universal tags to classify and filter this workspace.'\n )}\n </p>\n <TagsWidget\n entityType=\"Workspace\"\n entityId={workspaceId}\n workspaceId={workspaceId}\n tags={widgetTags}\n onAdd={handleAddTag}\n onRemove={handleRemoveTag}\n syncGraphNotifications={false}\n className={tagsUpdating ? 'pointer-events-none opacity-75' : undefined}\n />\n </CardContent>\n </Card>\n )}\n </div>\n </PermissionGate>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"WorkspaceSettingsPage.js","names":[],"sources":["../../src/pages/WorkspaceSettingsPage.tsx"],"sourcesContent":["import { useMemo } from 'react';\nimport { useParams, useNavigate } from 'react-router-dom';\nimport { ArrowLeft, Save } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { type TagItem, TagsWidget } from '@burdenoff/fe-libs/tag';\nimport { tWithFallback } from '../utils/i18n';\nimport {\n Button,\n Card,\n CardHeader,\n CardTitle,\n CardContent,\n OptionalFields,\n} from '@burdenoff/fe-libs/ui';\nimport {\n useGetWorkspaceTagsQuery,\n useReplaceWorkspaceTagsMutation,\n type WorkspaceTagAssignmentFieldsFragment,\n} from '../generated/wspace-operations';\nimport {\n Form,\n FormField,\n FormItem,\n FormLabel,\n FormControl,\n FormMessage,\n Input,\n z,\n} from '@burdenoff/fe-libs/form';\nimport { PermissionGate, AccessDenied } from '@burdenoff/fe-libs/shared/components';\nimport { WORKSPACE_PERMISSIONS } from '../constants/permissions';\nimport { useWorkspace } from '../hooks/useWorkspaces';\nimport { useUpdateWorkspace } from '../hooks/useWorkspaceMutations';\nimport { useWorkspacesContext } from '../providers/WorkspacesProvider';\nimport { useWorkspacesPageView } from '../hooks/useWorkspacesPageView';\n\nconst updateWorkspaceSchema = z.object({\n name: z.string().min(2).max(100),\n contactEmail: z.string().email().optional().or(z.literal('')),\n domain: z.string().optional(),\n website: z.string().url().optional().or(z.literal('')),\n});\n\ntype UpdateWorkspaceFormData = z.infer<typeof updateWorkspaceSchema>;\n\nexport function WorkspaceSettingsPage() {\n const { t } = useI18n();\n const { workspaceId } = useParams<{ workspaceId: string }>();\n useWorkspacesPageView('workspaces.settings', { workspaceId, requireParams: true });\n const navigate = useNavigate();\n const { basePath = '/' } = useWorkspacesContext();\n const { data: workspace, isLoading } = useWorkspace(workspaceId || '');\n const { mutate: updateWorkspace, isPending } = useUpdateWorkspace();\n\n // Workspace tags (universal-tags assignments) — backend exposed via\n // wspace-workspace-svc workspace-tags.gql; this UI is the canonical\n // editor for them. Identical pattern to ProjectDetailPage.\n const { data: tagsData, refetch: refetchTags } = useGetWorkspaceTagsQuery({\n variables: { input: { workspaceId: workspaceId ?? '' } },\n skip: !workspaceId,\n fetchPolicy: 'cache-and-network',\n });\n const [replaceWorkspaceTags, { loading: tagsUpdating }] = useReplaceWorkspaceTagsMutation();\n // The widget keys tags by `id` for add/remove. We store the\n // assignment's tagId there so the round-trip back to\n // replaceWorkspaceTags has the right value to send. Display fields\n // come from the embedded `tag: UniversalTag` resolved by the\n // assignment — assignments without a hydrated tag (deleted or\n // unresolved) are dropped so the widget never renders empty badges.\n const widgetTags = useMemo<TagItem[]>(() => {\n const assignments = tagsData?.getWorkspaceTags ?? [];\n return assignments.flatMap((a: WorkspaceTagAssignmentFieldsFragment) => {\n if (!a.tag) return [];\n return [\n {\n id: a.tagId,\n key: a.tag.key,\n label: a.tag.label,\n value: a.tag.value ?? null,\n vocabularyId: a.tag.vocabularyId ?? null,\n parentId: a.tag.parentId ?? null,\n hierarchyPath: a.tag.hierarchyPath ?? null,\n color: a.tag.color ?? null,\n icon: a.tag.icon ?? null,\n description: a.tag.description ?? null,\n } satisfies TagItem,\n ];\n });\n }, [tagsData?.getWorkspaceTags]);\n\n const persistTags = async (next: TagItem[]) => {\n if (!workspaceId) return;\n await replaceWorkspaceTags({\n variables: {\n input: {\n workspaceId,\n tagIds: next.map((t) => t.id),\n },\n },\n });\n await refetchTags();\n };\n\n const handleAddTag = (tag: TagItem) => persistTags([...widgetTags, tag]);\n const handleRemoveTag = (tagId: string) => persistTags(widgetTags.filter((t) => t.id !== tagId));\n\n const handleSubmit = (data: UpdateWorkspaceFormData) => {\n if (!workspaceId) return;\n\n updateWorkspace(\n {\n id: workspaceId,\n input: {\n ...data,\n contactEmail: data.contactEmail || undefined,\n domain: data.domain || undefined,\n website: data.website || undefined,\n },\n },\n {\n onSuccess: () => {\n navigate(`${basePath}/${workspaceId}`);\n },\n }\n );\n };\n\n if (isLoading) {\n return <div className=\"p-6\">{tWithFallback(t, 'workspaceSettings.loading', 'Loading...')}</div>;\n }\n\n if (!workspace) {\n return (\n <div className=\"p-6\">\n {tWithFallback(t, 'workspaceSettings.notFound', 'Workspace not found')}\n </div>\n );\n }\n\n return (\n <PermissionGate\n permission={WORKSPACE_PERMISSIONS.WORKSPACE_UPDATE}\n fallback={\n <div className=\"p-6\">\n <Button\n variant=\"ghost\"\n onClick={() => navigate(`${basePath}/${workspaceId}`)}\n className=\"gap-2 mb-6 -ml-2\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'workspaceSettings.backToWorkspace', 'Back to Workspace')}\n </Button>\n <AccessDenied\n variant=\"card\"\n title={tWithFallback(t, 'workspaceSettings.permissionRequired', 'Permission Required')}\n description={tWithFallback(\n t,\n 'workspaceSettings.permissionRequiredDesc',\n \"You don't have permission to edit workspace settings.\"\n )}\n details={tWithFallback(\n t,\n 'workspaceSettings.permissionRequiredDetails',\n 'Required: workspace:update'\n )}\n primaryAction={{\n label: tWithFallback(t, 'workspaceSettings.goBack', 'Go Back'),\n onClick: () => navigate(`${basePath}/${workspaceId}`),\n }}\n />\n </div>\n }\n >\n <div className=\"space-y-4 sm:space-y-6 p-3 sm:p-6\">\n <div>\n <Button\n variant=\"ghost\"\n onClick={() => navigate(`${basePath}/${workspaceId}`)}\n className=\"gap-2 mb-2 -ml-2\"\n >\n <ArrowLeft size={20} />\n {tWithFallback(t, 'workspaceSettings.backToWorkspace', 'Back to Workspace')}\n </Button>\n <h1 className=\"text-2xl sm:text-3xl font-bold\">\n {tWithFallback(t, 'workspaceSettings.title', 'Workspace Settings')}\n </h1>\n <p className=\"text-muted-foreground\">\n {tWithFallback(t, 'workspaceSettings.subtitle', 'Manage workspace configuration')}\n </p>\n </div>\n\n <Card>\n <CardHeader>\n <CardTitle>\n {tWithFallback(t, 'workspaceSettings.generalSettings', 'General Settings')}\n </CardTitle>\n </CardHeader>\n <CardContent>\n <Form\n schema={updateWorkspaceSchema}\n onSubmit={handleSubmit}\n formOptions={{\n defaultValues: {\n name: workspace?.name || '',\n contactEmail: workspace?.contactEmail || '',\n domain: workspace?.domain || '',\n website: workspace?.website || '',\n },\n }}\n >\n {(form) => (\n <div className=\"space-y-4\">\n <FormField\n control={form.control}\n name=\"name\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.workspaceName', 'Workspace Name')}\n </FormLabel>\n <FormControl>\n <Input {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"contactEmail\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.contactEmail', 'Contact Email')}\n </FormLabel>\n <FormControl>\n <Input type=\"email\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <OptionalFields\n title={tWithFallback(t, 'workspaceSettings.advancedTitle', 'Advanced settings')}\n description={tWithFallback(\n t,\n 'workspaceSettings.advancedDescription',\n 'Custom domain + website. Most workspaces leave these blank.'\n )}\n >\n <FormField\n control={form.control}\n name=\"domain\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.domain', 'Domain')}\n </FormLabel>\n <FormControl>\n <Input {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n\n <FormField\n control={form.control}\n name=\"website\"\n render={({ field }) => (\n <FormItem>\n <FormLabel>\n {tWithFallback(t, 'workspaceSettings.website', 'Website')}\n </FormLabel>\n <FormControl>\n <Input type=\"url\" {...field} />\n </FormControl>\n <FormMessage />\n </FormItem>\n )}\n />\n </OptionalFields>\n\n <div className=\"flex gap-2\">\n <Button type=\"submit\" disabled={isPending} className=\"gap-2\">\n <Save size={20} />\n {isPending\n ? tWithFallback(t, 'workspaceSettings.saving', 'Saving...')\n : tWithFallback(t, 'workspaceSettings.saveChanges', 'Save Changes')}\n </Button>\n <Button\n type=\"button\"\n variant=\"outline\"\n onClick={() => navigate(`${basePath}/${workspaceId}`)}\n >\n {tWithFallback(t, 'workspaceSettings.cancel', 'Cancel')}\n </Button>\n </div>\n </div>\n )}\n </Form>\n </CardContent>\n </Card>\n\n {workspaceId && (\n <Card>\n <CardHeader>\n <CardTitle>{tWithFallback(t, 'workspaceSettings.tags', 'Tags')}</CardTitle>\n </CardHeader>\n <CardContent className=\"space-y-3\">\n <p className=\"text-sm text-muted-foreground\">\n {tWithFallback(\n t,\n 'workspaceSettings.tagsDescription',\n 'Use universal tags to classify and filter this workspace.'\n )}\n </p>\n <TagsWidget\n entityType=\"Workspace\"\n entityId={workspaceId}\n workspaceId={workspaceId}\n tags={widgetTags}\n onAdd={handleAddTag}\n onRemove={handleRemoveTag}\n syncGraphNotifications={false}\n className={tagsUpdating ? 'pointer-events-none opacity-75' : undefined}\n />\n </CardContent>\n </Card>\n )}\n </div>\n </PermissionGate>\n );\n}\n"],"mappings":"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"}
|
|
@@ -119,7 +119,7 @@ function w() {
|
|
|
119
119
|
className: "space-y-3 p-6 border rounded-lg",
|
|
120
120
|
children: [/* @__PURE__ */ g("div", {
|
|
121
121
|
className: "flex items-center gap-3",
|
|
122
|
-
children: [/* @__PURE__ */ h(y, { className: "
|
|
122
|
+
children: [/* @__PURE__ */ h(y, { className: "size-12 rounded-lg" }), /* @__PURE__ */ g("div", {
|
|
123
123
|
className: "space-y-2 flex-1",
|
|
124
124
|
children: [/* @__PURE__ */ h(y, { className: "h-4 w-3/4" }), /* @__PURE__ */ h(y, { className: "h-3 w-1/2" })]
|
|
125
125
|
})]
|