diff --git a/packages/design-system/src/components/OcBreadcrumb/OcBreadcrumb.vue b/packages/design-system/src/components/OcBreadcrumb/OcBreadcrumb.vue index cf61af290c..0a5e578088 100644 --- a/packages/design-system/src/components/OcBreadcrumb/OcBreadcrumb.vue +++ b/packages/design-system/src/components/OcBreadcrumb/OcBreadcrumb.vue @@ -70,6 +70,9 @@ :aria-current="getAriaCurrent(index)" :to="item.to as RouteLocationRaw" class="first:text-base text-xl text-role-on-surface h-5 inline-flex items-center" + :class="{ + 'font-bold': index === displayItems.length - 1 + }" > {{ item.text @@ -93,7 +96,7 @@ 'leading-[1.2]', 'max-w-3xs', { - 'oc-breadcrumb-item-text-last': index === displayItems.length - 1 + 'oc-breadcrumb-item-text-last font-bold': index === displayItems.length - 1 } ]" v-text="item.text" @@ -164,7 +167,7 @@ 'justify-center': displayItems.length > 1 }" > - + diff --git a/packages/design-system/src/components/OcBreadcrumb/__snapshots__/OcBreadcrumb.spec.ts.snap b/packages/design-system/src/components/OcBreadcrumb/__snapshots__/OcBreadcrumb.spec.ts.snap index 4f9976a16b..d1c219b9a2 100644 --- a/packages/design-system/src/components/OcBreadcrumb/__snapshots__/OcBreadcrumb.spec.ts.snap +++ b/packages/design-system/src/components/OcBreadcrumb/__snapshots__/OcBreadcrumb.spec.ts.snap @@ -36,7 +36,7 @@ exports[`OcBreadcrumb > displays all items 1`] = ` -
Deeper ellipsize in responsive mode
" +
Deeper ellipsize in responsive mode
" `; exports[`OcBreadcrumb > sets correct variation 1`] = ` @@ -75,5 +75,5 @@ exports[`OcBreadcrumb > sets correct variation 1`] = ` -
Deeper ellipsize in responsive mode
" +
Deeper ellipsize in responsive mode
" `; diff --git a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue b/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue deleted file mode 100644 index 523e471397..0000000000 --- a/packages/web-app-files/src/components/AppBar/CreateAndUpload.vue +++ /dev/null @@ -1,219 +0,0 @@ - - - diff --git a/packages/web-app-files/src/composables/actions/files/index.ts b/packages/web-app-files/src/composables/actions/files/index.ts index 1cf249fa41..9ccfa7baa1 100644 --- a/packages/web-app-files/src/composables/actions/files/index.ts +++ b/packages/web-app-files/src/composables/actions/files/index.ts @@ -1,4 +1,5 @@ export * from './useFileActionsCopyPermanentLink' +export * from './useFileActionsClearClipboard' export * from './useFileActionsCopy' export * from './useFileActionsCreateLink' export * from './useFileActionsCreateNewFile' diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsClearClipboard.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsClearClipboard.ts new file mode 100644 index 0000000000..c6d0bf731e --- /dev/null +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsClearClipboard.ts @@ -0,0 +1,51 @@ +import { computed } from 'vue' +import { useGettext } from 'vue3-gettext' +import { + isPersonalSpaceResource, + isProjectSpaceResource, + isShareSpaceResource +} from '@opencloud-eu/web-client' +import { + FileAction, + isLocationTrashActive, + useClipboardStore, + useRouter +} from '@opencloud-eu/web-pkg' + +export const useFileActionsClearClipboard = () => { + const router = useRouter() + const { $gettext } = useGettext() + const clipboardStore = useClipboardStore() + + const actions = computed((): FileAction[] => [ + { + name: 'clearClipboard', + icon: 'eraser', + handler: () => { + clipboardStore.clearClipboard() + }, + label: () => $gettext('Clear clipboard'), + isVisible: ({ space }) => { + if (clipboardStore.resources.length === 0) { + return false + } + + if (isLocationTrashActive(router, 'files-trash-generic')) { + return false + } + + return ( + isProjectSpaceResource(space) || + isPersonalSpaceResource(space) || + isShareSpaceResource(space) + ) + }, + hideLabel: true, + class: 'oc-files-actions-clear-clipboard' + } + ]) + + return { + actions + } +} diff --git a/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts b/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts index 38e4615061..5613d559fb 100644 --- a/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts +++ b/packages/web-app-files/src/composables/actions/files/useFileActionsPaste.ts @@ -1,23 +1,28 @@ import { storeToRefs } from 'pinia' import { computed, unref } from 'vue' import { useGettext } from 'vue3-gettext' -import { Resource, SpaceResource, isShareSpaceResource } from '@opencloud-eu/web-client' +import { + Resource, + SpaceResource, + isPersonalSpaceResource, + isProjectSpaceResource, + isShareSpaceResource +} from '@opencloud-eu/web-client' import { ClipboardActions, FileAction, FileActionOptions, ResourceTransfer, TransferType, - isLocationCommonActive, - isLocationPublicActive, - isLocationSpacesActive, + isLocationTrashActive, isMacOs, useClientService, useClipboardStore, useGetMatchingSpace, usePasteWorker, useResourcesStore, - useRouter + useRouter, + useUserStore } from '@opencloud-eu/web-pkg' export const useFileActionsPaste = () => { @@ -27,6 +32,7 @@ export const useFileActionsPaste = () => { const { $gettext, $ngettext } = useGettext() const clipboardStore = useClipboardStore() const { startWorker } = usePasteWorker() + const userStore = useUserStore() const resourcesStore = useResourcesStore() const { currentFolder } = storeToRefs(resourcesStore) @@ -139,6 +145,20 @@ export const useFileActionsPaste = () => { clipboardStore.clearClipboard() } + const isMovingIntoSameFolder = computed(() => { + if (clipboardStore.action === ClipboardActions.Copy) { + return false + } + + if (!clipboardStore.resources || clipboardStore.resources.length < 1) { + return false + } + + return !clipboardStore.resources.some( + (resource) => resource.parentFolderId !== unref(currentFolder)?.id + ) + }) + const actions = computed((): FileAction[] => [ { name: 'paste', @@ -146,30 +166,38 @@ export const useFileActionsPaste = () => { handler, label: () => $gettext('Paste'), shortcut: unref(pasteShortcutString), - isVisible: ({ resources }) => { + isVisible: ({ space }) => { if (clipboardStore.resources.length === 0) { return false } - if ( - !isLocationSpacesActive(router, 'files-spaces-generic') && - !isLocationPublicActive(router, 'files-public-link') && - !isLocationCommonActive(router, 'files-common-favorites') - ) { + if (isLocationTrashActive(router, 'files-trash-generic')) { return false } - if (resources.length === 0) { - return false + + return ( + isProjectSpaceResource(space) || + isPersonalSpaceResource(space) || + isShareSpaceResource(space) + ) + }, + isDisabled: ({ space }) => { + if (!space) { + return true + } + return !space.canUpload({ user: userStore.user }) || unref(isMovingIntoSameFolder) + }, + disabledTooltip: ({ space }) => { + if (!space || !space.canUpload({ user: userStore.user })) { + return $gettext('You have no permission to paste files here.') } - if (isLocationPublicActive(router, 'files-public-link') && unref(currentFolder)) { - return unref(currentFolder)?.canCreate() + if (unref(isMovingIntoSameFolder)) { + return $gettext('You cannot cut and paste resources into the same folder.') } - // copy can't be restricted in authenticated context, because - // a user always has their home dir with write access - return true + return '' }, - class: 'oc-files-actions-copy-trigger' + class: 'oc-files-actions-copy-trigger font-bold' } ]) diff --git a/packages/web-app-files/src/composables/extensions/useFileActions.ts b/packages/web-app-files/src/composables/extensions/useFileActions.ts index e02e308760..464dc1ecc6 100644 --- a/packages/web-app-files/src/composables/extensions/useFileActions.ts +++ b/packages/web-app-files/src/composables/extensions/useFileActions.ts @@ -21,7 +21,8 @@ import { useFileActionsRename, useFileActionsShowDetails, useFileActionsShowShares, - useFileActionsToggleHideShare + useFileActionsToggleHideShare, + useFileActionsClearClipboard } from '../actions' import { unref } from 'vue' @@ -40,6 +41,7 @@ export const useFileActions = (): ActionExtension[] => { const { actions: enableSyncActions } = useFileActionsEnableSync() const { actions: moveActions } = useFileActionsMove() const { actions: pasteActions } = useFileActionsPaste() + const { actions: clearClipboardActions } = useFileActionsClearClipboard() const { actions: renameActions } = useFileActionsRename() const { actions: favoriteActions } = useFileActionsFavorite() const { actions: setSpaceImageActions } = useSpaceActionsSetImage() @@ -115,11 +117,20 @@ export const useFileActions = (): ActionExtension[] => { }, { id: 'com.github.opencloud-eu.web.files.context-action.paste', - extensionPointIds: [contextActionsExtensionPoint.id], + extensionPointIds: [batchActionsExtensionPoint.id], type: 'action', action: { ...unref(pasteActions)[0], - category: 'tertiary' + category: 'quaternary' + } + }, + { + id: 'com.github.opencloud-eu.web.files.context-action.clear-clipboard', + extensionPointIds: [batchActionsExtensionPoint.id], + type: 'action', + action: { + ...unref(clearClipboardActions)[0], + category: 'quaternary' } }, { @@ -174,7 +185,6 @@ export const useFileActions = (): ActionExtension[] => { id: 'com.github.opencloud-eu.web.files.context-action.favorite', extensionPointIds: [ previewToolBarActionsExtensionPointId, - batchActionsExtensionPoint.id, contextActionsExtensionPoint.id, fileSideBarActionsExtensionPoint.id ], diff --git a/packages/web-app-files/src/composables/index.ts b/packages/web-app-files/src/composables/index.ts index 38b2d94bef..6d9d475fa9 100644 --- a/packages/web-app-files/src/composables/index.ts +++ b/packages/web-app-files/src/composables/index.ts @@ -1,2 +1,3 @@ export * from './actions' export * from './resourcesViewDefaults' +export * from './useFileUpload' diff --git a/packages/web-app-files/src/composables/useFileUpload.ts b/packages/web-app-files/src/composables/useFileUpload.ts new file mode 100644 index 0000000000..142bc08c62 --- /dev/null +++ b/packages/web-app-files/src/composables/useFileUpload.ts @@ -0,0 +1,110 @@ +import { + useMessages, + useResourcesStore, + useRoute, + useSpacesStore, + useUserStore, + useClientService +} from '@opencloud-eu/web-pkg' +import { computed, onMounted, onBeforeUnmount, unref, watch, Ref } from 'vue' +import { SpaceResource, isPublicSpaceResource } from '@opencloud-eu/web-client' +import { useService, useUpload, UppyService, UploadResult } from '@opencloud-eu/web-pkg' +import { HandleUpload } from '../HandleUpload' +import { useGettext } from 'vue3-gettext' +import { storeToRefs } from 'pinia' + +export const useFileUpload = (space: Ref) => { + const uppyService = useService('$uppyService') + const clientService = useClientService() + const userStore = useUserStore() + const spacesStore = useSpacesStore() + const messageStore = useMessages() + const route = useRoute() + const language = useGettext() + + const resourcesStore = useResourcesStore() + const { currentFolder } = storeToRefs(resourcesStore) + + useUpload({ uppyService }) + + if (!uppyService.getPlugin('HandleUpload')) { + uppyService.addPlugin(HandleUpload, { + clientService, + language, + route, + space, + userStore, + spacesStore, + messageStore, + resourcesStore, + uppyService + }) + } + + let uploadCompletedSub: string + + const canUpload = computed(() => { + return unref(currentFolder)?.canUpload({ user: userStore.user }) + }) + + const onUploadComplete = async (result: UploadResult) => { + const file = result.successful?.[0] + if (!file) { + return + } + + const { spaceId, driveType } = file.meta + if (!isPublicSpaceResource(unref(space))) { + const isOwnSpace = spacesStore.spaces + .find(({ id }) => id === spaceId) + ?.isOwner(userStore.user) + + if (driveType === 'project' || isOwnSpace) { + const client = clientService.graphAuthenticated + const updatedSpace = await client.drives.getDrive(spaceId) + spacesStore.updateSpaceField({ + id: updatedSpace.id, + field: 'spaceQuota', + value: updatedSpace.spaceQuota + }) + } + } + + if (!unref(currentFolder) || spaceId !== unref(space).id) { + return + } + + const { children } = await clientService.webdav.listFiles(unref(space), { + path: unref(currentFolder).path + }) + + const existingIds = new Set(resourcesStore.resources.map((r) => r.id)) + const newResources = children.filter((child) => !existingIds.has(child.id)) + resourcesStore.upsertResources(newResources) + } + + onMounted(() => { + uploadCompletedSub = uppyService.subscribe('uploadCompleted', onUploadComplete) + }) + + onBeforeUnmount(() => { + uppyService.removePlugin(uppyService.getPlugin('HandleUpload')) + uppyService.unsubscribe('uploadCompleted', uploadCompletedSub) + uppyService.removeDropTarget() + }) + + watch( + canUpload, + () => { + const targetSelector = '#files-view' + const target = document.querySelector(targetSelector) + + if (target && unref(canUpload)) { + uppyService.useDropTarget({ targetSelector }) + } else { + uppyService.removeDropTarget() + } + }, + { immediate: true } + ) +} diff --git a/packages/web-app-files/src/views/spaces/GenericSpace.vue b/packages/web-app-files/src/views/spaces/GenericSpace.vue index ba6b5d0fe9..533524e4db 100644 --- a/packages/web-app-files/src/views/spaces/GenericSpace.vue +++ b/packages/web-app-files/src/views/spaces/GenericSpace.vue @@ -11,18 +11,7 @@ :space="space" :view-modes="viewModes" @item-dropped="fileDropped" - > - - + /> - diff --git a/packages/web-pkg/src/components/ContextActions/ActionMenuItem.vue b/packages/web-pkg/src/components/ContextActions/ActionMenuItem.vue index 6a605fe004..9ce4be49ee 100644 --- a/packages/web-pkg/src/components/ContextActions/ActionMenuItem.vue +++ b/packages/web-pkg/src/components/ContextActions/ActionMenuItem.vue @@ -11,7 +11,6 @@ : action.label(actionOptions) " data-testid="action-handler" - :size="size" justify-content="left" v-on="componentListeners" > diff --git a/packages/web-pkg/tests/unit/components/AppBar/AppBar.spec.ts b/packages/web-pkg/tests/unit/components/AppBar/AppBar.spec.ts index 85a5472ae1..5c57417fc3 100644 --- a/packages/web-pkg/tests/unit/components/AppBar/AppBar.spec.ts +++ b/packages/web-pkg/tests/unit/components/AppBar/AppBar.spec.ts @@ -11,7 +11,7 @@ import { } from '@opencloud-eu/web-test-helpers' import { ArchiverService } from '../../../../src/services' import { FolderView } from '../../../../src/ui/types' -import { useExtensionRegistry, ViewOptions } from '../../../../src' +import { ActionExtension, FileAction, useExtensionRegistry, ViewOptions } from '../../../../src' import { OcBreadcrumb } from '@opencloud-eu/design-system/components' import { useIsMobile } from '@opencloud-eu/design-system/composables' @@ -76,19 +76,18 @@ describe('AppBar component', () => { }) }) describe('bulkActions', () => { - it('if enabled', () => { - const { wrapper } = getShallowWrapper(selectedFiles, {}, { hasBulkActions: true }) - expect(wrapper.find(selectors.batchActionsStub).exists()).toBeTruthy() - }) - it('if 1 file selected on trash routes', () => { + it('if enabled and batch actions available', () => { + const currentRoute = mock({ + name: 'files-spaces-generic', + path: '/files/spaces/personal/admin' + }) const { wrapper } = getShallowWrapper( - [selectedFiles[0]], + selectedFiles, {}, { hasBulkActions: true }, - mock({ - name: 'files-trash-generic', - path: '/files/trash/personal/admin' - }) + currentRoute, + false, + true ) expect(wrapper.find(selectors.batchActionsStub).exists()).toBeTruthy() }) @@ -134,7 +133,8 @@ function getShallowWrapper( name: 'files-spaces-generic', path: '/files/spaces/personal/admin' }), - isMobile = false + isMobile = false, + hasBatchActions = false ) { vi.mocked(useIsMobile).mockReturnValue({ isMobile: computed(() => isMobile), @@ -147,8 +147,14 @@ function getShallowWrapper( } }) + const batchActions: ActionExtension[] = [] + if (hasBatchActions) { + batchActions.push( + mock({ action: mock({ isVisible: () => true }) }) + ) + } const { requestExtensions } = useExtensionRegistry() - vi.mocked(requestExtensions).mockReturnValue([]) + vi.mocked(requestExtensions).mockReturnValue(batchActions) const mocks = { ...defaultComponentMocks({ diff --git a/packages/web-pkg/tests/unit/components/AppBar/__snapshots__/AppBar.spec.ts.snap b/packages/web-pkg/tests/unit/components/AppBar/__snapshots__/AppBar.spec.ts.snap index 174c065695..91ef042572 100644 --- a/packages/web-pkg/tests/unit/components/AppBar/__snapshots__/AppBar.spec.ts.snap +++ b/packages/web-pkg/tests/unit/components/AppBar/__snapshots__/AppBar.spec.ts.snap @@ -12,8 +12,9 @@ exports[`AppBar component > renders > by default no breadcrumbs, no bulkactions, -
-
+
+
+
@@ -33,8 +34,9 @@ exports[`AppBar component > renders > if given, with content in the actions slot
-
-
+
+
+
@@ -54,8 +56,9 @@ exports[`AppBar component > renders > if given, with content in the content slot
-
-
+
+
+