feat: add support for sub routes under /my-account (#6218)
* feat: add support for sub routes under /my-account closes #6217 * feat(plugins/client-routes): page titles Add support for adding custom page titles in client routes. * fix(client/PluginPages): reload component upon URL change * Styling * docs(plugins): update registerClientRoute --------- Co-authored-by: Chocobozzz <me@florianbigard.com>
This commit is contained in:
parent
9f92c8c426
commit
cd42491cf0
13 changed files with 146 additions and 53 deletions
|
@ -1,8 +1,8 @@
|
|||
import { Component, OnInit } from '@angular/core'
|
||||
import { AuthUser, ScreenService } from '@app/core'
|
||||
import { TopMenuDropdownParam, TopMenuDropdownComponent } from '../shared/shared-main/misc/top-menu-dropdown.component'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { NgClass } from '@angular/common'
|
||||
import { Component, OnInit } from '@angular/core'
|
||||
import { RouterOutlet } from '@angular/router'
|
||||
import { AuthUser, PluginService, ScreenService } from '@app/core'
|
||||
import { TopMenuDropdownComponent, TopMenuDropdownParam } from '../shared/shared-main/misc/top-menu-dropdown.component'
|
||||
|
||||
@Component({
|
||||
selector: 'my-my-account',
|
||||
|
@ -15,17 +15,23 @@ export class MyAccountComponent implements OnInit {
|
|||
menuEntries: TopMenuDropdownParam[] = []
|
||||
user: AuthUser
|
||||
|
||||
constructor (private screenService: ScreenService) { }
|
||||
constructor (
|
||||
private pluginService: PluginService,
|
||||
private screenService: ScreenService
|
||||
) { }
|
||||
|
||||
get isBroadcastMessageDisplayed () {
|
||||
return this.screenService.isBroadcastMessageDisplayed
|
||||
}
|
||||
|
||||
ngOnInit (): void {
|
||||
this.buildMenu()
|
||||
this.pluginService.ensurePluginsAreLoaded('my-account')
|
||||
.then(() => this.buildMenu())
|
||||
}
|
||||
|
||||
private buildMenu () {
|
||||
const clientRoutes = this.pluginService.getAllRegisteredClientRoutesForParent('/my-account') || {}
|
||||
|
||||
const moderationEntries: TopMenuDropdownParam = {
|
||||
label: $localize`Moderation`,
|
||||
children: [
|
||||
|
@ -68,7 +74,13 @@ export class MyAccountComponent implements OnInit {
|
|||
routerLink: '/my-account/applications'
|
||||
},
|
||||
|
||||
moderationEntries
|
||||
moderationEntries,
|
||||
|
||||
...Object.values(clientRoutes)
|
||||
.map(clientRoute => ({
|
||||
label: clientRoute.menuItem?.label,
|
||||
routerLink: '/my-account/p/' + clientRoute.route
|
||||
}))
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import { BlocklistService } from '@app/shared/shared-moderation/blocklist.servic
|
|||
import { VideoBlockService } from '@app/shared/shared-moderation/video-block.service'
|
||||
import { TwoFactorService } from '@app/shared/shared-users/two-factor.service'
|
||||
import { VideoCommentService } from '@app/shared/shared-video-comment/video-comment.service'
|
||||
import { PluginPagesComponent } from '@app/shared/shared-plugin-pages/plugin-pages.component'
|
||||
|
||||
export default [
|
||||
{
|
||||
|
@ -160,6 +161,19 @@ export default [
|
|||
title: $localize`Import/Export`
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
path: 'p',
|
||||
children: [
|
||||
{
|
||||
path: '**',
|
||||
component: PluginPagesComponent,
|
||||
data: {
|
||||
parentRoute: '/my-account',
|
||||
pluginScope: 'my-account'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
import { AfterViewInit, Component, ElementRef, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { PluginService } from '@app/core'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
|
||||
@Component({
|
||||
templateUrl: './plugin-pages.component.html',
|
||||
standalone: true
|
||||
})
|
||||
export class PluginPagesComponent implements AfterViewInit {
|
||||
@ViewChild('root') root: ElementRef
|
||||
|
||||
constructor (
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private pluginService: PluginService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.pluginService.ensurePluginsAreLoaded('common')
|
||||
.then(() => this.loadRoute())
|
||||
}
|
||||
|
||||
private loadRoute () {
|
||||
const path = '/' + this.route.snapshot.url.map(u => u.path).join('/')
|
||||
|
||||
const registered = this.pluginService.getRegisteredClientRoute(path)
|
||||
if (!registered) {
|
||||
logger.info(`Could not find registered route ${path}`, this.pluginService.getAllRegisteredClientRoutes())
|
||||
|
||||
return this.router.navigate([ '/404' ], { skipLocationChange: true })
|
||||
}
|
||||
|
||||
registered.onMount({ rootEl: this.root.nativeElement })
|
||||
}
|
||||
}
|
|
@ -62,8 +62,11 @@ const routes: Routes = [
|
|||
},
|
||||
{
|
||||
path: 'p',
|
||||
loadChildren: () => import('./+plugin-pages/routes'),
|
||||
canActivateChild: [ MetaGuard ]
|
||||
loadChildren: () => import('./shared/shared-plugin-pages/routes'),
|
||||
canActivateChild: [ MetaGuard ],
|
||||
data: {
|
||||
parentRoute: '/'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
|
|
|
@ -50,7 +50,11 @@ export class PluginService implements ClientHook {
|
|||
video: []
|
||||
}
|
||||
private settingsScripts: { [ npmName: string ]: RegisterClientSettingsScriptOptions } = {}
|
||||
private clientRoutes: { [ route: string ]: RegisterClientRouteOptions } = {}
|
||||
private clientRoutes: {
|
||||
[ parentRoute in RegisterClientRouteOptions['parentRoute'] ]?: {
|
||||
[ route: string ]: RegisterClientRouteOptions
|
||||
}
|
||||
} = {}
|
||||
|
||||
private pluginsManager: PluginsManager
|
||||
|
||||
|
@ -126,12 +130,29 @@ export class PluginService implements ClientHook {
|
|||
return this.settingsScripts[npmName]
|
||||
}
|
||||
|
||||
getRegisteredClientRoute (route: string) {
|
||||
return this.clientRoutes[route]
|
||||
getRegisteredClientRoute (route: string, parentRoute: RegisterClientRouteOptions['parentRoute']) {
|
||||
if (!this.clientRoutes[parentRoute]) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return this.clientRoutes[parentRoute][route]
|
||||
}
|
||||
|
||||
getAllRegisteredClientRoutesForParent (parentRoute: RegisterClientRouteOptions['parentRoute']) {
|
||||
return this.clientRoutes[parentRoute]
|
||||
}
|
||||
|
||||
getAllRegisteredClientRoutes () {
|
||||
return Object.keys(this.clientRoutes)
|
||||
.map((parentRoute: RegisterClientRouteOptions['parentRoute']) => {
|
||||
return Object.keys(this.clientRoutes[parentRoute])
|
||||
.map(route => {
|
||||
if (parentRoute === '/') return route
|
||||
|
||||
return parentRoute + route
|
||||
})
|
||||
})
|
||||
.flat()
|
||||
}
|
||||
|
||||
async translateSetting (npmName: string, setting: RegisterClientFormFieldOptions) {
|
||||
|
@ -180,11 +201,17 @@ export class PluginService implements ClientHook {
|
|||
}
|
||||
|
||||
private onClientRoute (options: RegisterClientRouteOptions) {
|
||||
const parentRoute = options.parentRoute || '/'
|
||||
|
||||
const route = options.route.startsWith('/')
|
||||
? options.route
|
||||
: `/${options.route}`
|
||||
|
||||
this.clientRoutes[route] = options
|
||||
if (!this.clientRoutes[parentRoute]) {
|
||||
this.clientRoutes[parentRoute] = {}
|
||||
}
|
||||
|
||||
this.clientRoutes[parentRoute][route] = options
|
||||
}
|
||||
|
||||
private buildPeerTubeHelpers (pluginInfo: PluginInfo): RegisterClientHelpers {
|
||||
|
|
1
client/src/app/shared/shared-plugin-pages/index.ts
Normal file
1
client/src/app/shared/shared-plugin-pages/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './plugin-pages.component'
|
|
@ -0,0 +1,58 @@
|
|||
import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild } from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { MetaService, PluginService } from '@app/core'
|
||||
import { logger } from '@root-helpers/logger'
|
||||
import { Subscription } from 'rxjs/internal/Subscription'
|
||||
|
||||
@Component({
|
||||
templateUrl: './plugin-pages.component.html',
|
||||
standalone: true
|
||||
})
|
||||
export class PluginPagesComponent implements OnDestroy, AfterViewInit {
|
||||
@ViewChild('root') root: ElementRef
|
||||
|
||||
private urlSub: Subscription
|
||||
|
||||
constructor (
|
||||
private metaService: MetaService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private pluginService: PluginService
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
ngAfterViewInit () {
|
||||
this.urlSub = this.route.url.subscribe(() => {
|
||||
this.loadRoute()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy () {
|
||||
if (this.urlSub) this.urlSub.unsubscribe()
|
||||
}
|
||||
|
||||
private async loadRoute () {
|
||||
await this.pluginService.ensurePluginsAreLoaded(this.route.snapshot.data.pluginScope || 'common')
|
||||
|
||||
if (!this.route.snapshot.data.parentRoute) {
|
||||
logger.error('Missing "parentRoute" URL data to load plugin route ' + this.route.snapshot.url)
|
||||
return
|
||||
}
|
||||
|
||||
const path = '/' + this.route.snapshot.url.map(u => u.path).join('/')
|
||||
|
||||
const registered = this.pluginService.getRegisteredClientRoute(path, this.route.snapshot.data.parentRoute)
|
||||
if (!registered) {
|
||||
logger.info(`Could not find registered route ${path}`, { routes: this.pluginService.getAllRegisteredClientRoutes() })
|
||||
|
||||
return this.router.navigate([ '/404' ], { skipLocationChange: true })
|
||||
}
|
||||
|
||||
if (registered.title) {
|
||||
this.metaService.setTitle(registered.title)
|
||||
}
|
||||
|
||||
registered.onMount({ rootEl: this.root.nativeElement })
|
||||
}
|
||||
}
|
|
@ -70,7 +70,8 @@ class PluginsManager {
|
|||
'video-edit': new ReplaySubject<boolean>(1),
|
||||
'embed': new ReplaySubject<boolean>(1),
|
||||
'my-library': new ReplaySubject<boolean>(1),
|
||||
'video-channel': new ReplaySubject<boolean>(1)
|
||||
'video-channel': new ReplaySubject<boolean>(1),
|
||||
'my-account': new ReplaySubject<boolean>(1)
|
||||
}
|
||||
|
||||
private readonly peertubeHelpersFactory: PeertubeHelpersFactory
|
||||
|
|
|
@ -8,4 +8,5 @@ export type PluginClientScope =
|
|||
'video-edit' |
|
||||
'admin-plugin' |
|
||||
'my-library' |
|
||||
'video-channel'
|
||||
'video-channel' |
|
||||
'my-account'
|
||||
|
|
|
@ -1,6 +1,15 @@
|
|||
export interface RegisterClientRouteOptions {
|
||||
route: string
|
||||
|
||||
// Plugin route can be injected in a sub router, like the my-account page
|
||||
parentRoute?: '/' | '/my-account'
|
||||
// If parent route has a sub menu, specify the new entry sub menu settings
|
||||
menuItem?: {
|
||||
label?: string
|
||||
}
|
||||
|
||||
title?: string
|
||||
|
||||
onMount (options: {
|
||||
rootEl: HTMLElement
|
||||
}): void
|
||||
|
|
|
@ -872,6 +872,11 @@ To create a client page, register a new client route:
|
|||
function register ({ registerClientRoute }) {
|
||||
registerClientRoute({
|
||||
route: 'my-super/route',
|
||||
title: 'Page title for this route',
|
||||
parentRoute: '/my-account', // Optional. The full path will be /my-account/p/my-super/route.
|
||||
menuItem: { // Optional. This will add a menu item to this route. Only supported when parentRoute is '/my-account'.
|
||||
label: 'Sub route',
|
||||
},
|
||||
onMount: ({ rootEl }) => {
|
||||
rootEl.innerHTML = 'hello'
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue