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

feat: add registering nav items via extension #9814

Merged
merged 2 commits into from
Oct 17, 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
11 changes: 11 additions & 0 deletions changelog/unreleased/enhancement-nav-item-extension
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Enhancement: Registering nav items as extension

Nav items can now be registered with the new extension type `SidebarNavExtension`, which consists of a `AppNavigationItem` and optionally `scopes` (a list of app IDs where the nav item should show).

Also, 2 new optional properties have been added to the `AppNavigationItem` interface:

`handler` - a click handler that get executes on click. It takes priority over a given route.
`priority` - a number that determines the nav item's position.

https://github.com/owncloud/web/pull/9814
https://github.com/owncloud/web/issues/9239
12 changes: 8 additions & 4 deletions packages/web-app-admin-settings/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ const navItems = ({ $ability }: { $ability: Ability }): AppNavigationItem[] => [
},
enabled: () => {
return $ability.can('read-all', 'Setting')
}
},
priority: 10
},
{
name: $gettext('Users'),
Expand All @@ -119,7 +120,8 @@ const navItems = ({ $ability }: { $ability: Ability }): AppNavigationItem[] => [
},
enabled: () => {
return $ability.can('read-all', 'Account')
}
},
priority: 20
},
{
name: $gettext('Groups'),
Expand All @@ -129,7 +131,8 @@ const navItems = ({ $ability }: { $ability: Ability }): AppNavigationItem[] => [
},
enabled: () => {
return $ability.can('read-all', 'Group')
}
},
priority: 30
},
{
name: $gettext('Spaces'),
Expand All @@ -139,7 +142,8 @@ const navItems = ({ $ability }: { $ability: Ability }): AppNavigationItem[] => [
},
enabled: () => {
return $ability.can('read-all', 'Drive')
}
},
priority: 40
}
]

Expand Down
15 changes: 10 additions & 5 deletions packages/web-app-files/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ export const navItems = (context): AppNavigationItem[] => {
return !!context?.$store?.getters['runtime/spaces/spaces'].find(
(drive) => isPersonalSpaceResource(drive) && drive.isOwner(context.$store.getters.user)
)
}
},
priority: 10
},
{
name: $gettext('Favorites'),
Expand All @@ -72,7 +73,8 @@ export const navItems = (context): AppNavigationItem[] => {
},
enabled(capabilities) {
return capabilities.files?.favorites
}
},
priority: 20
},
{
name: $gettext('Shares'),
Expand All @@ -91,7 +93,8 @@ export const navItems = (context): AppNavigationItem[] => {
],
enabled(capabilities) {
return capabilities.files_sharing?.api_enabled !== false
}
},
priority: 30
},
{
name: $gettext('Spaces'),
Expand All @@ -102,7 +105,8 @@ export const navItems = (context): AppNavigationItem[] => {
activeFor: [{ path: `/${appInfo.id}/spaces/project` }],
enabled(capabilities) {
return capabilities.spaces?.projects
}
},
priority: 40
},
{
name: $gettext('Deleted files'),
Expand All @@ -113,7 +117,8 @@ export const navItems = (context): AppNavigationItem[] => {
activeFor: [{ path: `/${appInfo.id}/trash` }],
enabled(capabilities) {
return capabilities.dav?.trashbin === '1.0' && capabilities.files?.undelete
}
},
priority: 50
}
]
}
Expand Down
2 changes: 2 additions & 0 deletions packages/web-pkg/src/apps/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ export interface AppNavigationItem {
name?: string | ((capabilities?: Record<string, any>) => string)
route?: RouteLocationRaw
tag?: string
handler?: () => void
priority?: number
}

