diff --git a/packages/web-app-admin-settings/src/index.ts b/packages/web-app-admin-settings/src/index.ts index 849d676f37..7adb8e62f1 100644 --- a/packages/web-app-admin-settings/src/index.ts +++ b/packages/web-app-admin-settings/src/index.ts @@ -3,42 +3,38 @@ 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, urlJoin } from '@opencloud-eu/web-client' +import { urlJoin } from '@opencloud-eu/web-client' import { ApplicationInformation, AppMenuItemExtension, - AppNavigationItem, + ClassicApplicationScript, defineWebApplication, useAbility, useUserStore } from '@opencloud-eu/web-pkg' -import { RouteRecordRaw } from 'vue-router' import { computed } from 'vue' - -// just a dummy function to trick gettext tools -function $gettext(msg: string) { - return msg -} +import { useGettext } from 'vue3-gettext' const appId = 'admin-settings' -export const routes = ({ $ability }: { $ability: Ability }): RouteRecordRaw[] => [ +export const routes: ClassicApplicationScript['routes'] = ({ $ability, $gettext }) => [ { path: '/', - redirect: () => { + component: General, + beforeEnter: (to, from, next) => { if ($ability.can('read-all', 'Setting')) { - return { name: 'admin-settings-general' } + next({ name: 'admin-settings-general' }) } if ($ability.can('read-all', 'Account')) { - return { name: 'admin-settings-users' } + next({ name: 'admin-settings-users' }) } if ($ability.can('read-all', 'Group')) { - return { name: 'admin-settings-groups' } + next({ name: 'admin-settings-groups' }) } if ($ability.can('read-all', 'Drive')) { - return { name: 'admin-settings-spaces' } + next({ name: 'admin-settings-spaces' }) } - throw Error('Insufficient permissions') + next({ path: '/' }) } }, { @@ -103,7 +99,7 @@ export const routes = ({ $ability }: { $ability: Ability }): RouteRecordRaw[] => } ] -export const navItems = ({ $ability }: { $ability: Ability }): AppNavigationItem[] => [ +export const navItems: ClassicApplicationScript['navItems'] = ({ $ability, $gettext }) => [ { name: $gettext('General'), icon: 'settings-4', @@ -154,6 +150,7 @@ export default defineWebApplication({ setup() { const { can } = useAbility() const userStore = useUserStore() + const { $gettext } = useGettext() const appInfo: ApplicationInformation = { name: $gettext('Admin Settings'), diff --git a/packages/web-app-admin-settings/tests/unit/index.spec.ts b/packages/web-app-admin-settings/tests/unit/index.spec.ts index ee4f0f1c07..47d40a20d7 100644 --- a/packages/web-app-admin-settings/tests/unit/index.spec.ts +++ b/packages/web-app-admin-settings/tests/unit/index.spec.ts @@ -1,15 +1,21 @@ import { navItems, routes } from '../../src/index' import { Ability } from '@opencloud-eu/web-client' +import { AppNavigationItem, GlobalProperties } from '@opencloud-eu/web-pkg' import { mock } from 'vitest-mock-extended' +import { RouteRecordRaw } from 'vue-router' const getAbilityMock = (hasPermission: boolean) => mock({ can: () => hasPermission }) +const callableNavItems = navItems as (args: GlobalProperties) => AppNavigationItem[] +const callableRoutes = routes as (args: GlobalProperties) => RouteRecordRaw[] +const getPropMock = (ability: Ability) => + mock({ $ability: ability, $gettext: (s: string) => s }) describe('admin settings index', () => { describe('navItems', () => { describe('general', () => { it.each([true, false])('should be enabled according to the permissions', (enabled) => { expect( - navItems({ $ability: getAbilityMock(enabled) }) + callableNavItems(getPropMock(getAbilityMock(enabled))) .find((n) => n.name === 'General') .isVisible() ).toBe(enabled) @@ -18,7 +24,7 @@ describe('admin settings index', () => { describe('user management', () => { it.each([true, false])('should be enabled according to the permissions', (enabled) => { expect( - navItems({ $ability: getAbilityMock(enabled) }) + callableNavItems(getPropMock(getAbilityMock(enabled))) .find((n) => n.name === 'Users') .isVisible() ).toBe(enabled) @@ -27,7 +33,7 @@ describe('admin settings index', () => { describe('group management', () => { it.each([true, false])('should be enabled according to the permissions', (enabled) => { expect( - navItems({ $ability: getAbilityMock(enabled) }) + callableNavItems(getPropMock(getAbilityMock(enabled))) .find((n) => n.name === 'Groups') .isVisible() ).toBe(enabled) @@ -36,7 +42,7 @@ describe('admin settings index', () => { describe('space management', () => { it.each([true, false])('should be enabled according to the permissions', (enabled) => { expect( - navItems({ $ability: getAbilityMock(enabled) }) + callableNavItems(getPropMock(getAbilityMock(enabled))) .find((n) => n.name === 'Spaces') .isVisible() ).toBe(enabled) @@ -48,23 +54,29 @@ describe('admin settings index', () => { it('should redirect to general if permission given', () => { const ability = mock() ability.can.mockReturnValueOnce(true) - const route = routes({ $ability: ability }).find((n) => n.path === '/') - expect((route.redirect as any)().name).toEqual('admin-settings-general') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/') + const nextMock = vi.fn() + ;(route.beforeEnter as any)({}, {}, nextMock) + expect(nextMock).toHaveBeenCalledWith({ name: 'admin-settings-general' }) }) it('should redirect to user management if permission given', () => { const ability = mock() ability.can.mockReturnValueOnce(false) ability.can.mockReturnValueOnce(true) - const route = routes({ $ability: ability }).find((n) => n.path === '/') - expect((route.redirect as any)().name).toEqual('admin-settings-users') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/') + const nextMock = vi.fn() + ;(route.beforeEnter as any)({}, {}, nextMock) + expect(nextMock).toHaveBeenCalledWith({ name: 'admin-settings-users' }) }) it('should redirect to group management if permission given', () => { const ability = mock() ability.can.mockReturnValueOnce(false) ability.can.mockReturnValueOnce(false) ability.can.mockReturnValueOnce(true) - const route = routes({ $ability: ability }).find((n) => n.path === '/') - expect((route.redirect as any)().name).toEqual('admin-settings-groups') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/') + const nextMock = vi.fn() + ;(route.beforeEnter as any)({}, {}, nextMock) + expect(nextMock).toHaveBeenCalledWith({ name: 'admin-settings-groups' }) }) it('should redirect to space management if permission given', () => { const ability = mock() @@ -72,13 +84,18 @@ describe('admin settings index', () => { ability.can.mockReturnValueOnce(false) ability.can.mockReturnValueOnce(false) ability.can.mockReturnValueOnce(true) - const route = routes({ $ability: ability }).find((n) => n.path === '/') - expect((route.redirect as any)().name).toEqual('admin-settings-spaces') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/') + const nextMock = vi.fn() + ;(route.beforeEnter as any)({}, {}, nextMock) + expect(nextMock).toHaveBeenCalledWith({ name: 'admin-settings-spaces' }) }) - it('should throw an error if permissions are insufficient', () => { + it('redirects to / if permissions are insufficient', () => { const ability = mock() ability.can.mockReturnValue(false) - expect(routes({ $ability: ability }).find((n) => n.path === '/').redirect).toThrow() + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/') + const nextMock = vi.fn() + ;(route.beforeEnter as any)({}, {}, nextMock) + expect(nextMock).toHaveBeenCalledWith({ path: '/' }) }) }) it.each([ @@ -86,7 +103,7 @@ describe('admin settings index', () => { { can: false, redirect: { path: '/' } } ])('redirects "/general" with sufficient permissions', ({ can, redirect }) => { const ability = mock({ can: vi.fn(() => can) }) - const route = routes({ $ability: ability }).find((n) => n.path === '/general') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/general') const nextMock = vi.fn() ;(route.beforeEnter as any)({}, {}, nextMock) const args = [...(redirect ? [redirect] : [])] @@ -97,7 +114,7 @@ describe('admin settings index', () => { { can: false, redirect: { path: '/' } } ])('redirects "/users" with sufficient permissions', ({ can, redirect }) => { const ability = mock({ can: vi.fn(() => can) }) - const route = routes({ $ability: ability }).find((n) => n.path === '/users') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/users') const nextMock = vi.fn() ;(route.beforeEnter as any)({}, {}, nextMock) const args = [...(redirect ? [redirect] : [])] @@ -108,7 +125,7 @@ describe('admin settings index', () => { { can: false, redirect: { path: '/' } } ])('redirects "/groups" with sufficient permissions', ({ can, redirect }) => { const ability = mock({ can: vi.fn(() => can) }) - const route = routes({ $ability: ability }).find((n) => n.path === '/groups') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/groups') const nextMock = vi.fn() ;(route.beforeEnter as any)({}, {}, nextMock) const args = [...(redirect ? [redirect] : [])] @@ -119,7 +136,7 @@ describe('admin settings index', () => { { can: false, redirect: { path: '/' } } ])('redirects "/spaces" with sufficient permissions', ({ can, redirect }) => { const ability = mock({ can: vi.fn(() => can) }) - const route = routes({ $ability: ability }).find((n) => n.path === '/spaces') + const route = callableRoutes(getPropMock(ability)).find((n) => n.path === '/spaces') const nextMock = vi.fn() ;(route.beforeEnter as any)({}, {}, nextMock) const args = [...(redirect ? [redirect] : [])] diff --git a/packages/web-app-files/src/index.ts b/packages/web-app-files/src/index.ts index a4e368d3d5..d69bd81180 100644 --- a/packages/web-app-files/src/index.ts +++ b/packages/web-app-files/src/index.ts @@ -10,6 +10,7 @@ import TrashOverview from './views/trash/Overview.vue' import translations from '../l10n/translations.json' import { ApplicationInformation, + ClassicApplicationScript, defineWebApplication, useCapabilityStore, useEmbedMode, @@ -18,28 +19,22 @@ import { } from '@opencloud-eu/web-pkg' import { extensions } from './extensions' import { buildRoutes } from '@opencloud-eu/web-pkg' -import { AppNavigationItem } from '@opencloud-eu/web-pkg' // dirty: importing view from other extension within project import SearchResults from '../../web-app-search/src/views/List.vue' import { isPersonalSpaceResource, isShareSpaceResource } from '@opencloud-eu/web-client' -import { ComponentCustomProperties, unref } from 'vue' +import { unref } from 'vue' import { extensionPoints } from './extensionPoints' - -// just a dummy function to trick gettext tools -function $gettext(msg: string) { - return msg -} +import { useGettext } from 'vue3-gettext' const appInfo: ApplicationInformation = { - name: $gettext('Files'), id: 'files', icon: 'resource-type-folder', color: 'var(--oc-role-secondary)', extensions: [] } -export const navItems = (context: ComponentCustomProperties): AppNavigationItem[] => { +export const navItems: ClassicApplicationScript['navItems'] = ({ $ability, $gettext }) => { const spacesStores = useSpacesStore() const userStore = useUserStore() const capabilityStore = useCapabilityStore() @@ -75,7 +70,7 @@ export const navItems = (context: ComponentCustomProperties): AppNavigationItem[ path: `/${appInfo.id}/favorites` }, isVisible() { - return capabilityStore.filesFavorites && context.$ability.can('read', 'Favorite') + return capabilityStore.filesFavorites && $ability.can('read', 'Favorite') }, priority: 20 }, @@ -133,6 +128,9 @@ export const navItems = (context: ComponentCustomProperties): AppNavigationItem[ export default defineWebApplication({ setup() { + const { $gettext } = useGettext() + appInfo.name = $gettext('Files') + return { appInfo, routes: buildRoutes({ diff --git a/packages/web-app-files/tests/unit/index.spec.ts b/packages/web-app-files/tests/unit/index.spec.ts index 9d8389547a..5201cf286b 100644 --- a/packages/web-app-files/tests/unit/index.spec.ts +++ b/packages/web-app-files/tests/unit/index.spec.ts @@ -1,9 +1,11 @@ import { createPinia, setActivePinia } from 'pinia' import { navItems } from '../../src/index' -import { useSpacesStore } from '@opencloud-eu/web-pkg' +import { AppNavigationItem, GlobalProperties, useSpacesStore } from '@opencloud-eu/web-pkg' import { SpaceResource } from '@opencloud-eu/web-client' import { mock } from 'vitest-mock-extended' +const callableNavItems = navItems as (args: GlobalProperties) => AppNavigationItem[] + describe('Web app files', () => { beforeEach(() => { setActivePinia(createPinia()) @@ -18,7 +20,7 @@ describe('Web app files', () => { spacesStore.spaces = [ mock({ id: '1', driveType: 'personal', isOwner: () => true }) ] - const items = navItems(undefined) + const items = callableNavItems(mock()) expect(items[0].isVisible()).toBeTruthy() }) it('should be disabled if user has no a personal space', () => { @@ -26,7 +28,7 @@ describe('Web app files', () => { spacesStore.spaces = [ mock({ id: '1', driveType: 'project', isOwner: () => false }) ] - const items = navItems(undefined) + const items = callableNavItems(mock()) expect(items[0].isVisible()).toBeFalsy() }) }) diff --git a/packages/web-app-search/src/index.ts b/packages/web-app-search/src/index.ts index b644430d84..c77d11874c 100644 --- a/packages/web-app-search/src/index.ts +++ b/packages/web-app-search/src/index.ts @@ -1,27 +1,21 @@ import App from './App.vue' import List from './views/List.vue' - -// @ts-ignore import translations from '../l10n/translations.json' -import { ApplicationInformation, defineWebApplication } from '@opencloud-eu/web-pkg' +import { defineWebApplication } from '@opencloud-eu/web-pkg' import { extensions } from './extensions' import { extensionPoints } from './extensionPoints' - -// just a dummy function to trick gettext tools -const $gettext = (msg: string) => { - return msg -} - -const appInfo: ApplicationInformation = { - name: $gettext('Search'), - id: 'search', - icon: 'folder' -} +import { useGettext } from 'vue3-gettext' export default defineWebApplication({ setup() { + const { $gettext } = useGettext() + return { - appInfo, + appInfo: { + name: $gettext('Search'), + id: 'search', + icon: 'folder' + }, routes: [ { name: 'search', diff --git a/packages/web-pkg/src/apps/types.ts b/packages/web-pkg/src/apps/types.ts index 3b60948a8e..6cd6521b51 100644 --- a/packages/web-pkg/src/apps/types.ts +++ b/packages/web-pkg/src/apps/types.ts @@ -1,12 +1,41 @@ -import { App, ComponentCustomProperties, Ref } from 'vue' -import { RouteLocationRaw, Router, RouteRecordRaw } from 'vue-router' -import { Extension, ExtensionPoint } from '../composables/piniaStores' -import { IconFillType } from '../helpers' -import { Resource, SpaceResource } from '@opencloud-eu/web-client' -import { Translations } from 'vue3-gettext' +import type { App, ComponentCustomProperties, Ref } from 'vue' +import type { RouteLocationRaw, Router, RouteRecordRaw } from 'vue-router' +import type { Extension, ExtensionPoint } from '../composables/piniaStores' +import type { IconFillType } from '../helpers' +import type { Ability, Resource, SpaceResource } from '@opencloud-eu/web-client' +import type { Language, Translations } from 'vue3-gettext' +import type { Pinia } from 'pinia' +import type { + AppProviderService, + ArchiverService, + ClientService, + LoadingService, + PasswordPolicyService, + PreviewService, + UppyService +} from '../services' +import type { AuthServiceInterface } from '../composables' +import type { Wormhole } from 'portal-vue/types/types.js' + +export interface GlobalProperties extends ComponentCustomProperties, Language { + $ability: Ability + $appProviderService: AppProviderService + $archiverService: ArchiverService + $authService: AuthServiceInterface + $can: Ability['can'] + $clientService: ClientService + $language: Language + $loadingService: LoadingService + $pinia: Pinia + $previewService: PreviewService + $router: Router + $uppyService: UppyService + $wormhole: Wormhole + passwordPolicyService: PasswordPolicyService +} export interface AppReadyHookArgs { - globalProperties: ComponentCustomProperties & Record + globalProperties: GlobalProperties router: Router instance?: App portal?: any @@ -73,8 +102,8 @@ export interface ApplicationTranslations { /** ClassicApplicationScript reflects classic application script structure */ export interface ClassicApplicationScript { appInfo?: ApplicationInformation - routes?: ((args: ComponentCustomProperties) => RouteRecordRaw[]) | RouteRecordRaw[] - navItems?: ((args: ComponentCustomProperties) => AppNavigationItem[]) | AppNavigationItem[] + routes?: ((args: GlobalProperties) => RouteRecordRaw[]) | RouteRecordRaw[] + navItems?: ((args: GlobalProperties) => AppNavigationItem[]) | AppNavigationItem[] translations?: ApplicationTranslations extensions?: Ref extensionPoints?: Ref[]> diff --git a/packages/web-runtime/src/container/application/classic.ts b/packages/web-runtime/src/container/application/classic.ts index 4bf64fe82e..cfea8df414 100644 --- a/packages/web-runtime/src/container/application/classic.ts +++ b/packages/web-runtime/src/container/application/classic.ts @@ -4,7 +4,7 @@ import { App } from 'vue' import { isFunction, isObject } from 'lodash-es' import { NextApplication } from './next' import { Router } from 'vue-router' -import { RuntimeError, useAppsStore } from '@opencloud-eu/web-pkg' +import { GlobalProperties, RuntimeError, useAppsStore } from '@opencloud-eu/web-pkg' import { AppConfigObject, AppReadyHookArgs, ClassicApplicationScript } from '@opencloud-eu/web-pkg' import { useExtensionRegistry } from '@opencloud-eu/web-pkg' @@ -25,8 +25,10 @@ class ClassicApplication extends NextApplication { initialize(): Promise { const { routes, navItems } = this.applicationScript const { globalProperties } = this.app.config - const _routes = typeof routes === 'function' ? routes(globalProperties) : routes - const _navItems = typeof navItems === 'function' ? navItems(globalProperties) : navItems + const _routes = + typeof routes === 'function' ? routes(globalProperties as GlobalProperties) : routes + const _navItems = + typeof navItems === 'function' ? navItems(globalProperties as GlobalProperties) : navItems routes && this.runtimeApi.announceRoutes(_routes) navItems && this.runtimeApi.announceNavigationItems(_navItems) @@ -57,7 +59,7 @@ class ClassicApplication extends NextApplication { }), instance, router: this.runtimeApi.requestRouter(), - globalProperties: this.app.config.globalProperties + globalProperties: this.app.config.globalProperties as GlobalProperties }) } }