diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-11-17 01:12:41 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-17 01:12:41 +0000 |
| commit | 88c73e212c4510ce41ad8c6557fa7d5c8f72d199 (patch) | |
| tree | 11f47349b8c34de1bf541febd9ba48cc44aa305a /apps/web/app | |
| parent | cc8fee0d28d87299ee9a3ad11dcb4ae5a7b86c15 (diff) | |
| download | karakeep-88c73e212c4510ce41ad8c6557fa7d5c8f72d199.tar.zst | |
feat: Add collaborative lists (#2146)
* feat: Add collaborative lists backend implementation
This commit implements the core backend functionality for collaborative
lists, allowing multiple users to share and interact with bookmark lists.
Database changes:
- Add listCollaborators table to track users with access to lists and
their roles (viewer/editor)
- Add addedBy field to bookmarksInLists to track who added bookmarks
- Add relations for collaborative list functionality
Access control updates:
- Update List model to support role-based access (owner/editor/viewer)
- Add methods to check and enforce permissions for list operations
- Update Bookmark model to allow access through collaborative lists
- Modify bookmark queries to include bookmarks from collaborative lists
List collaboration features:
- Add/remove/update collaborators
- Get list of collaborators
- Get lists shared with current user
- Only manual lists can have collaborators
tRPC procedures:
- addCollaborator: Add a user as a collaborator to a list
- removeCollaborator: Remove a collaborator from a list
- updateCollaboratorRole: Change a collaborator's role
- getCollaborators: Get all collaborators for a list
- getSharedWithMe: Get all lists shared with the current user
- cloneBookmark: Clone a bookmark to the current user's collection
Implementation notes:
- Editors can add/remove bookmarks from the list (must own the bookmark)
- Viewers can only view bookmarks in the list
- Only the list owner can manage collaborators and list metadata
- Smart lists cannot have collaborators (only manual lists)
- Users cannot edit bookmarks they don't own, even in shared lists
* feat: Add collaborative lists frontend UI
This commit implements the frontend user interface for collaborative lists,
allowing users to view shared bookmarks and manage list collaborators.
New pages:
- /dashboard/shared: Shows bookmarks from lists shared with the user
- Displays bookmarks from all collaborative lists
- Uses SharedBookmarks component
- Shows empty state when no lists are shared
Navigation:
- Added "Shared with you" link to sidebar with Users icon
- Positioned after "Home" in main navigation
- Available in both desktop and mobile sidebar
Collaborator management:
- ManageCollaboratorsModal component for managing list collaborators
- Add collaborators by user ID with viewer/editor role
- View current collaborators with their roles
- Update collaborator roles inline
- Remove collaborators
- Shows empty state when no collaborators
- Integrated into ListOptions dropdown menu
- Accessible via "Manage Collaborators" menu item
Components created:
- SharedBookmarks.tsx: Server component fetching shared lists/bookmarks
- ManageCollaboratorsModal.tsx: Client component with tRPC mutations
- /dashboard/shared/page.tsx: Route for shared bookmarks page
UI features:
- Role selector for viewer/editor permissions
- Real-time collaborator list updates
- Toast notifications for success/error states
- Loading states for async operations
- Responsive design matching existing UI patterns
Implementation notes:
- Uses existing tRPC endpoints (getSharedWithMe, getCollaborators, etc.)
- Follows established modal patterns from ShareListModal
- Integrates seamlessly with existing list UI
- Currently uses user ID for adding collaborators (email lookup TBD)
* fix typecheck
* add collaborator by email
* add shared list in the sidebar
* fix perm issue
* hide UI components from non list owners
* list leaving
* fix shared bookmarks showing up in homepage
* fix getBookmark access check
* e2e tests
* hide user specific fields from shared lists
* simplify bookmark perm checks
* disable editable fields in bookmark preview
* hide lists if they don't have options
* fix list ownership
* fix highlights
* move tests to trpc
* fix alignment of leave list
* make tag lists unclickable
* allow editors to remove from list
* add a badge for shared lists
* remove bookmarks of user when they're removed from a list
* fix tests
* show owner in the manage collab modal
* fix hasCollab
* drop shared with you
* i18n
* beta badge
* correctly invalidate caches on collab change
* reduce unnecessary changes
* Add ratelimits
* stop manually removing bookmarks on remove
* some fixes
* fixes
* remove unused function
* improve tests
---------
Co-authored-by: Claude <noreply@anthropic.com>
Diffstat (limited to 'apps/web/app')
| -rw-r--r-- | apps/web/app/dashboard/lists/[listId]/page.tsx | 5 | ||||
| -rw-r--r-- | apps/web/app/reader/[bookmarkId]/page.tsx | 14 |
2 files changed, 12 insertions, 7 deletions
diff --git a/apps/web/app/dashboard/lists/[listId]/page.tsx b/apps/web/app/dashboard/lists/[listId]/page.tsx index 3f9c3416..2b2cc9bb 100644 --- a/apps/web/app/dashboard/lists/[listId]/page.tsx +++ b/apps/web/app/dashboard/lists/[listId]/page.tsx @@ -50,6 +50,9 @@ export default async function ListPage(props: { ? searchParams.includeArchived === "true" : userSettings.archiveDisplayBehaviour === "show"; + // Only show editor card if user is owner or editor (not viewer) + const canEdit = list.userRole === "owner" || list.userRole === "editor"; + return ( <BookmarkListContextProvider list={list}> <Bookmarks @@ -58,7 +61,7 @@ export default async function ListPage(props: { archived: !includeArchived ? false : undefined, }} showDivider={true} - showEditorCard={list.type === "manual"} + showEditorCard={list.type === "manual" && canEdit} header={<ListHeader initialData={list} />} /> </BookmarkListContextProvider> diff --git a/apps/web/app/reader/[bookmarkId]/page.tsx b/apps/web/app/reader/[bookmarkId]/page.tsx index 7c2b0c9e..e32811a9 100644 --- a/apps/web/app/reader/[bookmarkId]/page.tsx +++ b/apps/web/app/reader/[bookmarkId]/page.tsx @@ -1,7 +1,7 @@ "use client"; import { Suspense, useState } from "react"; -import { useRouter } from "next/navigation"; +import { useParams, useRouter } from "next/navigation"; import HighlightCard from "@/components/dashboard/highlights/HighlightCard"; import ReaderView from "@/components/dashboard/preview/ReaderView"; import { Button } from "@/components/ui/button"; @@ -29,16 +29,14 @@ import { Type, X, } from "lucide-react"; +import { useSession } from "next-auth/react"; import { api } from "@karakeep/shared-react/trpc"; import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; import { getBookmarkTitle } from "@karakeep/shared/utils/bookmarkUtils"; -export default function ReaderViewPage({ - params, -}: { - params: { bookmarkId: string }; -}) { +export default function ReaderViewPage() { + const params = useParams<{ bookmarkId: string }>(); const bookmarkId = params.bookmarkId; const { data: highlights } = api.highlights.getForBookmark.useQuery({ bookmarkId, @@ -47,12 +45,14 @@ export default function ReaderViewPage({ bookmarkId, }); + const { data: session } = useSession(); const router = useRouter(); const [fontSize, setFontSize] = useState([18]); const [lineHeight, setLineHeight] = useState([1.6]); const [fontFamily, setFontFamily] = useState("serif"); const [showHighlights, setShowHighlights] = useState(false); const [showSettings, setShowSettings] = useState(false); + const isOwner = session?.user?.id === bookmark?.userId; const fontFamilies = { serif: "ui-serif, Georgia, Cambria, serif", @@ -245,6 +245,7 @@ export default function ReaderViewPage({ lineHeight: lineHeight[0], }} bookmarkId={bookmarkId} + readOnly={!isOwner} /> </div> </Suspense> @@ -299,6 +300,7 @@ export default function ReaderViewPage({ key={highlight.id} highlight={highlight} clickable={true} + readOnly={!isOwner} /> ))} </div> |
