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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div class="flex flex-wrap items-center flex-1" :role="role" :aria-live="ariaLive">
<div class="flex items-center justify-between w-full">
<div class="flex items-center">
<oc-icon name="information" fill-type="line" class="mr-2" />
<oc-icon v-if="showInfoIcon" name="information" fill-type="line" class="mr-2" />
<div class="oc-notification-message-title text-lg">
{{ title }}
</div>
Expand All @@ -32,6 +32,7 @@
<oc-icon :name="showErrorLog ? 'arrow-up-s' : 'arrow-down-s'" />
</oc-button>
</div>
<slot name="actions" />
<oc-error-log v-if="showErrorLog" class="mt-4" :content="errorLogContent" />
</div>
</div>
Expand Down Expand Up @@ -66,6 +67,11 @@ export interface Props {
* @default 5
*/
timeout?: number
/**
* @docs Whether to show an info icon prior to the title.
* @default true
*/
showInfoIcon?: boolean
}

export interface Emits {
Expand All @@ -75,9 +81,24 @@ export interface Emits {
(e: 'close'): void
}

const { title, errorLogContent, message, status = 'passive', timeout = 5 } = defineProps<Props>()
export interface Slots {
/**
* @docs Slot for action buttons.
*/
actions?: () => unknown
}

const {
title,
errorLogContent,
message,
status = 'passive',
timeout = 5,
showInfoIcon = true
} = defineProps<Props>()

const emit = defineEmits<Emits>()
defineSlots<Slots>()

const showErrorLog = ref(false)

Expand Down
1 change: 1 addition & 0 deletions packages/design-system/src/directives/OcTooltip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ const initOrUpdate = (
if (!el.tooltip) {
el.tooltip = tippy(el, {
...props,
zIndex: 10000,
plugins: [hideOnEsc, customProps]
})
return
Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/actions/files/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,4 @@ export * from './useFileActionsOpenShortcut'
export * from './useFileActionsCreateLink'
export * from './useFileActionsOpenWithApp'
export * from './useFileActionsSaveAs'
export * from './useFileActionsUndoDelete'
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@ import type { FileAction, FileActionOptions } from '../types'
import { useMessages, useSpacesStore, useUserStore, useResourcesStore } from '../../piniaStores'
import { useRestoreWorker } from '../../webWorkers/restoreWorker'

export const useFileActionsRestore = () => {
export const useFileActionsRestore = ({
showSuccessMessage = true,
onRestoreComplete
}: {
showSuccessMessage?: boolean
onRestoreComplete?: (result: FileActionOptions) => void
} = {}) => {
const { showMessage, showErrorMessage } = useMessages()
const userStore = useUserStore()
const router = useRouter()
Expand Down Expand Up @@ -130,7 +136,10 @@ export const useFileActionsRestore = () => {
resourceCount: successful.length.toString()
})
}
showMessage({ title })

if (showSuccessMessage) {
showMessage({ title })
}

// user hasn't navigated to another location meanwhile
if (
Expand All @@ -149,6 +158,7 @@ export const useFileActionsRestore = () => {
field: 'spaceQuota',
value: updatedSpace.spaceQuota
})
onRestoreComplete?.({ space, resources: successful })
}

if (failed.length) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { computed, unref } from 'vue'
import { useGettext } from 'vue3-gettext'
import type { Action, FileActionOptions } from '../types'
import { useMessages, useResourcesStore } from '../../piniaStores'
import { useFileActionsRestore } from './useFileActionsRestore'
import { storeToRefs } from 'pinia'
import { useCapabilityStore, useClientService } from '../../'
import { isPersonalSpaceResource, isProjectSpaceResource } from '@opencloud-eu/web-client'
import { isMacOs } from '../../../helpers'

type UndoActionOptions = FileActionOptions & { callback?: () => void }

export const useFileActionsUndoDelete = () => {
const { $gettext } = useGettext()
const { showErrorMessage } = useMessages()
const { webdav } = useClientService()
const capabilityStore = useCapabilityStore()
const resourcesStore = useResourcesStore()
const { currentFolder } = storeToRefs(resourcesStore)

const { actions: restoreActions } = useFileActionsRestore({
showSuccessMessage: false,
onRestoreComplete: async ({ space, resources }) => {
if (unref(currentFolder)?.id === resources[0].parentFolderId) {
// update local folder
const { children } = await webdav.listFiles(space, { path: unref(currentFolder).path })
resourcesStore.upsertResources(
children.filter(({ id }) => resources.some((s) => s.id === transformToTrashId(id)))
)
}
}
})

const transformToTrashId = (id: string) => {
// deleted files only have the "fileId" without the "storageId$driveId!" prefix
return id.includes('!') ? id.split('!')[1] : id
}

const undoDeleteHandler = async ({ space, resources, callback }: UndoActionOptions) => {
const resourcesToRestore = resources.map((res) => ({
...res,
id: transformToTrashId(res.id)
}))

try {
const restoreAction = unref(restoreActions)[0]
await restoreAction.handler({ space, resources: resourcesToRestore })
callback()
} catch (e) {
console.error(e)
showErrorMessage({
title: $gettext('Failed to restore files'),
errors: [e]
})
}
}

const shortcutString = computed(() => {
if (isMacOs()) {
return $gettext('⌘ + Z')
}
return $gettext('Ctrl + Z')
})

const actions = computed<Action<UndoActionOptions>[]>(() => {
return [
{
name: 'undoDelete',
icon: 'arrow-go-back',
shortcut: unref(shortcutString),
isVisible: ({ space }) => {
if (!capabilityStore.davTrashbin) {
return false
}
return isProjectSpaceResource(space) || isPersonalSpaceResource(space)
},
label: () => $gettext('Undo'),
handler: undoDeleteHandler
}
]
})

return {
actions
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import {
import { storeToRefs } from 'pinia'
import { useDeleteWorker } from '../../webWorkers'
import { useEventBus } from '../../eventBus'
import { useFileActionsUndoDelete } from '../files'
import { Key, Modifier, useKeyboardActions } from '../../keyboardActions'

export const useFileActionsDeleteResources = () => {
const configStore = useConfigStore()
const messageStore = useMessages()
const { showMessage, showErrorMessage, removeMessage } = useMessages()
const router = useRouter()
const language = useGettext()
const { getMatchingSpace } = useGetMatchingSpace()
Expand All @@ -34,10 +36,13 @@ export const useFileActionsDeleteResources = () => {
const { dispatchModal } = useModals()
const spacesStore = useSpacesStore()
const eventBus = useEventBus()
const { bindKeyAction, removeKeyAction } = useKeyboardActions()
const { startWorker } = useDeleteWorker({
concurrentRequests: configStore.options.concurrentRequests.resourceBatchActions
})

const { actions: undoActions } = useFileActionsUndoDelete()

const resourcesStore = useResourcesStore()
const { currentFolder } = storeToRefs(resourcesStore)

Expand All @@ -57,6 +62,58 @@ export const useFileActionsDeleteResources = () => {
return cloneStateObject<Resource[]>(unref(resourcesToDelete))
})

const showSuccessMessage = ({
space,
filesToDelete,
deletedFiles
}: {
space: SpaceResource
filesToDelete: Resource[]
deletedFiles: Resource[]
}) => {
const title =
deletedFiles.length === 1 && filesToDelete.length === 1
? $gettext('"%{item}" was moved to trash bin', { item: deletedFiles[0].name })
: $ngettext(
'%{itemCount} item was moved to trash bin',
'%{itemCount} items were moved to trash bin',
deletedFiles.length,
{ itemCount: deletedFiles.length.toString() },
true
)

const messageTimeout = 7 // in seconds
const undoAction = unref(undoActions)[0]
const undoAvailable = undoAction.isVisible({ space, resources: deletedFiles })

const message = showMessage({
title,
timeout: messageTimeout,
actions: [undoAction],
actionOptions: {
space,
resources: deletedFiles,
callback: () => {
removeMessage(message)
}
}
})

if (undoAvailable) {
const keyActionId = bindKeyAction({ primary: Key.Z, modifier: Modifier.Ctrl }, () => {
removeKeyAction(keyActionId)
return undoAction.handler({
space,
resources: deletedFiles,
callback: () => {
removeMessage(message)
}
})
})
setTimeout(() => removeKeyAction(keyActionId), messageTimeout * 1000)
}
}

const dialogTitle = computed(() => {
const currentResources = unref(resources)
const isFolder = currentResources[0].type === 'folder'
Expand Down Expand Up @@ -130,12 +187,12 @@ export const useFileActionsDeleteResources = () => {
true
)

messageStore.showMessage({ title })
showMessage({ title })
}

failed.forEach(({ resource }) => {
const title = $gettext('Failed to delete "%{item}"', { item: resource.name })
messageStore.showErrorMessage({ title, errors: [new Error()] })
showErrorMessage({ title, errors: [new Error()] })
})

// user hasn't navigated to another location meanwhile
Expand Down Expand Up @@ -181,18 +238,11 @@ export const useFileActionsDeleteResources = () => {
{ topic: 'fileListDelete', space: spaceForDeletion, resources: resourcesForDeletion },
async ({ successful, failed }) => {
if (successful.length) {
const title =
successful.length === 1 && resources.length === 1
? $gettext('"%{item}" was moved to trash bin', { item: successful[0].name })
: $ngettext(
'%{itemCount} item was moved to trash bin',
'%{itemCount} items were moved to trash bin',
successful.length,
{ itemCount: successful.length.toString() },
true
)

messageStore.showMessage({ title })
showSuccessMessage({
space: spaceForDeletion,
filesToDelete: resourcesForDeletion,
deletedFiles: successful
})
eventBus.publish('runtime.resource.deleted', successful)
}

Expand All @@ -207,7 +257,7 @@ export const useFileActionsDeleteResources = () => {
})
}

messageStore.showErrorMessage({ title, errors: [error] })
showErrorMessage({ title, errors: [error] })
})

// user hasn't navigated to another location meanwhile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export enum Key {
X = 'x',
A = 'a',
S = 's',
Z = 'z',
Plus = '+',
Minus = '-',
Space = ' ',
Expand Down
3 changes: 3 additions & 0 deletions packages/web-pkg/src/composables/piniaStores/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
import { v4 as uuidV4 } from 'uuid'
import { ref, unref } from 'vue'
import { HttpError } from '@opencloud-eu/web-client'
import { Action, ActionOptions } from '../actions'

type MessageError = Error | HttpError

Expand All @@ -13,6 +14,8 @@ export interface Message {
errorLogContent?: string
timeout?: number
status?: 'passive' | 'primary' | 'success' | 'warning' | 'danger'
actions?: Action[]
actionOptions?: ActionOptions
}

export const useMessages = defineStore('messages', () => {
Expand Down
Loading