From eed9504a535b44b5fba7ab2e72bd284484dcd79e Mon Sep 17 00:00:00 2001 From: AnuragJangid18 Date: Fri, 23 Oct 2026 21:54:48 +0530 Subject: [PATCH] feat: enhance workspace sidebar with create menu and document draft management - Added a create menu in the WorkspaceSidebar for creating new documents (live docs, pages, whiteboards, databases, smart links, folders). - Implemented document draft functionality to save and restore drafts for documents across various pages (ShedPage, SiloPage, SketchPage, SteadPage). - Improved error handling in useDashboardDocuments for better user feedback on API errors. - Introduced a NotificationBell component for displaying notifications with a read/unread state. - Updated document update and publish logic to clear drafts upon successful updates. - Enhanced user experience with keyboard shortcuts for editing and saving documents. --- README.md | 61 +++++ frontend/src/components/ProtectedRoute.tsx | 5 +- frontend/src/components/auth/LoginForm.tsx | 7 - .../src/components/create/CreateDropdown.tsx | 48 +++- .../components/dashboard/DashboardFeed.tsx | 6 +- .../components/dashboard/DashboardHeader.tsx | 9 +- .../components/dashboard/DashboardLayout.tsx | 4 + .../components/dashboard/DiscoverPanel.tsx | 17 +- .../components/dashboard/RecentActivities.tsx | 78 ++++-- .../notifications/NotificationBell.tsx | 76 ++++++ frontend/src/components/stead/SteadHub.tsx | 42 +++- .../components/workspace/WorkspaceShell.tsx | 78 ++++-- .../components/workspace/WorkspaceSidebar.tsx | 226 +++++++++++++++--- frontend/src/hooks/useDashboardDocuments.ts | 18 +- frontend/src/lib/api.ts | 11 + frontend/src/lib/documentDrafts.ts | 51 ++++ frontend/src/pages/LoginPage.tsx | 4 + frontend/src/pages/workspace/ShedPage.tsx | 65 ++++- frontend/src/pages/workspace/SiloPage.tsx | 104 ++++++-- frontend/src/pages/workspace/SketchPage.tsx | 32 ++- frontend/src/pages/workspace/SteadPage.tsx | 74 +++++- 21 files changed, 891 insertions(+), 125 deletions(-) create mode 100644 frontend/src/components/notifications/NotificationBell.tsx create mode 100644 frontend/src/lib/documentDrafts.ts diff --git a/README.md b/README.md index fd7eabc..b2e0c2e 100644 --- a/README.md +++ b/README.md @@ -320,3 +320,64 @@ Next tenant work: - 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. +- Dashboard and workspace notification bells now share the same notification + feed, unread indicator, read-state behavior, and notification panel. The + workspace header no longer displays the redundant people icon. +- Dashboard Feed activity now loads persisted per-document activity from the + collaboration API, orders it by newest event, links each event to its page, + and refreshes live every 15 seconds. Discover actions now open the existing + Templates or All Projects flyouts instead of being inert buttons. +- Dashboard workspace selection now validates the locally stored workspace ID + against the authenticated user’s workspace memberships before requesting + documents. Stale or revoked workspace IDs are cleared, and 403 membership + errors are no longer incorrectly reported as MongoDB outages. +- Authentication failures are now handled centrally: a protected API `401` + clears the stale local session and redirects to login, while protected routes + also require an access token instead of trusting only cached user metadata. + This prevents an expired token from leaving the dashboard visible while all + document and workspace requests fail. +- Page editing now follows Confluence-style keyboard actions: `E` enters edit + mode, `Ctrl/Cmd+S` updates the draft, and `Ctrl/Cmd+Enter` publishes (or + unpublishes) the page. The buttons expose the same shortcuts visually and + through `aria-keyshortcuts`, while typing in form fields or the editor does + not trigger the edit shortcut. +- Read-only page content now requires an explicit confirmation before editing: + double-clicking the content opens a “Would you like to edit this page?” + dialog, and only “Yes, edit” enables the editor. Escape and “No” leave the + page read-only. +- Workspace titles now support fast renaming: double-clicking a title enters + title-edit mode and selects the complete name, while the sidebar Rename + action preselects the full existing name. Pressing Enter commits the title + change once and stops the event from affecting page editing shortcuts. +- Unsaved edits now persist as per-document local drafts across workspace + navigation and reloads. Text pages and whiteboards restore a draft only when + it is newer than the server version, while successful Update, Publish, + history restore, archive, and delete operations clear the local draft. +- Deleting the active file from the workspace sidebar now keeps navigation + inside the workspace: it opens the parent page, a remaining sibling, or the + containing space instead of always redirecting to the dashboard. +- The workspace sidebar’s existing “Create new” button now opens the same + creation menu and uses the same page, whiteboard, space, and template + creation flows as the header Create button. Its original compact sidebar + appearance is preserved, with the menu opening upward to remain visible. +- The workspace sidebar creation menu now floats between the sidebar and the + workspace content instead of being constrained inside the sidebar. It is + aligned to the existing button and remains upward-opening and accessible + without changing the button’s appearance. +- File action menus opened from the workspace sidebar three-dot controls now + float outside the sidebar. They align to the left edge of the action button, + open downward for items with room below, and automatically open upward with + bottom-left alignment near the bottom of the list. +- Dashboard Recent Activities now includes the user’s recently visited files, + even when no separate activity event exists, and labels those fallback + entries as recently opened. The Discover something new panel no longer + displays the redundant See all button. +- Workspace editing shortcuts and title controls now apply consistently to + Silo pages, Sketch whiteboards, Shed documents, and Stead spaces. Each + supports E to edit, Ctrl/Cmd+S to update, Ctrl/Cmd+Enter to publish or + unpublish, double-click title selection, and Enter-to-commit title changes. + Stead plan controls also remain read-only until Edit mode is enabled. +- Every workspace sidebar file row now has a plus create action. Its floating + Confluence-style menu supports Live Doc, Page, Whiteboard, Database, Smart + Link, and Folder actions, creating the corresponding Silos document or + container in the current workspace context. diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx index ae545e6..80277e5 100644 --- a/frontend/src/components/ProtectedRoute.tsx +++ b/frontend/src/components/ProtectedRoute.tsx @@ -1,12 +1,13 @@ import { type ReactNode } from 'react' import { Navigate, useLocation } from 'react-router-dom' -import { getStoredUser } from '../lib/api' +import { getStoredUser, clearAuth } from '../lib/api' export default function ProtectedRoute({ children }: { children: ReactNode }) { const location = useLocation() const user = getStoredUser() - if (!user) { + if (!user || !localStorage.getItem('silos_access_token')) { + clearAuth() return } diff --git a/frontend/src/components/auth/LoginForm.tsx b/frontend/src/components/auth/LoginForm.tsx index 45689fa..6626371 100644 --- a/frontend/src/components/auth/LoginForm.tsx +++ b/frontend/src/components/auth/LoginForm.tsx @@ -99,13 +99,6 @@ export default function LoginForm({ - ) } diff --git a/frontend/src/components/create/CreateDropdown.tsx b/frontend/src/components/create/CreateDropdown.tsx index 4e25d71..a4f730c 100644 --- a/frontend/src/components/create/CreateDropdown.tsx +++ b/frontend/src/components/create/CreateDropdown.tsx @@ -1,5 +1,6 @@ import { useState, useRef, useEffect } from 'react' import { useNavigate } from 'react-router-dom' +import type { ReactNode } from 'react' import { Plus, ChevronRight, @@ -24,10 +25,25 @@ const iconMap: Record = { LayoutGrid, } -export default function CreateDropdown({ steadId, parentPageId }: { steadId?: string; parentPageId?: string }) { +interface CreateDropdownProps { + steadId?: string + parentPageId?: string + trigger?: (open: boolean, onClick: () => void) => ReactNode + dropUp?: boolean + floating?: boolean +} + +export default function CreateDropdown({ + steadId, + parentPageId, + trigger, + dropUp = false, + floating = false, +}: CreateDropdownProps) { const [open, setOpen] = useState(false) const [shedOpen, setShedOpen] = useState(false) const [pageOpen, setPageOpen] = useState(false) + const [floatingPosition, setFloatingPosition] = useState({ left: 0, bottom: 0 }) const ref = useRef(null) const navigate = useNavigate() @@ -46,6 +62,19 @@ export default function CreateDropdown({ steadId, parentPageId }: { steadId?: st const user = getStoredUser() const author = user?.full_name ?? 'You' + function toggleOpen() { + if (!open && floating && ref.current) { + const bounds = ref.current.getBoundingClientRect() + setFloatingPosition({ + left: bounds.right + 8, + bottom: window.innerHeight - bounds.bottom, + }) + } + setOpen(!open) + setShedOpen(false) + setPageOpen(false) + } + async function handleCreate(type: 'silo' | 'sketch') { const defaults: Record> = { silo: { body: '', bodyHtml: '', status: 'draft' }, @@ -103,20 +132,25 @@ export default function CreateDropdown({ steadId, parentPageId }: { steadId?: st return (
- + } {open && ( -
+

Create new

Choose what you want to build

diff --git a/frontend/src/components/dashboard/DashboardFeed.tsx b/frontend/src/components/dashboard/DashboardFeed.tsx index 7d8458e..de68484 100644 --- a/frontend/src/components/dashboard/DashboardFeed.tsx +++ b/frontend/src/components/dashboard/DashboardFeed.tsx @@ -35,6 +35,7 @@ interface DashboardFeedProps { error?: string | null onOpenRecent?: () => void onOpenTemplates: () => void + onOpenProjects: () => void } export default function DashboardFeed({ @@ -43,6 +44,7 @@ export default function DashboardFeed({ error, onOpenRecent, onOpenTemplates, + onOpenProjects, }: DashboardFeedProps) { const [activeTab, setActiveTab] = useState<'feed' | 'discover'>('feed') const user = getStoredUser() @@ -144,12 +146,12 @@ export default function DashboardFeed({ {!(!loading && recent.length === 0) && ( )} - +
) : (
- +
)}
diff --git a/frontend/src/components/dashboard/DashboardHeader.tsx b/frontend/src/components/dashboard/DashboardHeader.tsx index 8b3dccb..37bf498 100644 --- a/frontend/src/components/dashboard/DashboardHeader.tsx +++ b/frontend/src/components/dashboard/DashboardHeader.tsx @@ -3,13 +3,13 @@ import { Link } from 'react-router-dom' import { Menu, Search, - Bell, ChevronDown, } from 'lucide-react' import { logos } from '../../data/brandAssets' import { getStoredUser } from '../../lib/api' import CreateDropdown from '../create/CreateDropdown' import SearchOverlay from '../search/SearchOverlay' +import NotificationBell from '../notifications/NotificationBell' interface DashboardHeaderProps { onToggleSidebar: () => void @@ -81,12 +81,7 @@ export default function DashboardHeader({
- +
{displayName[0]} diff --git a/frontend/src/components/dashboard/DashboardLayout.tsx b/frontend/src/components/dashboard/DashboardLayout.tsx index 3136b6e..0d27ecd 100644 --- a/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/frontend/src/components/dashboard/DashboardLayout.tsx @@ -91,6 +91,10 @@ export default function DashboardLayout() { setActiveNav('templates') setFlyout('templates') }} + onOpenProjects={() => { + setActiveNav('all-projects') + setFlyout('all-projects') + }} />
diff --git a/frontend/src/components/dashboard/DiscoverPanel.tsx b/frontend/src/components/dashboard/DiscoverPanel.tsx index 5ee1d86..c807032 100644 --- a/frontend/src/components/dashboard/DiscoverPanel.tsx +++ b/frontend/src/components/dashboard/DiscoverPanel.tsx @@ -13,15 +13,22 @@ const iconMap: Record = { BookOpen, } -export default function DiscoverPanel() { +interface DiscoverPanelProps { + onOpenTemplates?: () => void + onOpenProjects?: () => void +} + +export default function DiscoverPanel({ onOpenTemplates, onOpenProjects }: DiscoverPanelProps) { + function handleDiscover(id: string) { + if (id === '1') onOpenTemplates?.() + else onOpenProjects?.() + } + return (

Discover something new

-
    @@ -29,7 +36,7 @@ export default function DiscoverPanel() { const Icon = iconMap[item.icon] return (
  • - + {open && ( +
    +
    +

    Notifications

    + +
    + {notifications.length === 0 ? ( +

    You’re all caught up.

    + ) : ( + notifications.slice(0, 8).map((notification) => ( +
    +

    {notification.message}

    + +
    + )) + )} +
    + )} +
+ ) +} diff --git a/frontend/src/components/stead/SteadHub.tsx b/frontend/src/components/stead/SteadHub.tsx index 2e3a8a8..e100ee5 100644 --- a/frontend/src/components/stead/SteadHub.tsx +++ b/frontend/src/components/stead/SteadHub.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { useNavigate } from 'react-router-dom' import { Plus, FileText, PenTool, Zap, LayoutGrid } from 'lucide-react' import type { SteadContent } from '../../data/steadData' @@ -16,6 +16,10 @@ interface SteadHubProps { onSectionChange: (section: string) => void onAddPlan: () => void onUpdatePlan: (index: number, field: string, value: string) => void + editing: boolean + onTitleChange: (title: string) => void + onTitleDoubleClick: () => void + onTitleCommit: () => void } const typeIcons: Record = { @@ -31,9 +35,14 @@ export default function SteadHub({ onSectionChange, onAddPlan, onUpdatePlan, + editing, + onTitleChange, + onTitleDoubleClick, + onTitleCommit, }: SteadHubProps) { const navigate = useNavigate() const [linkedDocs, setLinkedDocs] = useState([]) + const titleRef = useRef(null) useEffect(() => { void listDocumentsByStead(doc.id).then(setLinkedDocs) @@ -66,7 +75,29 @@ export default function SteadHub({ > {content.icon} - {doc.title} + onTitleChange(event.target.value)} + onDoubleClick={() => { + onTitleDoubleClick() + requestAnimationFrame(() => { + titleRef.current?.focus() + titleRef.current?.select() + }) + }} + onKeyDown={(event) => { + if (event.key === 'Enter') { + event.preventDefault() + event.stopPropagation() + onTitleCommit() + titleRef.current?.blur() + } + }} + className="min-w-0 flex-1 border-none bg-transparent text-sm font-semibold text-silos-text outline-none read-only:cursor-default" + aria-label="Space title" + />