Skip to content

Commit

Permalink
refactor: register nav items via extension registry
Browse files Browse the repository at this point in the history
Removes the nav items from the vuex navigation store module. It now uses the extension registry with the already existing `SidebarNavExtension` type for registering and retrieving  nav items.
  • Loading branch information
JammingBen committed Jan 12, 2024
1 parent 015dd7b commit aacedb8
Show file tree
Hide file tree
Showing 9 changed files with 68 additions and 85 deletions.
9 changes: 6 additions & 3 deletions packages/web-runtime/src/components/Topbar/TopBar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,13 @@ import {
useAuthStore,
useCapabilityStore,
useEmbedMode,
useExtensionRegistry,
useRouter,
useStore,
useThemeStore
} from '@ownclouders/web-pkg'
import { isRuntimeRoute } from '../../router'
import { getExtensionNavItems } from '../../helpers/navItems'
export default {
components: {
Expand All @@ -81,6 +83,7 @@ export default {
setup(props) {
const store = useStore()
const capabilityStore = useCapabilityStore()
const extensionRegistry = useExtensionRegistry()
const themeStore = useThemeStore()
const { currentTheme } = storeToRefs(themeStore)
Expand Down Expand Up @@ -127,9 +130,9 @@ export default {
if (app.type === 'extension') {
// check if the extension has at least one navItem with a matching menuId
return (
store.getters
.getNavItemsByExtension(app.id)
.filter((navItem) => isNavItemPermitted(permittedMenus, navItem)).length > 0 ||
getExtensionNavItems({ extensionRegistry, appId: app.id }).filter((navItem) =>
isNavItemPermitted(permittedMenus, navItem)
).length > 0 ||
(app.applicationMenu.enabled instanceof Function &&
app.applicationMenu.enabled(store, ability) &&
!permittedMenus.includes('user'))
Expand Down
25 changes: 15 additions & 10 deletions packages/web-runtime/src/container/api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { RouteRecordRaw, Router } from 'vue-router'
import clone from 'lodash-es/clone'
import { RuntimeApi } from './types'
import { ApiError } from '@ownclouders/web-pkg'
import { ApiError, ExtensionRegistry, SidebarNavExtension } from '@ownclouders/web-pkg'
import { get, isEqual, isObject, isArray, merge } from 'lodash-es'
import { Module, Store } from 'vuex'
import { App, Component, h } from 'vue'
import { App, Component, computed, h } from 'vue'
import { ApplicationTranslations, AppNavigationItem } from '@ownclouders/web-pkg'
import type { Language } from 'vue3-gettext'

Expand Down Expand Up @@ -61,22 +61,25 @@ const announceRoutes = (applicationId: string, router: Router, routes: RouteReco
* inject application specific navigation items into runtime
*
* @param applicationId
* @param store
* @param extensionRegistry
* @param navigationItems
*/
const announceNavigationItems = (
applicationId: string,
store: Store<unknown>,
extensionRegistry: ExtensionRegistry,
navigationItems: AppNavigationItem[]
): void => {
if (!isObject(navigationItems)) {
throw new ApiError("navigationItems can't be blank")
}

store.commit('SET_NAV_ITEMS_FROM_CONFIG', {
extension: applicationId,
navItems: navigationItems
})
const navExtensions = navigationItems.map((navItem) => ({
type: 'sidebarNav',
navItem,
scopes: [applicationId, `app.${applicationId}`]
})) as SidebarNavExtension[]

extensionRegistry.registerExtensions(computed(() => navExtensions))
}

/**
Expand Down Expand Up @@ -213,14 +216,16 @@ export const buildRuntimeApi = ({
store,
router,
gettext,
supportedLanguages
supportedLanguages,
extensionRegistry
}: {
applicationName: string
applicationId: string
store: Store<unknown>
gettext: Language
router: Router
supportedLanguages: { [key: string]: string }
extensionRegistry: ExtensionRegistry
}): RuntimeApi => {
if (!applicationName) {
throw new ApiError("applicationName can't be blank")
Expand All @@ -234,7 +239,7 @@ export const buildRuntimeApi = ({
announceRoutes: (routes: RouteRecordRaw[]): void =>
announceRoutes(applicationId, router, routes),
announceNavigationItems: (navigationItems: AppNavigationItem[]): void =>
announceNavigationItems(applicationId, store, navigationItems),
announceNavigationItems(applicationId, extensionRegistry, navigationItems),
announceTranslations: (appTranslations: ApplicationTranslations): void =>
announceTranslations(supportedLanguages, gettext, appTranslations),
announceStore: (applicationStore: Module<unknown, unknown>): void =>
Expand Down
7 changes: 5 additions & 2 deletions packages/web-runtime/src/container/application/classic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,19 +119,22 @@ export const convertClassicApplication = async ({
throw new RuntimeError("appInfo.name can't be blank")
}

const extensionRegistry = useExtensionRegistry({ configurationManager })

const runtimeApi = buildRuntimeApi({
applicationName,
applicationId,
store,
router,
gettext,
supportedLanguages
supportedLanguages,
extensionRegistry
})

await store.dispatch('registerApp', applicationScript.appInfo)

if (applicationScript.extensions) {
useExtensionRegistry({ configurationManager }).registerExtensions(applicationScript.extensions)
extensionRegistry.registerExtensions(applicationScript.extensions)
}

return new ClassicApplication(runtimeApi, applicationScript, app)
Expand Down
26 changes: 21 additions & 5 deletions packages/web-runtime/src/container/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { registerClient } from '../services/clientRegistration'
import { RuntimeConfiguration } from './types'
import { buildApplication, NextApplication } from './application'
import { Store } from 'vuex'
import { Router } from 'vue-router'
import { Router, RouteRecordNormalized } from 'vue-router'
import { App, computed } from 'vue'
import { loadTheme } from '../helpers/theme'
import OwnCloud from 'owncloud-sdk'
Expand All @@ -18,7 +18,9 @@ import {
useAuthStore,
AuthStore,
useCapabilityStore,
CapabilityStore
CapabilityStore,
useExtensionRegistry,
ExtensionRegistry
} from '@ownclouders/web-pkg'
import { authService } from '../services/auth'
import {
Expand Down Expand Up @@ -46,6 +48,7 @@ import { Resource } from '@ownclouders/web-client'
import PQueue from 'p-queue'
import { extractNodeId, extractStorageId } from '@ownclouders/web-client/src/helpers'
import { storeToRefs } from 'pinia'
import { getExtensionNavItems } from '../helpers/navItems'

const getEmbedConfigFromQuery = (
doesEmbedEnabledOptionExists: boolean
Expand Down Expand Up @@ -344,11 +347,21 @@ export const announceTheme = async ({
export const announcePiniaStores = () => {
const authStore = useAuthStore()
const capabilityStore = useCapabilityStore()
const extensionRegistry = useExtensionRegistry({ configurationManager })
const messagesStore = useMessages()
const modalStore = useModals()
const spacesStore = useSpacesStore()
const userStore = useUserStore()
return { authStore, capabilityStore, messagesStore, modalStore, spacesStore, userStore }

return {
authStore,
capabilityStore,
extensionRegistry,
messagesStore,
modalStore,
spacesStore,
userStore
}
}

/**
Expand Down Expand Up @@ -539,10 +552,12 @@ export const announcePasswordPolicyService = ({ app }: { app: App }): void => {
*/
export const announceDefaults = ({
store,
router
router,
extensionRegistry
}: {
store: Store<unknown>
router: Router
extensionRegistry: ExtensionRegistry
}): void => {
// set home route
const appIds = store.getters.appIds
Expand All @@ -555,7 +570,8 @@ export const announceDefaults = ({
return r.path.startsWith(`/${defaultExtensionId}`) && r.meta?.entryPoint === true
})
if (!route) {
route = store.getters.getNavItemsByExtension(defaultExtensionId)[0]?.route
route = getExtensionNavItems({ extensionRegistry, appId: defaultExtensionId })[0]
?.route as RouteRecordNormalized
}
if (route) {
router.addRoute({
Expand Down
14 changes: 13 additions & 1 deletion packages/web-runtime/src/helpers/navItems.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { AppNavigationItem } from '@ownclouders/web-pkg'
import { AppNavigationItem, ExtensionRegistry, SidebarNavExtension } from '@ownclouders/web-pkg'

export interface NavItem extends Omit<AppNavigationItem, 'name'> {
name: string
active: boolean
}

export const getExtensionNavItems = ({
extensionRegistry,
appId
}: {
extensionRegistry: ExtensionRegistry
appId: string
}) =>
extensionRegistry
.requestExtensions<SidebarNavExtension>('sidebarNav', [appId])
.map(({ navItem }) => navItem)
.filter((n) => n.enabled())
5 changes: 3 additions & 2 deletions packages/web-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ export const bootstrapApp = async (configurationPath: string): Promise<void> =>
const app = createApp(pages.success)
app.use(pinia)

const { authStore, capabilityStore, spacesStore, userStore } = announcePiniaStores()
const { authStore, capabilityStore, extensionRegistry, spacesStore, userStore } =
announcePiniaStores()

app.provide('$router', router)

Expand Down Expand Up @@ -145,7 +146,7 @@ export const bootstrapApp = async (configurationPath: string): Promise<void> =>
})
announceCustomStyles({ runtimeConfiguration })
announceCustomScripts({ runtimeConfiguration })
announceDefaults({ store, router })
announceDefaults({ store, router, extensionRegistry })

app.use(router)
app.use(store)
Expand Down
25 changes: 4 additions & 21 deletions packages/web-runtime/src/layouts/Application.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,13 @@
<script lang="ts">
import { mapGetters } from 'vuex'
import orderBy from 'lodash-es/orderBy'
import {
AppLoadingSpinner,
SidebarNavExtension,
useAuthStore,
useExtensionRegistry
} from '@ownclouders/web-pkg'
import { AppLoadingSpinner, useAuthStore, useExtensionRegistry } from '@ownclouders/web-pkg'
import TopBar from '../components/Topbar/TopBar.vue'
import MessageBar from '../components/MessageBar.vue'
import SidebarNav from '../components/SidebarNav/SidebarNav.vue'
import UploadInfo from '../components/UploadInfo.vue'
import MobileNav from '../components/MobileNav.vue'
import { NavItem } from '../helpers/navItems'
import { NavItem, getExtensionNavItems } from '../helpers/navItems'
import { LoadingIndicator } from '@ownclouders/web-pkg'
import {
useActiveApp,
Expand All @@ -61,7 +56,6 @@ import { useRouter } from 'vue-router'
import { useGettext } from 'vue3-gettext'
import '@uppy/core/dist/style.min.css'
import { AppNavigationItem } from '@ownclouders/web-pkg'
const MOBILE_BREAKPOINT = 640
Expand All @@ -86,13 +80,7 @@ export default defineComponent({
const extensionRegistry = useExtensionRegistry()
const extensionNavItems = computed(() =>
extensionRegistry
.requestExtensions<SidebarNavExtension>('sidebarNav', [
unref(activeApp),
`app.${unref(activeApp)}`
])
.map(({ navItem }) => navItem)
.filter((n) => n.enabled())
getExtensionNavItems({ extensionRegistry, appId: unref(activeApp) })
)
// FIXME: we can convert to a single router-view without name (thus without the loop) and without this watcher when we release v6.0.0
Expand Down Expand Up @@ -135,14 +123,9 @@ export default defineComponent({
return []
}
const items = [
...store.getters['getNavItemsByExtension'](unref(activeApp)),
...unref(extensionNavItems)
] as AppNavigationItem[]
const { href: currentHref } = router.resolve(unref(route))
return orderBy(
items.map((item) => {
unref(extensionNavItems).map((item) => {
let active = typeof item.isActive !== 'function' || item.isActive()
if (active) {
Expand Down
41 changes: 1 addition & 40 deletions packages/web-runtime/src/store/navigation.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,14 @@
const state = {
// static nav items are set during extension loading and will not be modified later on
staticNavItems: {},
closed: false
}

const mutations = {
/**
* Sets the nav items that are statically provided through extension configuration,
* i.e. the appInfo json.
*
* @param state
* @param extension
* @param navItems
* @constructor
*/
SET_NAV_ITEMS_FROM_CONFIG(state, { extension, navItems }) {
const staticNavItems = state.staticNavItems
staticNavItems[extension] = navItems
state.staticNavItems = staticNavItems
},
SET_CLOSED(state, closed) {
state.closed = closed
}
}

const getters = {
/**
* Get all nav items that are associated with the given extension id.
*
* @param state
* @param getters
* @returns {function(*): *[]}
*/
getNavItemsByExtension: (state) => (extension) => {
const staticNavItems = state.staticNavItems[extension] || []
return staticNavItems.filter((navItem) => {
if (!navItem.enabled) {
// when `enabled` callback not provided: count as enabled.
return true
}
try {
return navItem.enabled()
} catch (e) {
console.error('`enabled` callback on navItem ' + navItem.name + ' threw an error', e)
return false
}
})
}
}
const getters = {}

const actions = {
openNavigation({ commit }) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export const defaultStoreMockOptions = {
commit: jest.fn(),
getters: {
newFileHandlers: jest.fn(() => []),
getNavItemsByExtension: jest.fn(),
apps: jest.fn(() => ({})),
configuration: jest.fn().mockImplementation(() => ({
options: {
Expand Down

0 comments on commit aacedb8

Please sign in to comment.