Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: register file quick actions as extensions #10102

Merged
merged 2 commits into from
Dec 1, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 8 additions & 0 deletions changelog/unreleased/enhancement-quick-actions-via-extension
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Enhancement: Registering quick actions as extension

Quick actions can now registered as extension via our extension registry. They need to be of type `action` and have the `files.quick-action` scope.

The old way of registering quick actions via the `quickaction` property of an app is now officially deprecated.

https://github.com/owncloud/web/pull/10102
https://github.com/owncloud/web/issues/7338
58 changes: 22 additions & 36 deletions packages/web-app-files/src/components/FilesList/QuickActions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,64 +2,50 @@
<div v-if="!isEmbedModeEnabled" class="oc-flex">
<oc-button
v-for="action in filteredActions"
:key="action.label($gettext)"
v-oc-tooltip="action.label($gettext)"
:aria-label="action.label($gettext)"
:key="action.label()"
v-oc-tooltip="action.label()"
:aria-label="action.label()"
appearance="raw"
class="oc-mr-xs quick-action-button oc-p-xs"
:class="`files-quick-action-${action.id}`"
@click="
action.handler({ ability, clientService, passwordPolicyService, item, language, store })
"
:class="`files-quick-action-${action.name}`"
@click="action.handler({ space, resources: [item] })"
>
<oc-icon :name="action.icon" fill-type="line" />
</oc-button>
</div>
</template>

<script lang="ts">
import pickBy from 'lodash-es/pickBy'
import { computed, defineComponent } from 'vue'
import {
useAbility,
useClientService,
useEmbedMode,
usePasswordPolicyService,
useStore
} from '@ownclouders/web-pkg'
import { useGettext } from 'vue3-gettext'
import { computed, defineComponent, PropType } from 'vue'
import { ActionExtension, useEmbedMode, useExtensionRegistry } from '@ownclouders/web-pkg'
import { Resource, SpaceResource } from '@ownclouders/web-client'
import { unref } from 'vue'

