From e905184c7f8e7b2a2d03f448fbe6fff83b7b8539 Mon Sep 17 00:00:00 2001 From: AnuragJangid18 Date: Fri, 23 Oct 2026 02:09:59 +0530 Subject: [PATCH] feat: add personal workspace creation for legacy users and invitation landing page --- README.md | 28 +++++++ backend/app/routers/tenant.py | 2 + backend/app/services/tenant_service.py | 31 ++++++++ frontend/src/App.tsx | 2 + .../components/dashboard/DashboardLayout.tsx | 11 ++- .../components/dashboard/DashboardSidebar.tsx | 18 ++--- .../components/dashboard/DocumentFlyout.tsx | 4 +- .../components/dashboard/TemplatesFlyout.tsx | 5 +- .../components/workspace/WorkspaceShell.tsx | 48 +++++++++++- .../components/workspace/WorkspaceSidebar.tsx | 78 ++++++++++++++++++- frontend/src/pages/InvitationPage.tsx | 30 +++++++ frontend/src/pages/workspace/SiloPage.tsx | 4 +- 12 files changed, 240 insertions(+), 21 deletions(-) create mode 100644 frontend/src/pages/InvitationPage.tsx diff --git a/README.md b/README.md index 2fa0bf5..fd7eabc 100644 --- a/README.md +++ b/README.md @@ -265,6 +265,8 @@ npm run build - Added workspace-scoped MongoDB document metadata and dashboard filtering. - Existing owner-owned spaces and pages are backfilled into the selected workspace on first workspace load without changing their content or sharing. +- Legacy users without a tenant are automatically given a personal + organization and workspace, so Invite Fellow works immediately after login. - New document creation rejects workspace IDs the authenticated user cannot access. - Added a workspace administration page at `/settings/workspaces` for @@ -276,6 +278,7 @@ npm run build - Removed the unused Customize sidebar action and its controls. - Updated Invite Fellow to create a workspace invitation and open a prefilled email containing the invitation link. +- Added the invitation landing page at `/invite/:token`. Validation completed: @@ -292,3 +295,28 @@ Next tenant work: - Add invitation and audit history views to the administration page. - Add organization-level security settings. - Add authenticated browser coverage for workspace creation and role denial. + +#### Workspace navigation polish + +- Workspace and page views now retain the primary dashboard navigation: + For you, Templates, Recent, Favourite Projects, and All Projects. +- The navigation appears above the current space/page tree instead of replacing + the dashboard experience; the duplicate Dashboard button was removed because + the workspace header already provides that route. +- Space views now keep the sidebar visible, with the active space and its + content shown beneath the primary navigation. +- Navigating to a child page now automatically keeps its ancestor pages + expanded in the tree. +- New child pages are created as `Untitled` so users can name them directly. +- Template content now stays isolated to the page created from that template; + switching between pages remounts the editor for the active document instead + of reusing stale content from the previous page. +- Workspace sidebar navigation now opens the corresponding dashboard view: + Recent, Favourite Projects, All Projects, and Templates no longer all open + the default dashboard state. +- Workspace navigation now opens those flyouts in place without leaving the + current page or space. Dashboard and workspace navigation use matching + sidebar width, button height, padding, and text sizing. +- Workspace navigation now mirrors dashboard controls: “For you” links to the + dashboard, while Recent, Favourite Projects, All Projects, and Templates + use trailing chevrons and active flyout states. diff --git a/backend/app/routers/tenant.py b/backend/app/routers/tenant.py index 7daff05..ceec6d7 100644 --- a/backend/app/routers/tenant.py +++ b/backend/app/routers/tenant.py @@ -28,6 +28,7 @@ from app.services.tenant_service import ( list_members, list_workspaces, update_member_role, + ensure_personal_workspace, ) router = APIRouter(prefix="/tenants", tags=["tenants"]) @@ -48,6 +49,7 @@ def workspace_response(row) -> WorkspaceResponse: @router.get("/workspaces", response_model=list[WorkspaceResponse]) def get_workspaces(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): + ensure_personal_workspace(db, current_user) return [workspace_response(row) for row in list_workspaces(db, current_user.id)] diff --git a/backend/app/services/tenant_service.py b/backend/app/services/tenant_service.py index 242e9a6..1555503 100644 --- a/backend/app/services/tenant_service.py +++ b/backend/app/services/tenant_service.py @@ -53,6 +53,37 @@ def create_workspace(db: Session, owner: User, data) -> Workspace: return workspace +def ensure_personal_workspace(db: Session, owner: User) -> Workspace: + existing = ( + db.query(Workspace) + .join(WorkspaceMembership, WorkspaceMembership.workspace_id == Workspace.id) + .filter(WorkspaceMembership.user_id == owner.id) + .order_by(Workspace.created_at.asc()) + .first() + ) + if existing is not None: + return existing + label = (owner.full_name or owner.username or "Personal").strip() + org_slug = f"{slugify(owner.username)}-personal" + if db.query(Organization).filter_by(slug=org_slug).first(): + org_slug = f"{org_slug}-{secrets.token_hex(3)}" + organization = Organization(name=f"{label}'s organization", slug=org_slug, owner_id=owner.id) + db.add(organization) + db.flush() + workspace = Workspace( + organization_id=organization.id, + name="Personal workspace", + slug="personal-workspace", + ) + db.add(workspace) + db.flush() + db.add(WorkspaceMembership(workspace_id=workspace.id, user_id=owner.id, role="owner")) + audit(db, organization.id, workspace.id, owner.id, "workspace.bootstrapped", workspace.name) + db.commit() + db.refresh(workspace) + return workspace + + def list_workspaces(db: Session, user_id: int): return ( db.query(Workspace, Organization, WorkspaceMembership.role) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index c9d0c16..c8df6dd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,7 @@ import ForbiddenPage from './pages/ForbiddenPage' import ProtectedRoute from './components/ProtectedRoute' import AccountPage from './pages/AccountPage' import WorkspaceAdminPage from './pages/WorkspaceAdminPage' +import InvitationPage from './pages/InvitationPage' import { authApi, saveAuth } from './lib/api' function GoogleCallback() { @@ -79,6 +80,7 @@ export default function App() { } /> + } /> } /> > = { } export default function DashboardLayout() { + const [searchParams] = useSearchParams() const [sidebarCollapsed, setSidebarCollapsed] = useState(false) const [activeNav, setActiveNav] = useState('for-you') const [flyout, setFlyout] = useState(null) + useEffect(() => { + const view = searchParams.get('view') as NavItemId | null + if (!view || !(view in flyoutMap)) return + setActiveNav(view) + setFlyout(flyoutMap[view] ?? null) + }, [searchParams]) + const { recent, projects, starredProjects, loading, error, refresh } = useDashboardDocuments() diff --git a/frontend/src/components/dashboard/DashboardSidebar.tsx b/frontend/src/components/dashboard/DashboardSidebar.tsx index 1f0af67..a306e54 100644 --- a/frontend/src/components/dashboard/DashboardSidebar.tsx +++ b/frontend/src/components/dashboard/DashboardSidebar.tsx @@ -65,7 +65,7 @@ export default function DashboardSidebar({ if (collapsed) { return ( -