Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/assets/AssetList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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]
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/folders/FolderCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/components/folders/FolderRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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',
Expand Down
101 changes: 101 additions & 0 deletions frontend/src/components/folders/ShareFolderPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<template>
<SidePanel :open="open" title="Share folder" @update:open="emit('update:open', $event)">
<div class="space-y-6 p-4">
<div class="flex items-start justify-between gap-4">
<div>
<p class="text-base-medium text-ink-gray-8">Public share link</p>
<p class="mt-1 text-p-sm text-ink-gray-5">
Anyone with the link can browse and download the files in this folder.
Subfolders are not included.
</p>
</div>
<Switch v-model="enabled" aria-label="Public share link" :disabled="toggling" />
</div>

<div v-if="enabled && shareUrl" class="space-y-2">
<FormControl :model-value="shareUrl" label="Share link" readonly />
<Button label="Copy link" icon-left="lucide-copy" variant="subtle" @click="copy" />
</div>

<Alert v-if="enabled" title="Link access is enabled" variant="subtle" theme="green">
Revoking the link immediately removes guest access.
</Alert>
</div>
</SidePanel>
</template>

<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { Alert, Button, FormControl, Switch, toast, useCall } from 'frappe-ui'
import type { Folder } from '@/types'
import { serverMessage } from '@/lib/format'
import SidePanel from '@/components/common/SidePanel.vue'

const props = defineProps<{ open: boolean; folder: Folder }>()
const emit = defineEmits<{ 'update:open': [value: boolean]; changed: [] }>()

const enabled = ref(Boolean(props.folder.share_token))
const token = ref(props.folder.share_token ?? '')
const toggling = ref(false)
let syncing = false

const shareUrl = computed(() =>
token.value
? `${window.location.origin}/vms/shared/folder/${props.folder.name}?token=${token.value}`
: '',
)

const enableSharing = useCall<{ share_token: string; share_url: string }, { folder: string }>({
url: '/api/v2/method/vms.api.enable_folder_sharing',
method: 'POST',
immediate: false,
})
const disableSharing = useCall<{ status: string }, { folder: string }>({
url: '/api/v2/method/vms.api.disable_folder_sharing',
method: 'POST',
immediate: false,
})

watch(
() => props.folder,
(value) => {
syncing = true
token.value = value.share_token ?? ''
enabled.value = Boolean(value.share_token)
syncing = false
},
)

watch(enabled, async (value, previous) => {
if (syncing || value === previous) return
toggling.value = true
try {
if (value) {
const result = await enableSharing.submit({ folder: props.folder.name })
token.value = result?.share_token ?? ''
toast.success('Folder sharing enabled')
} else {
await disableSharing.submit({ folder: props.folder.name })
token.value = ''
toast.success('Folder sharing disabled')
}
emit('changed')
} catch (error) {
syncing = true
enabled.value = !value
syncing = false
toast.error(serverMessage(error) || 'Could not update sharing')
} finally {
toggling.value = false
}
})