export default defineComponent({
name: 'QuickActions',

props: {
actions: {
type: Object,
required: true
},
item: {
type: Object,
type: Object as PropType<Resource>,
required: true
},
space: {
type: Object as PropType<SpaceResource>,
default: undefined
}
},
setup(props) {
const store = useStore()
const ability = useAbility()
const clientService = useClientService()
const passwordPolicyService = usePasswordPolicyService()
const language = useGettext()
const extensionRegistry = useExtensionRegistry()
const { isEnabled: isEmbedModeEnabled } = useEmbedMode()

const filteredActions = computed(() =>
pickBy(props.actions, (action) => action.displayed(props.item, store, ability) === true)
)
const filteredActions = computed(() => {
return unref(extensionRegistry)
.requestExtensions<ActionExtension>('action')
.filter(({ scopes }) => scopes.includes('files.quick-action'))
.map((e) => e.action)
.filter(({ isEnabled }) => isEnabled({ space: props.space, resources: [props.item] }))
})

return {
ability,
clientService,
passwordPolicyService,
store,
language,
filteredActions,
isEmbedModeEnabled
}
Expand Down
21 changes: 19 additions & 2 deletions packages/web-app-files/src/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,40 @@ import {
Extension,
useStore,
useRouter,
useSearch
useSearch,
useFileActionsShowShares,
useFileActionsCreateQuickLink
} from '@ownclouders/web-pkg'
import { computed } from 'vue'
import { computed, unref } from 'vue'
import { SDKSearch } from './search'

export const extensions = ({ applicationConfig }: ApplicationSetupOptions) => {
const store = useStore()
const router = useRouter()
const { search: searchFunction } = useSearch()

const { actions: showSharesActions } = useFileActionsShowShares()
const { actions: quickLinkActions } = useFileActionsCreateQuickLink()

return computed(
() =>
[
{
id: 'com.github.owncloud.web.files.search',
type: 'search',
searchProvider: new SDKSearch(store, router, searchFunction)
},
{
id: 'com.github.owncloud.web.files.quick-action.collaborator',
scopes: ['files', 'files.quick-action'],
type: 'action',
action: unref(showSharesActions)[0]
},
{
id: 'com.github.owncloud.web.files.quick-action.quicklink',
scopes: ['files', 'files.quick-action'],
type: 'action',
action: unref(quickLinkActions)[0]
}
] satisfies Extension[]
)
Expand Down
3 changes: 1 addition & 2 deletions packages/web-app-files/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import SpaceDriveResolver from './views/spaces/DriveResolver.vue'
import SpaceProjects from './views/spaces/Projects.vue'
import TrashOverview from './views/trash/Overview.vue'
import translations from '../l10n/translations.json'
import { defineWebApplication, quickActions } from '@ownclouders/web-pkg'
import { defineWebApplication } from '@ownclouders/web-pkg'
import store from './store'
import { extensions } from './extensions'
import fileSideBars from './fileSideBars'
Expand Down Expand Up @@ -151,7 +151,6 @@ export default defineWebApplication({
}
}),
navItems,
quickActions,
translations,
extensions: extensions(args)
}
Expand Down
6 changes: 1 addition & 5 deletions packages/web-app-files/src/views/Favorites.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,7 @@
@sort="handleSort"
>
<template #quickActions="props">
<quick-actions
class="oc-visible@s"
:item="props.resource"
:actions="app.quickActions"
/>
<quick-actions class="oc-visible@s" :item="props.resource" />
</template>
<template #contextMenu="{ resource }">
<context-actions
Expand Down
2 changes: 1 addition & 1 deletion packages/web-app-files/src/views/spaces/GenericSpace.vue
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@
<quick-actions
:class="resource.preview"
class="oc-visible@s"
:space="space"
:item="resource"
:actions="app.quickActions"
/>
</template>
<template #contextMenu="{ resource }">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,27 +1,32 @@
import { useEmbedMode } from '@ownclouders/web-pkg'
import { ActionExtension, useEmbedMode } from '@ownclouders/web-pkg'
import QuickActions from '../../../../src/components/FilesList/QuickActions.vue'
import { defaultComponentMocks, defaultPlugins, shallowMount } from 'web-test-helpers'
import { useExtensionRegistry } from '@ownclouders/web-pkg'
import { mock } from 'jest-mock-extended'
import { ref } from 'vue'
import { useExtensionRegistryMock } from 'web-test-helpers/src/mocks/useExtensionRegistryMock'

jest.mock('@ownclouders/web-pkg', () => ({
...jest.requireActual('@ownclouders/web-pkg'),
useEmbedMode: jest.fn()
useEmbedMode: jest.fn(),
useExtensionRegistry: jest.fn()
}))

const collaboratorAction = {
displayed: jest.fn(() => true),
isEnabled: jest.fn(() => true),
handler: jest.fn(),
icon: 'group-add',
id: 'collaborators',
name: 'show-shares',
label: () => 'Add people'
}

const quicklinkAction = {
displayed: jest.fn(() => false),
isEnabled: jest.fn(() => false),
handler: jest.fn(),
icon: 'link-add',
id: 'quicklink',
name: 'create-quicklink',
label: () => 'Create and copy quicklink'
}

Expand All @@ -45,14 +50,14 @@ describe('QuickActions', () => {
const iconEl = actionButton.find('oc-icon-stub')

expect(actionButton.exists()).toBeTruthy()
expect(actionButton.attributes().class).toContain('files-quick-action-collaborators')
expect(actionButton.attributes().class).toContain('files-quick-action-show-shares')
expect(iconEl.exists()).toBeTruthy()
expect(iconEl.attributes().name).toBe('group-add')
expect(actionButton.attributes('aria-label')).toBe('Add people')
})

it('should not display action buttons where "displayed" is set to false', () => {
const linkActionButton = wrapper.find('.files-quick-action-public-link')
const linkActionButton = wrapper.find('.files-quick-action-create-quicklink')

expect(linkActionButton.exists()).toBeFalsy()
})
Expand All @@ -66,9 +71,6 @@ describe('QuickActions', () => {
const actionButton = wrapper.find('.oc-button')
await actionButton.trigger('click')
expect(handlerAction).toHaveBeenCalledTimes(1)
Object.keys(testItem).forEach((key) => {
expect(handlerAction.mock.calls[0][0].item[key]).toBe(testItem[key])
})
})
})

