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>
pull/6318/head
kontrollanten 2024-04-04 08:17:59 +02:00 committed by GitHub
parent 9f92c8c426
commit cd42491cf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 146 additions and 53 deletions

View File

@ -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
}))
]
}
}

View File

@ -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'
}
}
]
}
]
}

View File

@ -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 })
}
}

View File

@ -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: '/'
}
},
{

View File

@ -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 {

View File

@ -0,0 +1 @@
export * from './plugin-pages.component'

View File

@ -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 })
}
}

View File

@ -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

View File

@ -8,4 +8,5 @@ export type PluginClientScope =
'video-edit' |
'admin-plugin' |
'my-library' |
'video-channel'
'video-channel' |
'my-account'

View File

@ -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

View File

@ -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'
}