/**
Expand Down
20 changes: 17 additions & 3 deletions packages/web-pkg/src/composables/piniaStores/extensionRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { defineStore } from 'pinia'
import { Ref, hasInjectionContext, unref } from 'vue'
import { useConfigurationManager } from '../configuration'
import { ConfigurationManager } from '../../configuration'
import { AppNavigationItem } from '../../apps'

export type BaseExtension = {
id: string
type: string
scopes?: string[]
}

export interface ActionExtension extends BaseExtension {
Expand All @@ -20,7 +22,12 @@ export interface SearchExtension extends BaseExtension {
searchProvider: SearchProvider
}

export type Extension = ActionExtension | SearchExtension
export interface SidebarNavExtension extends BaseExtension {
type: 'sidebarNav'
navItem: AppNavigationItem
}

export type Extension = ActionExtension | SearchExtension | SidebarNavExtension

export const useExtensionRegistry = ({
configurationManager
Expand All @@ -41,13 +48,20 @@ export const useExtensionRegistry = ({
getters: {
requestExtensions:
(state) =>
<ExtensionType extends Extension>(type: string) => {
<ExtensionType extends Extension>(type: string, scope?: string) => {
return state.extensions
.map((e) =>
unref(e).filter((e) => e.type === type && !options.disabledExtensions.includes(e.id))
unref(e).filter(
(e) =>
e.type === type &&
!options.disabledExtensions.includes(e.id) &&
(!scope || e.scopes?.includes(scope))
)
)
.flat() as ExtensionType[]
}
}
})()
}

export type ExtensionRegistry = ReturnType<typeof useExtensionRegistry>
7 changes: 5 additions & 2 deletions packages/web-runtime/src/components/SidebarNav/SidebarNav.vue
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
:name="link.name"
:collapsed="navigation.closed"
:tag="link.tag"
:handler="link.handler"
/>
</oc-list>
</nav>
Expand Down Expand Up @@ -189,7 +190,8 @@ export default defineComponent({
justify-content: flex-end !important;
}

.oc-sidebar-nav li a:not(.active) {
.oc-sidebar-nav li a:not(.active),
.oc-sidebar-nav li button:not(.active) {
&:hover,
&:focus {
text-decoration: none !important;
Expand All @@ -198,7 +200,8 @@ export default defineComponent({
}
}

.oc-sidebar-nav li a.active {
.oc-sidebar-nav li a.active,
.oc-sidebar-nav li button.active {
&:focus,
&:hover {
color: var(--oc-color-swatch-primary-contrast);
Expand Down
28 changes: 23 additions & 5 deletions packages/web-runtime/src/components/SidebarNav/SidebarNavItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
<li class="oc-sidebar-nav-item oc-pb-xs oc-px-s" :aria-current="active ? 'page' : null">
<oc-button
v-oc-tooltip="toolTip"
type="router-link"
:type="handler ? 'button' : 'router-link'"
:appearance="active ? 'raw-inverse' : 'raw'"
:variation="active ? 'primary' : 'passive'"
:class="['oc-sidebar-nav-item-link', { active: active }]"
:to="target"
:class="['oc-sidebar-nav-item-link', 'oc-oc-width-1-1', { active: active }]"
:data-nav-id="index"
:data-nav-name="navName"
v-bind="attrs"
>
<span class="oc-flex">
<oc-icon :name="icon" :fill-type="fillType" variation="inherit" />
Expand All @@ -19,7 +19,7 @@
</li>
</template>
<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { computed, defineComponent, PropType } from 'vue'
import { RouteLocationRaw } from 'vue-router'

export default defineComponent({
Expand Down Expand Up @@ -60,11 +60,29 @@ export default defineComponent({
type: String,
required: false,
default: null
},
handler: {
type: Function as PropType<() => void>,
required: false,
default: null
}
},
setup(props) {
const attrs = computed(() => {
return {
...(props.handler && { onClick: props.handler }),
...(props.target && { to: props.target })
}
})

return { attrs }
},
computed: {
navName() {
return this.$router?.resolve(this.target, this.$route)?.name || 'route.name'
if (this.target) {
return this.$router?.resolve(this.target, this.$route)?.name || 'route.name'
}
return this.name
},
toolTip() {
const value = this.collapsed
Expand Down
63 changes: 37 additions & 26 deletions packages/web-runtime/src/layouts/Application.vue
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@

<script lang="ts">
import { mapActions, mapGetters } from 'vuex'
import { AppLoadingSpinner } from '@ownclouders/web-pkg'
import orderBy from 'lodash-es/orderBy'
import { AppLoadingSpinner, SidebarNavExtension, 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'
Expand Down Expand Up @@ -76,6 +77,13 @@ export default defineComponent({
const { $gettext } = useGettext()
const isUserContext = useUserContext({ store })
const activeApp = useActiveApp()
const extensionRegistry = useExtensionRegistry()

const extensionNavItems = computed(() =>
extensionRegistry
.requestExtensions<SidebarNavExtension>('sidebarNav', unref(activeApp))
.map(({ navItem }) => navItem)
)

// 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
watch(
Expand Down Expand Up @@ -117,36 +125,39 @@ export default defineComponent({
return []
}

const items = store.getters['getNavItemsByExtension'](unref(activeApp)) as AppNavigationItem[]
if (!items) {
return []
}
const items = [
...store.getters['getNavItemsByExtension'](unref(activeApp)),
...unref(extensionNavItems)
] as AppNavigationItem[]

const { href: currentHref } = router.resolve(unref(route))
return items.map((item) => {
let active = typeof item.isActive !== 'function' || item.isActive()
return orderBy(
items.map((item) => {
let active = typeof item.isActive !== 'function' || item.isActive()

if (active) {
active = [item.route, ...(item.activeFor || [])].filter(Boolean).some((currentItem) => {
try {
const comparativeHref = router.resolve(currentItem).href
return currentHref.startsWith(comparativeHref)
} catch (e) {
console.error(e)
return false
}
})
}
if (active) {
active = [item.route, ...(item.activeFor || [])].filter(Boolean).some((currentItem) => {
try {
const comparativeHref = router.resolve(currentItem).href
return currentHref.startsWith(comparativeHref)
} catch (e) {
console.error(e)
return false
}
})
}

const name =
typeof item.name === 'function' ? item.name(store.getters['capabilities']) : item.name
const name =
typeof item.name === 'function' ? item.name(store.getters['capabilities']) : item.name

return {
...item,
name: $gettext(name),
active
}
})
return {
...item,
name: $gettext(name),
active
}
}),
['priority', 'name']
)
})

const isSidebarVisible = computed(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

exports[`OcSidebarNav renders navItem with toolTip if collapsed 1`] = `
<li class="oc-sidebar-nav-item oc-pb-xs oc-px-s" id="123">
<router-link-stub class="oc-button oc-rounded oc-button-m oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-sidebar-nav-item-link" data-nav-id="5" data-nav-name="route.name" to="/files/list/all"></router-link-stub>
<router-link-stub class="oc-button oc-rounded oc-button-m oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-sidebar-nav-item-link oc-oc-width-1-1" data-nav-id="5" data-nav-name="route.name" to="/files/list/all"></router-link-stub>
</li>
`;

exports[`OcSidebarNav renders navItem without toolTip if expanded 1`] = `
<li class="oc-sidebar-nav-item oc-pb-xs oc-px-s" id="123">
<router-link-stub class="oc-button oc-rounded oc-button-m oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-sidebar-nav-item-link" data-nav-id="5" data-nav-name="route.name" to="/files/list/all"></router-link-stub>
<router-link-stub class="oc-button oc-rounded oc-button-m oc-button-justify-content-center oc-button-gap-m oc-button-passive oc-button-passive-raw oc-sidebar-nav-item-link oc-oc-width-1-1" data-nav-id="5" data-nav-name="route.name" to="/files/list/all"></router-link-stub>
</li>
`;