async function copy() {
try {
await navigator.clipboard.writeText(shareUrl.value)
toast.success('Share link copied')
} catch {
toast.error('Could not copy the link')
}
}
</script>
10 changes: 9 additions & 1 deletion frontend/src/components/projects/useProjectBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,15 @@ export function useProjectBrowser(
const project = useDoc<Project>({ doctype: 'VMS Project', name: currentProject })
const folders = useList<Folder>({
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,
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/components/projects/useProjectPageActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Folder | null>(null)
const shareFolderTarget = ref<Folder | null>(null)

const plainDescription = computed(() =>
(browser.project.doc?.description ?? '')
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -163,8 +176,10 @@ export function useProjectPageActions(
moveFolderOpen,
moveAssetsOpen,
shareOpen,
shareFolderOpen,
settingsOpen,
folderAction,
shareFolderTarget,
plainDescription,
projectActions,
folderActions,
Expand All @@ -173,6 +188,7 @@ export function useProjectPageActions(
openFolder,
openRenameFolder,
openMoveFolder,
openShareFolder,
handleFolderMoved,
handleAssetsMoved,
saveProject,
Expand Down
27 changes: 22 additions & 5 deletions frontend/src/pages/ProjectDetailPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,18 +37,23 @@
@drop-folder="moveFolder"
/>
<PageHeaderTitle v-else><h1 class="truncate">Project</h1></PageHeaderTitle>
<Dropdown v-if="project.doc" :options="projectActions" align="end">
<Button variant="ghost" icon="lucide-ellipsis" aria-label="Project actions" />
<Dropdown
v-if="project.doc"
:options="currentFolderDoc ? folderActions : projectActions"
align="end"
>
<Button
variant="ghost"
icon="lucide-ellipsis"
:aria-label="currentFolderDoc ? 'Folder actions' : 'Project actions'"
/>
</Dropdown>
</div>
<!-- One right-hand group: `Dropdown` declares `inheritAttrs: false`, so a
`sm:hidden` on the component itself never reaches the DOM and the
mobile trigger used to sit apart from the actions at every width. -->
<div class="flex shrink-0 items-center gap-2">
<div class="hidden items-center gap-2 sm:flex">
<Dropdown v-if="currentFolderDoc" :options="folderActions" align="end">
<Button variant="subtle" icon="lucide-folder-cog" label="Folder actions" />
</Dropdown>
<Button
label="New folder"
icon-left="lucide-folder-plus"
Expand Down Expand Up @@ -129,6 +134,7 @@
draggable
@rename="openRenameFolder"
@move="openMoveFolder"
@share="openShareFolder"
@delete="deleteFolder"
@drop-assets="(names, target) => moveAssets(names, target)"
@drop-folder="(name, target) => moveFolder(name, target)"
Expand Down Expand Up @@ -163,6 +169,7 @@
@changed="reloadAssets"
@rename-folder="openRenameFolder"
@move-folder="openMoveFolder"
@share-folder="openShareFolder"
@delete-folder="deleteFolder"
@drop-assets="(names, target) => moveAssets(names, target)"
@drop-folder="(name, target) => moveFolder(name, target)"
Expand Down Expand Up @@ -237,6 +244,12 @@
:project="project.doc"
@changed="project.reload"
/>
<ShareFolderPanel
v-if="shareFolderTarget"
v-model:open="shareFolderOpen"
:folder="shareFolderTarget"
@changed="reloadAll"
/>
<ProjectSettingsDialog
v-if="project.doc"
v-model:open="settingsOpen"
Expand Down Expand Up @@ -279,6 +292,7 @@ import FolderCard from '@/components/folders/FolderCard.vue'
import MoveFolderDialog from '@/components/folders/MoveFolderDialog.vue'
import MoveToFolderDialog from '@/components/folders/MoveToFolderDialog.vue'
import RenameFolderDialog from '@/components/folders/RenameFolderDialog.vue'
import ShareFolderPanel from '@/components/folders/ShareFolderPanel.vue'
import ProjectBrowserToolbar from '@/components/projects/ProjectBrowserToolbar.vue'
import ProjectSettingsDialog from '@/components/projects/ProjectSettingsDialog.vue'
import IdentityAvatar from '@/components/common/IdentityAvatar.vue'
Expand Down Expand Up @@ -347,8 +361,10 @@ const {
moveFolderOpen,
moveAssetsOpen,
shareOpen,
shareFolderOpen,
settingsOpen,
folderAction,
shareFolderTarget,
plainDescription,
projectActions,
folderActions,
Expand All @@ -357,6 +373,7 @@ const {
openFolder,
openRenameFolder,
openMoveFolder,
openShareFolder,
handleFolderMoved,
handleAssetsMoved,
saveProject,
Expand Down
Loading
Loading