From e23bb8cab0bdd6b2e1c4810911b6aa988ef2339e Mon Sep 17 00:00:00 2001 From: vishwajeet-13 Date: Sun, 6 Sep 2026 00:01:29 +0530 Subject: [PATCH 1/3] feat: folder-scoped sharing Share a single VMS Folder without exposing the rest of the project. Each folder gets its own share_token, independent of the project share and of other folders. The public link is /vms/shared/folder/ with no project segment, so moving the folder never breaks it. Trashing the folder clears the token; scope is the folder's own assets only (nested subfolders excluded). Backend (vms/api.py, VMS Folder): - share_token field + revoke_share_on_trash controller hook - enable/disable_folder_sharing, get_shared_folder, get_shared_folder_assets - get_shared_asset_view_url / _download_url take an optional folder arg, validated against the folder token (project path unchanged); folder validation also rejects an unshared folder for authenticated non-VMS users Frontend: - ShareFolderPanel, Share entry in folder card / row / actions menus - /vms/shared/folder/:folderId route reuses SharedProjectPage in folder mode - in-folder header menu now shows the folder's actions Tests deferred (see plans/folder-scoped-sharing.md). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01UDbtFtgkUmtPEU8PhbcoC5 --- frontend/components.d.ts | 1 + frontend/src/components/assets/AssetList.vue | 2 + .../src/components/folders/FolderCard.vue | 2 + frontend/src/components/folders/FolderRow.vue | 2 + .../components/folders/ShareFolderPanel.vue | 101 ++++++++++++ .../components/projects/useProjectBrowser.ts | 10 +- .../projects/useProjectPageActions.ts | 16 ++ frontend/src/pages/ProjectDetailPage.vue | 27 +++- frontend/src/pages/SharedProjectPage.vue | 104 +++++++----- frontend/src/router.ts | 7 + frontend/src/types.ts | 1 + plans/folder-scoped-sharing.md | 87 ++++++++++ vms/api.py | 148 ++++++++++++++++-- .../doctype/vms_folder/vms_folder.json | 17 +- .../doctype/vms_folder/vms_folder.py | 5 + 15 files changed, 469 insertions(+), 61 deletions(-) create mode 100644 frontend/src/components/folders/ShareFolderPanel.vue create mode 100644 plans/folder-scoped-sharing.md diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 816dda9..d31090b 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -66,6 +66,7 @@ declare module 'vue' { SettingsTabTranscription: typeof import('./src/components/settings/SettingsTabTranscription.vue')['default'] SettingsTabUsers: typeof import('./src/components/settings/SettingsTabUsers.vue')['default'] SettingsTabYoutube: typeof import('./src/components/settings/SettingsTabYoutube.vue')['default'] + ShareFolderPanel: typeof import('./src/components/folders/ShareFolderPanel.vue')['default'] ShareProjectPanel: typeof import('./src/components/projects/ShareProjectPanel.vue')['default'] SidebarProjects: typeof import('./src/components/shell/SidebarProjects.vue')['default'] SidePanel: typeof import('./src/components/common/SidePanel.vue')['default'] diff --git a/frontend/src/components/assets/AssetList.vue b/frontend/src/components/assets/AssetList.vue index f109480..874f011 100644 --- a/frontend/src/components/assets/AssetList.vue +++ b/frontend/src/components/assets/AssetList.vue @@ -37,6 +37,7 @@ draggable @rename="emit('rename-folder', $event)" @move="emit('move-folder', $event)" + @share="emit('share-folder', $event)" @delete="emit('delete-folder', $event)" @drop-assets="(names, target) => emit('drop-assets', names, target)" @drop-folder="(name, target) => emit('drop-folder', name, target)" @@ -148,6 +149,7 @@ const emit = defineEmits<{ 'open-folder': [name: string] 'rename-folder': [folder: Folder] 'move-folder': [folder: Folder] + 'share-folder': [folder: Folder] 'delete-folder': [folder: Folder] 'drop-assets': [names: string[], folder: string] 'drop-folder': [name: string, folder: string] diff --git a/frontend/src/components/folders/FolderCard.vue b/frontend/src/components/folders/FolderCard.vue index f226ec9..1e58ea7 100644 --- a/frontend/src/components/folders/FolderCard.vue +++ b/frontend/src/components/folders/FolderCard.vue @@ -82,6 +82,7 @@ const props = defineProps<{ const emit = defineEmits<{ rename: [folder: Folder] move: [folder: Folder] + share: [folder: Folder] delete: [folder: Folder] 'drop-assets': [names: string[], folder: string] 'drop-folder': [name: string, folder: string] @@ -91,6 +92,7 @@ const dragOver = ref(false) const actions = computed(() => [ { label: 'Rename', icon: 'lucide-pencil', onClick: () => emit('rename', props.folder) }, { label: 'Move', icon: 'lucide-folder-input', onClick: () => emit('move', props.folder) }, + { label: 'Share', icon: 'lucide-share-2', onClick: () => emit('share', props.folder) }, { label: 'Delete', icon: 'lucide-trash-2', diff --git a/frontend/src/components/folders/FolderRow.vue b/frontend/src/components/folders/FolderRow.vue index 82842ab..d7afb30 100644 --- a/frontend/src/components/folders/FolderRow.vue +++ b/frontend/src/components/folders/FolderRow.vue @@ -62,6 +62,7 @@ const props = defineProps<{ const emit = defineEmits<{ rename: [folder: Folder] move: [folder: Folder] + share: [folder: Folder] delete: [folder: Folder] 'drop-assets': [names: string[], folder: string] 'drop-folder': [name: string, folder: string] @@ -71,6 +72,7 @@ const dragOver = ref(false) const actions = computed(() => [ { label: 'Rename', icon: 'lucide-pencil', onClick: () => emit('rename', props.folder) }, { label: 'Move', icon: 'lucide-folder-input', onClick: () => emit('move', props.folder) }, + { label: 'Share', icon: 'lucide-share-2', onClick: () => emit('share', props.folder) }, { label: 'Delete', icon: 'lucide-trash-2', diff --git a/frontend/src/components/folders/ShareFolderPanel.vue b/frontend/src/components/folders/ShareFolderPanel.vue new file mode 100644 index 0000000..c51779b --- /dev/null +++ b/frontend/src/components/folders/ShareFolderPanel.vue @@ -0,0 +1,101 @@ + + + diff --git a/frontend/src/components/projects/useProjectBrowser.ts b/frontend/src/components/projects/useProjectBrowser.ts index c6da39a..457e8a2 100644 --- a/frontend/src/components/projects/useProjectBrowser.ts +++ b/frontend/src/components/projects/useProjectBrowser.ts @@ -49,7 +49,15 @@ export function useProjectBrowser( const project = useDoc({ doctype: 'VMS Project', name: currentProject }) const folders = useList({ doctype: 'VMS Folder', - fields: ['name', 'folder_name', 'project', 'parent_folder', 'creation', 'modified'], + fields: [ + 'name', + 'folder_name', + 'project', + 'parent_folder', + 'share_token', + 'creation', + 'modified', + ], filters: () => ({ project: currentProject.value, deleted_at: ['is', 'not set'] }), orderBy: 'folder_name asc', limit: 500, diff --git a/frontend/src/components/projects/useProjectPageActions.ts b/frontend/src/components/projects/useProjectPageActions.ts index 358ec65..3cfaf97 100644 --- a/frontend/src/components/projects/useProjectPageActions.ts +++ b/frontend/src/components/projects/useProjectPageActions.ts @@ -19,8 +19,10 @@ export function useProjectPageActions( const moveFolderOpen = ref(false) const moveAssetsOpen = ref(false) const shareOpen = ref(false) + const shareFolderOpen = ref(false) const settingsOpen = ref(false) const folderAction = ref(null) + const shareFolderTarget = ref(null) const plainDescription = computed(() => (browser.project.doc?.description ?? '') @@ -52,6 +54,12 @@ export function useProjectPageActions( onClick: () => browser.currentFolderDoc.value && openMoveFolder(browser.currentFolderDoc.value), }, + { + label: 'Share', + icon: 'lucide-share-2', + onClick: () => + browser.currentFolderDoc.value && openShareFolder(browser.currentFolderDoc.value), + }, { label: 'Delete', icon: 'lucide-trash-2', @@ -92,6 +100,11 @@ export function useProjectPageActions( moveFolderOpen.value = true } + function openShareFolder(folder: Folder) { + shareFolderTarget.value = folder + shareFolderOpen.value = true + } + async function handleFolderMoved(targetProject: string) { if ( folderAction.value?.name === browser.currentFolder.value && @@ -163,8 +176,10 @@ export function useProjectPageActions( moveFolderOpen, moveAssetsOpen, shareOpen, + shareFolderOpen, settingsOpen, folderAction, + shareFolderTarget, plainDescription, projectActions, folderActions, @@ -173,6 +188,7 @@ export function useProjectPageActions( openFolder, openRenameFolder, openMoveFolder, + openShareFolder, handleFolderMoved, handleAssetsMoved, saveProject, diff --git a/frontend/src/pages/ProjectDetailPage.vue b/frontend/src/pages/ProjectDetailPage.vue index 034f76c..c909643 100644 --- a/frontend/src/pages/ProjectDetailPage.vue +++ b/frontend/src/pages/ProjectDetailPage.vue @@ -37,8 +37,16 @@ @drop-folder="moveFolder" />

Project

- -