diff --git a/packages/web-app-admin-settings/src/index.ts b/packages/web-app-admin-settings/src/index.ts index 5dd5e8e3b57..f8c77aa7846 100644 --- a/packages/web-app-admin-settings/src/index.ts +++ b/packages/web-app-admin-settings/src/index.ts @@ -3,14 +3,17 @@ import General from './views/General.vue' import Users from './views/Users.vue' import Groups from './views/Groups.vue' import Spaces from './views/Spaces.vue' -import { Ability } from '@ownclouders/web-client' +import { Ability, urlJoin } from '@ownclouders/web-client' import { + ApplicationInformation, + AppMenuItemExtension, AppNavigationItem, defineWebApplication, useAbility, useUserStore } from '@ownclouders/web-pkg' import { RouteRecordRaw } from 'vue-router' +import { computed } from 'vue' // just a dummy function to trick gettext tools function $gettext(msg: string) { @@ -152,29 +155,45 @@ export default defineWebApplication({ const { can } = useAbility() const userStore = useUserStore() + const appInfo: ApplicationInformation = { + name: $gettext('Admin Settings'), + id: appId, + icon: 'settings-4', + color: '#2b2b2b', + isFileEditor: false + } + + const menuItems = computed(() => { + const items: AppMenuItemExtension[] = [] + + const menuItemAvailable = + userStore.user && + (can('read-all', 'Setting') || + can('read-all', 'Account') || + can('read-all', 'Group') || + can('read-all', 'Drive')) + + if (menuItemAvailable) { + items.push({ + id: `app.${appInfo.id}.menuItem`, + type: 'appMenuItem', + label: () => appInfo.name, + color: appInfo.color, + icon: appInfo.icon, + priority: 40, + path: urlJoin(appInfo.id) + }) + } + + return items + }) + return { - appInfo: { - name: $gettext('Admin Settings'), - id: appId, - icon: 'settings-4', - color: '#2b2b2b', - isFileEditor: false, - applicationMenu: { - enabled: () => { - return ( - userStore.user && - (can('read-all', 'Setting') || - can('read-all', 'Account') || - can('read-all', 'Group') || - can('read-all', 'Drive')) - ) - }, - priority: 40 - } - }, + appInfo, routes, navItems, - translations + translations, + extensions: menuItems } } }) diff --git a/packages/web-app-files/src/extensions.ts b/packages/web-app-files/src/extensions.ts index 09a4e35bb99..7397ff68078 100644 --- a/packages/web-app-files/src/extensions.ts +++ b/packages/web-app-files/src/extensions.ts @@ -1,21 +1,25 @@ import { + ApplicationInformation, Extension, useCapabilityStore, useConfigStore, useFileActionsCopyQuickLink, useFileActionsShowShares, useRouter, - useSearch + useSearch, + useUserStore } from '@ownclouders/web-pkg' import { computed, unref } from 'vue' import { SDKSearch } from './search' import { useSideBarPanels } from './composables/extensions/useFileSideBars' import { useFolderViews } from './composables/extensions/useFolderViews' import { quickActionsExtensionPoint } from './extensionPoints' +import { urlJoin } from '@ownclouders/web-client' -export const extensions = () => { +export const extensions = (appInfo: ApplicationInformation) => { const capabilityStore = useCapabilityStore() const configStore = useConfigStore() + const userStore = useUserStore() const router = useRouter() const { search: searchFunction } = useSearch() @@ -45,6 +49,18 @@ export const extensions = () => { extensionPointIds: [quickActionsExtensionPoint.id], type: 'action', action: unref(quickLinkActions)[0] - } + }, + ...((userStore.user && [ + { + id: `app.${appInfo.id}.menuItem`, + type: 'appMenuItem', + label: () => appInfo.name, + color: appInfo.color, + icon: appInfo.icon, + priority: 10, + path: urlJoin(appInfo.id) + } + ]) || + []) ]) } diff --git a/packages/web-app-files/src/index.ts b/packages/web-app-files/src/index.ts index 2a7e8508f20..a8ebbec96dc 100644 --- a/packages/web-app-files/src/index.ts +++ b/packages/web-app-files/src/index.ts @@ -127,18 +127,8 @@ export const navItems = (context: ComponentCustomProperties): AppNavigationItem[ export default defineWebApplication({ setup() { - const userStore = useUserStore() - return { - appInfo: { - ...appInfo, - applicationMenu: { - enabled: () => { - return !!userStore.user - }, - priority: 10 - } - }, + appInfo, routes: buildRoutes({ App, Favorites, @@ -159,7 +149,7 @@ export default defineWebApplication({ }), navItems, translations, - extensions: extensions(), + extensions: extensions(appInfo), extensionPoints: extensionPoints() } } diff --git a/packages/web-app-ocm/src/extensions.ts b/packages/web-app-ocm/src/extensions.ts index 118ec6a9dd9..edc3fd3a4a9 100644 --- a/packages/web-app-ocm/src/extensions.ts +++ b/packages/web-app-ocm/src/extensions.ts @@ -1,19 +1,22 @@ import { + ApplicationInformation, FileActionOptions, useClientService, useConfigStore, useMessages, + useUserStore, useWindowOpen } from '@ownclouders/web-pkg' import { useGettext } from 'vue3-gettext' import { computed } from 'vue' import { Extension } from '@ownclouders/web-pkg' -import { OCM_PROVIDER_ID } from '@ownclouders/web-client' +import { OCM_PROVIDER_ID, urlJoin } from '@ownclouders/web-client' -export const extensions = () => { +export const extensions = (appInfo: ApplicationInformation) => { const { showErrorMessage } = useMessages() const clientService = useClientService() const configStore = useConfigStore() + const userStore = useUserStore() const { $gettext } = useGettext() const { openUrl } = useWindowOpen() @@ -69,6 +72,17 @@ export const extensions = () => { componentType: 'button', class: 'oc-files-actions-open-file-remote' } - } + }, + ...((userStore.user && [ + { + id: `app.${appInfo.id}.menuItem`, + type: 'appMenuItem', + label: () => appInfo.name, + color: appInfo.color, + icon: appInfo.icon, + path: urlJoin(appInfo.id) + } + ]) || + []) ]) } diff --git a/packages/web-app-ocm/src/index.ts b/packages/web-app-ocm/src/index.ts index 16629c083e3..4cf27a43fba 100644 --- a/packages/web-app-ocm/src/index.ts +++ b/packages/web-app-ocm/src/index.ts @@ -1,5 +1,5 @@ import App from './views/App.vue' -import { defineWebApplication, useRouter, useUserStore } from '@ownclouders/web-pkg' +import { ApplicationInformation, defineWebApplication, useRouter } from '@ownclouders/web-pkg' import translations from '../l10n/translations.json' import { extensions } from './extensions' import { RouteRecordRaw } from 'vue-router' @@ -27,19 +27,13 @@ export default defineWebApplication({ setup() { const { $gettext } = useGettext() const router = useRouter() - const userStore = useUserStore() - const appInfo = { + const appInfo: ApplicationInformation = { name: $gettext('ScienceMesh'), id: 'ocm', color: '#AE291D', icon: 'contacts-book', - isFileEditor: false, - applicationMenu: { - enabled: () => { - return !!userStore.user - } - } + isFileEditor: false } router.addRoute({ @@ -64,7 +58,7 @@ export default defineWebApplication({ appInfo, routes, navItems, - extensions: extensions(), + extensions: extensions(appInfo), translations } } diff --git a/packages/web-app-text-editor/src/index.ts b/packages/web-app-text-editor/src/index.ts index 659b6f8977e..84a6a11c0dc 100644 --- a/packages/web-app-text-editor/src/index.ts +++ b/packages/web-app-text-editor/src/index.ts @@ -2,16 +2,22 @@ import { useGettext } from 'vue3-gettext' import translations from '../l10n/translations.json' import TextEditor from './App.vue' import { + AppMenuItemExtension, AppWrapperRoute, ApplicationFileExtension, + ApplicationInformation, defineWebApplication, + useOpenEmptyEditor, useUserStore } from '@ownclouders/web-pkg' +import { computed } from 'vue' +import { urlJoin } from '@ownclouders/web-client' export default defineWebApplication({ setup({ applicationConfig }) { const { $gettext } = useGettext() const userStore = useUserStore() + const { openEmptyEditor } = useOpenEmptyEditor() const appId = 'text-editor' @@ -93,32 +99,47 @@ export default defineWebApplication({ } ] - return { - appInfo: { - name: $gettext('Text Editor'), - id: appId, - icon: 'file-text', - color: '#0D856F', - isFileEditor: true, - applicationMenu: { - enabled: () => { - return !!userStore.user - }, + const appInfo: ApplicationInformation = { + name: $gettext('Text Editor'), + id: appId, + icon: 'file-text', + color: '#0D856F', + isFileEditor: true, + defaultExtension: 'txt', + extensions: fileExtensions().map((extensionItem) => { + return { + extension: extensionItem.extension, + ...(Object.prototype.hasOwnProperty.call(extensionItem, 'newFileMenu') && { + newFileMenu: extensionItem.newFileMenu + }) + } + }) + } + + const menuItems = computed(() => { + const items: AppMenuItemExtension[] = [] + + if (userStore.user) { + items.push({ + id: `app.${appInfo.id}.menuItem`, + type: 'appMenuItem', + label: () => appInfo.name, + color: appInfo.color, + icon: appInfo.icon, priority: 20, - openAsEditor: true - }, - defaultExtension: 'txt', - extensions: fileExtensions().map((extensionItem) => { - return { - extension: extensionItem.extension, - ...(Object.prototype.hasOwnProperty.call(extensionItem, 'newFileMenu') && { - newFileMenu: extensionItem.newFileMenu - }) - } + path: urlJoin(appInfo.id), + handler: () => openEmptyEditor(appInfo.id, appInfo.defaultExtension) }) - }, + } + + return items + }) + + return { + appInfo, routes, - translations + translations, + extensions: menuItems } } }) diff --git a/packages/web-pkg/src/composables/actions/index.ts b/packages/web-pkg/src/composables/actions/index.ts index 4c0c25ad8c4..eaab8226851 100644 --- a/packages/web-pkg/src/composables/actions/index.ts +++ b/packages/web-pkg/src/composables/actions/index.ts @@ -4,5 +4,6 @@ export * from './spaces' export * from './types' export * from './useActionsShowDetails' +export * from './useOpenEmptyEditor' export * from './useOpenWithDefaultApp' export * from './useWindowOpen' diff --git a/packages/web-pkg/src/composables/actions/useOpenEmptyEditor.ts b/packages/web-pkg/src/composables/actions/useOpenEmptyEditor.ts new file mode 100644 index 00000000000..e0c73ec1823 --- /dev/null +++ b/packages/web-pkg/src/composables/actions/useOpenEmptyEditor.ts @@ -0,0 +1,54 @@ +import { useGettext } from 'vue3-gettext' +import { useGetMatchingSpace } from '../spaces' +import { useAppsStore, useResourcesStore, useSpacesStore } from '../piniaStores' +import { useClientService } from '../clientService' +import { EDITOR_MODE_EDIT, useFileActions } from './files' +import { storeToRefs } from 'pinia' +import { unref } from 'vue' +import { resolveFileNameDuplicate } from '../../helpers' +import { urlJoin } from '@ownclouders/web-client' + +// open an editor with an empty file within the current folder +export const useOpenEmptyEditor = () => { + const { getMatchingSpace } = useGetMatchingSpace() + const spacesStore = useSpacesStore() + const appsStore = useAppsStore() + const resourcesStore = useResourcesStore() + const clientService = useClientService() + const { $gettext } = useGettext() + const { openEditor } = useFileActions() + const { resources, currentFolder } = storeToRefs(resourcesStore) + + const openEmptyEditor = async (appId: string, extension: string) => { + let destinationSpace = unref(currentFolder) ? getMatchingSpace(unref(currentFolder)) : null + let destinationFiles = unref(resources) + let filePath = unref(currentFolder)?.path + + if (!destinationSpace || !unref(currentFolder).canCreate()) { + destinationSpace = spacesStore.personalSpace + destinationFiles = (await clientService.webdav.listFiles(destinationSpace)).children + filePath = '' + } + + let fileName = $gettext('New file') + `.${extension}` + + if (destinationFiles.some((f) => f.name === fileName)) { + fileName = resolveFileNameDuplicate(fileName, extension, destinationFiles) + } + + const emptyResource = await clientService.webdav.putFileContents(destinationSpace, { + path: urlJoin(filePath, fileName) + }) + + const space = getMatchingSpace(emptyResource) + const appFileExtension = appsStore.fileExtensions.find( + ({ app, extension: ext }) => app === appId && ext === extension + ) + + openEditor(appFileExtension, space, emptyResource, EDITOR_MODE_EDIT) + } + + return { + openEmptyEditor + } +} diff --git a/packages/web-pkg/tests/unit/composables/actions/files/useFileActions.spec.ts b/packages/web-pkg/tests/unit/composables/actions/files/useFileActions.spec.ts index 875f7e12f81..1b07105a90f 100644 --- a/packages/web-pkg/tests/unit/composables/actions/files/useFileActions.spec.ts +++ b/packages/web-pkg/tests/unit/composables/actions/files/useFileActions.spec.ts @@ -77,11 +77,6 @@ function getWrapper({ setup }: { setup: (instance: ReturnType true - }, defaultExtension: 'txt', icon: 'file-text', name: 'Text Editor', @@ -95,9 +90,6 @@ function getWrapper({ setup }: { setup: (instance: ReturnType true - }, defaultExtension: '', icon: 'check_box_outline_blank', name: 'External', diff --git a/packages/web-runtime/src/components/Topbar/TopBar.vue b/packages/web-runtime/src/components/Topbar/TopBar.vue index a1b1ff333f0..3791759eef7 100644 --- a/packages/web-runtime/src/components/Topbar/TopBar.vue +++ b/packages/web-runtime/src/components/Topbar/TopBar.vue @@ -50,28 +50,19 @@ import { ApplicationInformation, AppMenuItemExtension, CustomComponentTarget, - EDITOR_MODE_EDIT, queryItemAsString, - resolveFileNameDuplicate, - useAppsStore, useAuthStore, useCapabilityStore, - useClientService, useConfigStore, useEmbedMode, useExtensionRegistry, - useFileActions, - useGetMatchingSpace, - useResourcesStore, + useOpenEmptyEditor, useRouteQuery, useRouter, - useSpacesStore, useThemeStore } from '@ownclouders/web-pkg' import { isRuntimeRoute } from '../../router' import { appMenuExtensionPoint, topBarCenterExtensionPoint } from '../../extensionPoints' -import { useGettext } from 'vue3-gettext' -import { urlJoin } from '@ownclouders/web-client' export default { components: { @@ -96,6 +87,7 @@ export default { const configStore = useConfigStore() const { options: configOptions } = storeToRefs(configStore) const extensionRegistry = useExtensionRegistry() + const { openEmptyEditor } = useOpenEmptyEditor() const authStore = useAuthStore() const router = useRouter() @@ -140,48 +132,6 @@ export default { contentOnLeftPortal.value = newContent.hasContent } - /** - * FIXME: remove the following once we remove the deprecated applicationMenu prop (see below). - * The old implementation was bad since this shouldn't live in the runtime. - * Rather register such logic in the app itself via a handler. - */ - const { getMatchingSpace } = useGetMatchingSpace() - const spacesStore = useSpacesStore() - const appsStore = useAppsStore() - const resourcesStore = useResourcesStore() - const clientService = useClientService() - const { $gettext } = useGettext() - const { openEditor } = useFileActions() - const { resources, currentFolder } = storeToRefs(resourcesStore) - const onEditorApplicationClick = async (appId: string, defaultExtension: string) => { - let destinationSpace = unref(currentFolder) ? getMatchingSpace(unref(currentFolder)) : null - let destinationFiles = unref(resources) - let filePath = unref(currentFolder)?.path - - if (!destinationSpace || !unref(currentFolder).canCreate()) { - destinationSpace = spacesStore.personalSpace - destinationFiles = (await clientService.webdav.listFiles(destinationSpace)).children - filePath = '' - } - - let fileName = $gettext('New file') + `.${defaultExtension}` - - if (destinationFiles.some((f) => f.name === fileName)) { - fileName = resolveFileNameDuplicate(fileName, defaultExtension, destinationFiles) - } - - const emptyResource = await clientService.webdav.putFileContents(destinationSpace, { - path: urlJoin(filePath, fileName) - }) - - const space = getMatchingSpace(emptyResource) - const appFileExtension = appsStore.fileExtensions.find( - ({ app, extension }) => app === appId && extension === defaultExtension - ) - - openEditor(appFileExtension, space, emptyResource, EDITOR_MODE_EDIT) - } - onMounted(() => { // FIXME: backwards compatibility for the deprecated applicationMenu prop const navExtensions = props.applicationsList @@ -197,9 +147,7 @@ export default { priority: app.applicationMenu?.priority || 50, ...((app as any).url && { url: (app as any).url, target: '_blank' }), ...(app.applicationMenu?.openAsEditor && { - handler: () => { - onEditorApplicationClick(app.id, app.defaultExtension) - } + handler: () => openEmptyEditor(app.id, app.defaultExtension) }) })) as AppMenuItemExtension[] diff --git a/tests/e2e/support/objects/runtime/application.ts b/tests/e2e/support/objects/runtime/application.ts index baac5cc7f7c..aaf6b8ed4a9 100644 --- a/tests/e2e/support/objects/runtime/application.ts +++ b/tests/e2e/support/objects/runtime/application.ts @@ -23,7 +23,7 @@ export class Application { async open({ name }: { name: string }): Promise { await this.#page.waitForTimeout(1000) await this.#page.locator(appSwitcherButton).click() - await this.#page.locator(util.format(appSelector, name)).click() + await this.#page.locator(util.format(appSelector, `app.${name}.menuItem`)).click() } async getNotificationMessages(): Promise {