Expand All @@ -83,13 +85,19 @@ function getWrapper({ embedModeEnabled = false } = {}) {
.mocked(useEmbedMode)
.mockReturnValue(mock<ReturnType<typeof useEmbedMode>>({ isEnabled: ref(embedModeEnabled) }))

jest.mocked(useExtensionRegistry).mockImplementation(() =>
useExtensionRegistryMock({
requestExtensions: () =>
[
mock<ActionExtension>({ scopes: ['files.quick-action'], action: collaboratorAction }),
mock<ActionExtension>({ scopes: ['files.quick-action'], action: quicklinkAction })
] as any
})
)

return {
wrapper: shallowMount(QuickActions, {
props: {
actions: {
collaborators: collaboratorAction,
publicLink: quicklinkAction
},
item: testItem
},
global: {
Expand Down
7 changes: 3 additions & 4 deletions packages/web-pkg/src/apps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ export interface AppNavigationItem {
/**
* ApplicationQuickAction describes an application action that is used in the runtime.
*
* @deprecated In the future quick actions should be registered just like any other extension. Fine
* to use this interface for now, but it will be changed in the near future.
* @deprecated Quick actions should be registered as extension via the `files.quick-action` scope.
*/
export interface ApplicationQuickAction {
id?: string
Expand All @@ -43,8 +42,7 @@ export interface ApplicationQuickAction {
/**
* ApplicationQuickActions describes a map of application actions that are used in the runtime
*
* @deprecated In the future quick actions should be registered just like any other extension. Fine
* to use this interface for now, but it will be changed in the near future.
* @deprecated Quick actions should be registered as extension via the `files.quick-action` scope.
*/
export interface ApplicationQuickActions {
[key: string]: ApplicationQuickAction
Expand Down Expand Up @@ -85,6 +83,7 @@ export interface ClassicApplicationScript {
store?: Module<unknown, unknown>
routes?: ((...args) => RouteRecordRaw[]) | RouteRecordRaw[]
navItems?: ((...args) => AppNavigationItem[]) | AppNavigationItem[]
/** @deprecated Quick actions should be registered as extension via the `files.quick-action` scope. */
quickActions?: ApplicationQuickActions
translations?: ApplicationTranslations
extensions?: Ref<Extension[]>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import quickActions, { canShare } from '../../../quickActions'
import { copyQuicklink } from '../../../helpers/share'
import { ShareStatus } from '@ownclouders/web-client/src/helpers/share'

Expand All @@ -12,6 +11,7 @@ import { useGettext } from 'vue3-gettext'
import { Store } from 'vuex'
import { FileAction, FileActionOptions } from '../types'
import { usePasswordPolicyService } from '../../passwordPolicyService'
import { useCanShare } from '../../shares'

export const useFileActionsCreateQuickLink = ({
store
Expand All @@ -25,6 +25,7 @@ export const useFileActionsCreateQuickLink = ({
const ability = useAbility()
const clientService = useClientService()
const passwordPolicyService = usePasswordPolicyService()
const { canShare } = useCanShare()

const handler = async ({ space, resources }: FileActionOptions) => {
const [resource] = resources
Expand All @@ -43,8 +44,7 @@ export const useFileActionsCreateQuickLink = ({
const actions = computed((): FileAction[] => [
{
name: 'create-quicklink',
icon: quickActions.quicklink.icon,
iconFillType: quickActions.quicklink.iconFillType,
icon: 'link',
label: () => $gettext('Copy link'),
handler,
isEnabled: ({ resources }) => {
Expand All @@ -56,7 +56,7 @@ export const useFileActionsCreateQuickLink = ({
return false
}
}
return canShare(resources[0], store, ability)
return canShare(resources[0])
},
componentType: 'button',
class: 'oc-files-actions-create-quicklink-trigger'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import quickActions, { canShare } from '../../../quickActions'
import { isLocationSharesActive, isLocationTrashActive } from '../../../router'
import { ShareStatus } from '@ownclouders/web-client/src/helpers/share'
import { eventBus } from '../../../services'
Expand All @@ -10,14 +9,14 @@ import { useRouter } from '../../router'
import { useStore } from '../../store'
import { Store } from 'vuex'
import { FileAction, FileActionOptions } from '../types'
import { useAbility } from '../../ability'
import { useCanShare } from '../../shares'

export const useFileActionsShowShares = ({ store }: { store?: Store<any> } = {}) => {
store = store || useStore()
const router = useRouter()
const ability = useAbility()
const { $gettext } = useGettext()
const isFilesAppActive = useIsFilesAppActive()
const { canShare } = useCanShare()

const handler = ({ resources }: FileActionOptions) => {
store.commit('Files/SET_FILE_SELECTION', resources)
Expand All @@ -27,8 +26,7 @@ export const useFileActionsShowShares = ({ store }: { store?: Store<any> } = {})
const actions = computed((): FileAction[] => [
{
name: 'show-shares',
icon: quickActions.collaborators.icon,
iconFillType: quickActions.collaborators.iconFillType,
icon: 'user-add',
label: () => $gettext('Share'),
handler,
isEnabled: ({ resources }) => {
Expand All @@ -48,7 +46,7 @@ export const useFileActionsShowShares = ({ store }: { store?: Store<any> } = {})
return false
}
}
return canShare(resources[0], store, ability)
return canShare(resources[0])
},
componentType: 'button',
class: 'oc-files-actions-show-shares-trigger'
Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export * from './scrollTo'
export * from './search'
export * from './selection'
export * from './service'
export * from './shares'
export * from './sideBar'
export * from './sort'
export * from './spaces'
Expand Down
1 change: 1 addition & 0 deletions packages/web-pkg/src/composables/shares/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './useCanShare'