From 1976bb998afb44e5c6a1f44a95dce5b325d9b632 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 30 May 2025 15:03:43 +0200 Subject: [PATCH 01/10] Update breadcrumps parsing to accept also function from meta.breadcrump attribute - remove support to store it in layoutStore --- web-app/packages/app/src/router.ts | 97 ++++++++++--------- .../packages/app/src/shims-vue-router.d.ts | 4 +- .../layout/components/AppBreadcrumbs.vue | 73 +++++++------- .../packages/lib/src/modules/layout/store.ts | 4 +- .../packages/lib/src/shims-vue-router.d.ts | 4 +- 5 files changed, 99 insertions(+), 83 deletions(-) diff --git a/web-app/packages/app/src/router.ts b/web-app/packages/app/src/router.ts index e8e1522d..6dbfdc22 100644 --- a/web-app/packages/app/src/router.ts +++ b/web-app/packages/app/src/router.ts @@ -194,75 +194,82 @@ export const createRouter = (pinia: Pinia) => { name: ProjectRouteName.ProjectTree, component: FileBrowserView, props: true, - meta: { public: true } + meta: { + public: true, + breadcrump: (route) => [ + { + title: route.params?.projectName, + path: `/projects/${route.params.namespace}/${route.params.projectName}/tree` + } + ] + } }, { path: 'settings', name: 'project-settings', component: ProjectSettingsView, - props: true + props: true, + meta: { + breadcrump: (route) => [ + { + title: route.params?.projectName, + path: `/projects/${route.params.namespace}/${route.params.projectName}/tree` + } + ] + } }, { path: 'history', name: 'project-versions', component: ProjectVersionsView, - props: true + props: true, + meta: { + breadcrump: (route) => [ + { + title: route.params?.projectName, + path: `/projects/${route.params.namespace}/${route.params.projectName}/tree` + } + ] + } }, { path: 'collaborators', name: ProjectRouteName.ProjectCollaborators, component: ProjectCollaboratorsView, - props: true + props: true, + meta: { + breadcrump: (route) => [ + { + title: route.params?.projectName, + path: `/projects/${route.params.namespace}/${route.params.projectName}/tree` + } + ] + } }, { path: 'history/:version_id/:path', name: 'file-version-detail', component: FileVersionDetailView, props: true, - meta: { public: true }, - // TODO: refactor to function in utils - beforeEnter(to, from, next) { - to.meta = { - ...to.meta, - breadcrump: [ - { - title: String(to.params.projectName), - path: `/projects/${to.params.namespace}/${to.params.projectName}/history` - }, - { - title: String(to.params.version_id), - path: `/projects/${to.params.namespace}/${to.params.projectName}/history/${to.params.version_id}` - }, - { - title: String(to.params.path), - path: to.fullPath - } - ] - } - next() + meta: { + public: true, + breadcrump: (route) => [ + { + title: route.params?.projectName, + path: `/projects/${route.params.namespace}/${route.params.projectName}/tree` + }, + { + title: route.params?.version_id, + path: `/projects/${route.params.namespace}/${route.params.projectName}/history/${route.params.version_id}` + }, + { + title: route.params?.path, + path: route.fullPath + } + ] } } ] - // Not apply for project version detail , which have own breadcrump - .map((child) => - child.name === 'file-version-detail' - ? child - : { - ...child, - beforeEnter: (to, from, next) => { - to.meta = { - ...to.meta, - breadcrump: [ - { - title: String(to.params.projectName), - path: to.fullPath - } - ] - } - next() - } - } - ) }, { path: '/:pathMatch(.*)*', diff --git a/web-app/packages/app/src/shims-vue-router.d.ts b/web-app/packages/app/src/shims-vue-router.d.ts index 9cbc6fc4..bce2786d 100644 --- a/web-app/packages/app/src/shims-vue-router.d.ts +++ b/web-app/packages/app/src/shims-vue-router.d.ts @@ -4,6 +4,8 @@ declare module 'vue-router' { interface RouteMeta { public?: boolean allowedForNoWorkspace?: boolean - breadcrump?: { title: string; path: string }[] + breadcrump?: + | { title: string; path: string }[] + | ((route) => { title: string; path: string }[]) } } diff --git a/web-app/packages/lib/src/modules/layout/components/AppBreadcrumbs.vue b/web-app/packages/lib/src/modules/layout/components/AppBreadcrumbs.vue index 3ef7239a..5e84fa55 100644 --- a/web-app/packages/lib/src/modules/layout/components/AppBreadcrumbs.vue +++ b/web-app/packages/lib/src/modules/layout/components/AppBreadcrumbs.vue @@ -49,44 +49,51 @@ import { MenuItem } from 'primevue/menuitem' import { computed } from 'vue' import { useRoute } from 'vue-router' -import { useLayoutStore } from '../store' - type EnhancedMenuItem = MenuItem & { path: string; active?: boolean } -const layoutStore = useLayoutStore() const route = useRoute() -// Merge all matched meta.breadcrumps with current route breadcrumps -const items = computed(() => - layoutStore.breadcrumps?.length - ? layoutStore.breadcrumps?.map((item, index, items) => ({ - label: item.title, - path: item.path, - active: index === items.length - 1 - })) - : [ - ...route.matched.reduce((acc, curr) => { - if (curr.name === route.name) return acc +function parseBreadcrump(item: { + title: string + path: string +}): EnhancedMenuItem { + return { + label: item.title, + path: item.path + } +} - return [ - ...acc, - ...(curr.meta?.breadcrump ?? []).map((item) => ({ - label: item.title, - path: item.path - })) - ] - }, []), - // adding current route wich is not in matched meta - ...(route.meta.breadcrump ?? []).map((item) => ({ - label: item.title, - path: item.path - })) - ] - // last will be active - .map((item, index, items) => ({ - ...item, - active: index === items.length - 1 - })) +const matchedBreacrumps = computed(() => { + return route.matched.reduce((acc, curr) => { + if (curr.name === route.name) return acc + const breadcrumps = + typeof curr.meta?.breadcrump === 'function' + ? curr.meta?.breadcrump(route) + : curr.meta?.breadcrump + return [...acc, ...(breadcrumps ?? []).map(parseBreadcrump)] + }, []) +}) + +const currentRouteBreacrumps = computed(() => { + const breadcrumps = + typeof route.meta?.breadcrump === 'function' + ? route.meta?.breadcrump(route) + : route.meta?.breadcrump + return (breadcrumps ?? []).map(parseBreadcrump) +}) + +// Merge all matched meta.breadcrumps with current route breadcrumps +const items = computed(() => + [ + ...matchedBreacrumps.value, + // adding current route wich is not in matched meta + ...currentRouteBreacrumps.value + ] + // last will be active + .map((item, index, items) => ({ + ...item, + active: index === items.length - 1 + })) ) diff --git a/web-app/packages/lib/src/modules/layout/store.ts b/web-app/packages/lib/src/modules/layout/store.ts index d75ef80a..2f3aef49 100644 --- a/web-app/packages/lib/src/modules/layout/store.ts +++ b/web-app/packages/lib/src/modules/layout/store.ts @@ -11,7 +11,6 @@ export interface LayoutState { isUnderOverlayBreakpoint: boolean /** Parsed closed elements from local storage and pushed back to local storage */ closedElements: string[] - breadcrumps: { title: string; path: string }[] } const CLOSED_ELEMENTS_KEY = 'mm-closed-elements' @@ -21,8 +20,7 @@ export const useLayoutStore = defineStore('layoutModule', { overlayBreakpoint: 1200, drawer: false, isUnderOverlayBreakpoint: false, - closedElements: [], - breadcrumps: [] + closedElements: [] }), getters: { /** diff --git a/web-app/packages/lib/src/shims-vue-router.d.ts b/web-app/packages/lib/src/shims-vue-router.d.ts index 9cbc6fc4..bce2786d 100644 --- a/web-app/packages/lib/src/shims-vue-router.d.ts +++ b/web-app/packages/lib/src/shims-vue-router.d.ts @@ -4,6 +4,8 @@ declare module 'vue-router' { interface RouteMeta { public?: boolean allowedForNoWorkspace?: boolean - breadcrump?: { title: string; path: string }[] + breadcrump?: + | { title: string; path: string }[] + | ((route) => { title: string; path: string }[]) } } From eeee2f035d38418f963241317af52a75d90ed92d Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Fri, 30 May 2025 15:21:06 +0200 Subject: [PATCH 02/10] show profile menu button just for logged user (in case of public projects) --- .../lib/src/modules/layout/components/AppHeaderTemplate.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-app/packages/lib/src/modules/layout/components/AppHeaderTemplate.vue b/web-app/packages/lib/src/modules/layout/components/AppHeaderTemplate.vue index b04a03c5..de50b643 100644 --- a/web-app/packages/lib/src/modules/layout/components/AppHeaderTemplate.vue +++ b/web-app/packages/lib/src/modules/layout/components/AppHeaderTemplate.vue @@ -38,6 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial @@ -74,7 +76,9 @@ import { useNotificationStore, ProjectVersion, errorUtils, - ProjectVersionChanges + ProjectVersionChanges, + DownloadProgress, + DownloadFileLarge } from '@mergin/lib' import { computed, ref, watch } from 'vue' import { useRoute } from 'vue-router' From 2040b25d9966e8643c7fb56a858e958c264932ee Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 4 Jun 2025 21:41:14 +0200 Subject: [PATCH 06/10] Text: change Account -> Profile texts --- .../lib/src/modules/user/views/ProfileViewTemplate.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue b/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue index 12358d8c..0a670892 100644 --- a/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue +++ b/web-app/packages/lib/src/modules/user/views/ProfileViewTemplate.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial class="flex flex-column lg:flex-row lg:align-items-center row-gap-3" > -

Account details

+

Profile

@@ -81,7 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial Date: Wed, 4 Jun 2025 21:42:46 +0200 Subject: [PATCH 07/10] Initial impl for page titles: - use route.meta.title for storing titles - parse titles from meta with composable useRouteTitle - store titles with specific handlers in modules.route.ts file - impl also admin --- web-app/packages/admin-app/src/App.vue | 13 +++++-- web-app/packages/admin-app/src/router.ts | 8 +++- .../admin-app/src/shims-vue-router.d.ts | 4 +- .../admin-lib/src/modules/admin/routes.ts | 26 ++++++++++++- .../admin-lib/src/modules/admin/types.ts | 8 ++++ .../admin-lib/src/shims-vue-router.d.ts | 7 ++++ web-app/packages/app/shims-vue-router.d.ts | 5 ++- web-app/packages/app/src/App.vue | 14 +++++-- web-app/packages/app/src/router.ts | 35 ++++++++++++----- .../lib/src/common/composables/index.ts | 5 +++ .../common/composables/use_router_title.ts | 39 +++++++++++++++++++ web-app/packages/lib/src/common/index.ts | 1 + .../packages/lib/src/common/route_utils.ts | 2 + .../lib/src/modules/dashboard/routes.ts | 10 +++++ .../lib/src/modules/project/routes.ts | 36 ++++++++++++++++- .../packages/lib/src/modules/project/types.ts | 14 +++++++ .../packages/lib/src/modules/user/routes.ts | 21 +++++++++- .../packages/lib/src/modules/user/types.ts | 4 ++ .../packages/lib/src/shims-vue-router.d.ts | 1 + 19 files changed, 227 insertions(+), 26 deletions(-) create mode 100644 web-app/packages/admin-lib/src/shims-vue-router.d.ts create mode 100644 web-app/packages/lib/src/common/composables/index.ts create mode 100644 web-app/packages/lib/src/common/composables/use_router_title.ts diff --git a/web-app/packages/admin-app/src/App.vue b/web-app/packages/admin-app/src/App.vue index 63d31509..cc7dff16 100644 --- a/web-app/packages/admin-app/src/App.vue +++ b/web-app/packages/admin-app/src/App.vue @@ -55,11 +55,12 @@ import { useNotificationStore, useUserStore, useLayoutStore, - useProjectStore + useProjectStore, + useRouterTitle } from '@mergin/lib' import { mapActions, mapState } from 'pinia' import { useToast } from 'primevue/usetoast' -import { defineComponent } from 'vue' +import { defineComponent, watchEffect } from 'vue' import { useMeta } from 'vue-meta' export default defineComponent({ @@ -120,8 +121,11 @@ export default defineComponent({ } }, setup() { + const { title } = useRouterTitle({ + defaultTitle: 'Mergin Maps Admin Panel' + }) useMeta({ - title: 'Mergin Maps', + title: 'Mergin Maps Admin Panel', meta: [ { name: 'description', @@ -144,6 +148,9 @@ export default defineComponent({ notificationStore.init(toast) layoutStore.init() projectStore.filterPermissions(['editor'], ['edit']) + watchEffect(() => { + document.title = title.value + }) }, async created() { await this.fetchConfig() diff --git a/web-app/packages/admin-app/src/router.ts b/web-app/packages/admin-app/src/router.ts index df1a3152..b30fe77a 100644 --- a/web-app/packages/admin-app/src/router.ts +++ b/web-app/packages/admin-app/src/router.ts @@ -13,7 +13,8 @@ import { ProjectSettingsView, ProjectVersionView, ProjectVersionsView, - OverviewView + OverviewView, + getAdminTitle } from '@mergin/admin-lib' import { NotFoundView, @@ -165,5 +166,10 @@ export const createRouter = (pinia: Pinia) => { routeUtils.isSuperUser(to, from, next, userStore) }) + router.beforeEach((to, from, next) => { + // Set the page title based on the route's meta title + to.meta.title = getAdminTitle + next() + }) return router } diff --git a/web-app/packages/admin-app/src/shims-vue-router.d.ts b/web-app/packages/admin-app/src/shims-vue-router.d.ts index 9cbc6fc4..3a973ee0 100644 --- a/web-app/packages/admin-app/src/shims-vue-router.d.ts +++ b/web-app/packages/admin-app/src/shims-vue-router.d.ts @@ -2,8 +2,6 @@ import 'vue-router' declare module 'vue-router' { interface RouteMeta { - public?: boolean - allowedForNoWorkspace?: boolean - breadcrump?: { title: string; path: string }[] + title?: string | string[] | ((route) => string | string[]) } } diff --git a/web-app/packages/admin-lib/src/modules/admin/routes.ts b/web-app/packages/admin-lib/src/modules/admin/routes.ts index c4835066..3439a561 100644 --- a/web-app/packages/admin-lib/src/modules/admin/routes.ts +++ b/web-app/packages/admin-lib/src/modules/admin/routes.ts @@ -2,7 +2,9 @@ // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -import { RouteRecord } from 'vue-router' +import { RouteLocationNormalizedLoaded, RouteRecord } from 'vue-router' + +import { AdminRouteParams } from './types' export enum AdminRoutes { ACCOUNTS = 'accounts', @@ -15,7 +17,27 @@ export enum AdminRoutes { ProjectHistory = 'project-versions', ProjectSettings = 'project-settings', ProjectVersion = 'project-version', - FileVersionDetail = 'file-version-detail' + FileVersionDetail = 'file-version-detail', + Login = 'login' +} + +export const getAdminTitle = (route: RouteLocationNormalizedLoaded) => { + const params = route.params as AdminRouteParams + const titles: Record = { + [AdminRoutes.Login]: ['Sign in', 'Mergin Maps Admin Panel'], + [AdminRoutes.ACCOUNTS]: 'Accounts', + [AdminRoutes.ACCOUNT]: 'Account details', + [AdminRoutes.OVERVIEW]: 'Admin overview', + [AdminRoutes.PROJECTS]: 'Projects', + [AdminRoutes.PROJECT]: ['Details', params.projectName], + [AdminRoutes.SETTINGS]: 'Settings', + [AdminRoutes.ProjectTree]: ['Files', params.projectName], + [AdminRoutes.ProjectHistory]: ['History', params.projectName], + [AdminRoutes.ProjectSettings]: ['Settings', params.projectName], + [AdminRoutes.ProjectVersion]: [params.version_id, params.projectName], + [AdminRoutes.FileVersionDetail]: [params.path, params.version_id] + } + return titles[route.name as AdminRoutes] } export const getRoutes = (): RouteRecord[] => [] diff --git a/web-app/packages/admin-lib/src/modules/admin/types.ts b/web-app/packages/admin-lib/src/modules/admin/types.ts index ce706c09..353d94df 100644 --- a/web-app/packages/admin-lib/src/modules/admin/types.ts +++ b/web-app/packages/admin-lib/src/modules/admin/types.ts @@ -84,4 +84,12 @@ export interface DownloadReportParams { date_to: string } +export interface AdminRouteParams { + namespace?: string + projectName?: string + version_id?: string + path?: string + username?: string +} + /* eslint-enable camelcase */ diff --git a/web-app/packages/admin-lib/src/shims-vue-router.d.ts b/web-app/packages/admin-lib/src/shims-vue-router.d.ts new file mode 100644 index 00000000..3a973ee0 --- /dev/null +++ b/web-app/packages/admin-lib/src/shims-vue-router.d.ts @@ -0,0 +1,7 @@ +import 'vue-router' + +declare module 'vue-router' { + interface RouteMeta { + title?: string | string[] | ((route) => string | string[]) + } +} diff --git a/web-app/packages/app/shims-vue-router.d.ts b/web-app/packages/app/shims-vue-router.d.ts index 9cbc6fc4..10ed52d0 100644 --- a/web-app/packages/app/shims-vue-router.d.ts +++ b/web-app/packages/app/shims-vue-router.d.ts @@ -4,6 +4,9 @@ declare module 'vue-router' { interface RouteMeta { public?: boolean allowedForNoWorkspace?: boolean - breadcrump?: { title: string; path: string }[] + breadcrump?: + | { title: string; path: string }[] + | ((route) => { title: string; path: string }[]) + title?: string | string[] | ((route) => string | string[]) } } diff --git a/web-app/packages/app/src/App.vue b/web-app/packages/app/src/App.vue index 1e9da4bc..94540b82 100644 --- a/web-app/packages/app/src/App.vue +++ b/web-app/packages/app/src/App.vue @@ -53,11 +53,13 @@ import { useNotificationStore, useUserStore, InstanceMaintenanceMessage, - useProjectStore + useProjectStore, + useRouterTitle, + routeUtils } from '@mergin/lib' import { mapActions, mapState } from 'pinia' import { useToast } from 'primevue/usetoast' -import { defineComponent } from 'vue' +import { defineComponent, watchEffect } from 'vue' import { useMeta } from 'vue-meta' export default defineComponent({ @@ -101,8 +103,11 @@ export default defineComponent({ } }, setup() { + const { title } = useRouterTitle({ + defaultTitle: routeUtils.DEFAULT_PAGE_TITLE + }) useMeta({ - title: 'Mergin Maps', + title: routeUtils.DEFAULT_PAGE_TITLE, meta: [ { name: 'description', @@ -125,6 +130,9 @@ export default defineComponent({ notificationStore.init(toast) layoutStore.init() projectStore.filterPermissions(['editor'], ['edit']) + watchEffect(() => { + document.title = title.value + }) }, async created() { await this.fetchConfig() diff --git a/web-app/packages/app/src/router.ts b/web-app/packages/app/src/router.ts index 6dbfdc22..27383245 100644 --- a/web-app/packages/app/src/router.ts +++ b/web-app/packages/app/src/router.ts @@ -12,7 +12,12 @@ import { routeUtils, useUserStore, SideBarTemplate as SideBar, - ProjectRouteName + ProjectRouteName, + UserRouteName, + DashboardRouteName, + getDashboardTitle, + getUserTitle, + getProjectTitle } from '@mergin/lib' import { Pinia } from 'pinia' import { @@ -57,28 +62,28 @@ export const createRouter = (pinia: Pinia) => { } }, path: '/login/:reset?', - name: 'login', + name: UserRouteName.Login, component: LoginView, props: true, meta: { public: true } }, { path: '/confirm-email/:token', - name: 'confirm_email', + name: UserRouteName.ConfirmEmail, component: VerifyEmailView, props: true, meta: { public: true } }, { path: '/change-password/:token', - name: 'change_password', + name: UserRouteName.ChangePassword, component: ChangePasswordView, props: true, meta: { public: true } }, { path: '/dashboard', - name: 'dashboard', + name: DashboardRouteName.Dashboard, components: { default: DashboardView, header: AppHeader, @@ -93,7 +98,7 @@ export const createRouter = (pinia: Pinia) => { }, { path: '/profile', - name: 'user_profile', + name: UserRouteName.UserProfile, meta: { allowedForNoWorkspace: true, breadcrump: [{ title: 'Profile', path: '/profile' }] @@ -107,7 +112,7 @@ export const createRouter = (pinia: Pinia) => { }, { path: '/projects', - name: 'projects', + name: ProjectRouteName.Projects, components: { default: ProjectsListView, header: AppHeader, @@ -118,13 +123,12 @@ export const createRouter = (pinia: Pinia) => { }, meta: { public: true, - title: 'Projects', breadcrump: [{ title: 'Projects', path: '/projects' }] }, children: [ { path: 'explore', - name: 'explore', + name: ProjectRouteName.ProjectsExplore, component: ProjectsListView, props: true, meta: { @@ -248,7 +252,7 @@ export const createRouter = (pinia: Pinia) => { }, { path: 'history/:version_id/:path', - name: 'file-version-detail', + name: ProjectRouteName.FileVersionDetail, component: FileVersionDetailView, props: true, meta: { @@ -284,5 +288,16 @@ export const createRouter = (pinia: Pinia) => { const userStore = useUserStore(pinia) routeUtils.isAuthenticatedGuard(to, from, next, userStore) }) + + router.beforeEach(async (to, from, next) => { + const titleResolvers = [getProjectTitle, getUserTitle, getDashboardTitle] + + // Use `find` to get the first resolved title + to.meta.title = titleResolvers + .map((resolver) => resolver(to)) + .find((title) => !!title) + + next() + }) return router } diff --git a/web-app/packages/lib/src/common/composables/index.ts b/web-app/packages/lib/src/common/composables/index.ts new file mode 100644 index 00000000..dd4fe405 --- /dev/null +++ b/web-app/packages/lib/src/common/composables/index.ts @@ -0,0 +1,5 @@ +// Copyright (C) Lutra Consulting Limited +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial + +export { default as useRouterTitle } from './use_router_title' diff --git a/web-app/packages/lib/src/common/composables/use_router_title.ts b/web-app/packages/lib/src/common/composables/use_router_title.ts new file mode 100644 index 00000000..1709eea1 --- /dev/null +++ b/web-app/packages/lib/src/common/composables/use_router_title.ts @@ -0,0 +1,39 @@ +import { computed } from 'vue' +import { useRoute } from 'vue-router' + +interface RouterTitleConfig { + defaultTitle?: string | string[] +} + +const SEPARATOR = ' \u2022 ' // Unicode for bullet character + +const useRouterTitle = (config: RouterTitleConfig = {}) => { + const route = useRoute() + + const metaTitle = computed(() => { + const metaTitle = route.meta.title + const defaultTitle = config.defaultTitle + if (!metaTitle) { + return defaultTitle + } + if (typeof metaTitle === 'function') { + return metaTitle(route) || defaultTitle + } + return metaTitle + }) + + const title = computed(() => { + const result = metaTitle.value + if (Array.isArray(result)) { + return result.filter(Boolean).join(` ${SEPARATOR} `) + } + return result + }) + + return { + metaTitle, + title + } +} + +export default useRouterTitle diff --git a/web-app/packages/lib/src/common/index.ts b/web-app/packages/lib/src/common/index.ts index 4e3f31f8..87759529 100644 --- a/web-app/packages/lib/src/common/index.ts +++ b/web-app/packages/lib/src/common/index.ts @@ -18,6 +18,7 @@ export * from './errors' export * from './mixins' export * from './http' export * from './types' +export * from './composables' // export * from './router_without_navigation_failure' export * as dateUtils from './date_utils' export * as htmlUtils from './html_utils' diff --git a/web-app/packages/lib/src/common/route_utils.ts b/web-app/packages/lib/src/common/route_utils.ts index d9866194..1c516eb8 100644 --- a/web-app/packages/lib/src/common/route_utils.ts +++ b/web-app/packages/lib/src/common/route_utils.ts @@ -6,6 +6,8 @@ import isEqual from 'lodash/isEqual' import pick from 'lodash/pick' import { NavigationGuardNext, RouteLocationNormalized } from 'vue-router' +export const DEFAULT_PAGE_TITLE = 'Mergin Maps' + export type IsAuthenticatedGuardOptions = { notAuthenticatedRedirectPath?: string } diff --git a/web-app/packages/lib/src/modules/dashboard/routes.ts b/web-app/packages/lib/src/modules/dashboard/routes.ts index 055037c7..3b0add1f 100644 --- a/web-app/packages/lib/src/modules/dashboard/routes.ts +++ b/web-app/packages/lib/src/modules/dashboard/routes.ts @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial +import { RouteLocationNormalizedLoaded } from 'vue-router' + /** * Enum for route names in the app's router. * Defines string constants for each route path used by project module. @@ -10,3 +12,11 @@ export enum DashboardRouteName { Dashboard = 'dashboard' } + +export const getDashboardTitle = (route: RouteLocationNormalizedLoaded) => { + const name = route.name as DashboardRouteName + const titles: Record = { + [DashboardRouteName.Dashboard]: 'Dashboard' + } + return titles[name] +} diff --git a/web-app/packages/lib/src/modules/project/routes.ts b/web-app/packages/lib/src/modules/project/routes.ts index a07b601b..a3322b1b 100644 --- a/web-app/packages/lib/src/modules/project/routes.ts +++ b/web-app/packages/lib/src/modules/project/routes.ts @@ -2,7 +2,11 @@ // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -import { RouteRecord } from 'vue-router' +import { RouteLocationNormalizedLoaded, RouteRecord } from 'vue-router' + +import { ProjectRouteParams, ProjectRouteQuery } from './types' + +import { DEFAULT_PAGE_TITLE } from '@/common/route_utils' /** * Enum for route names in the app's router. @@ -17,7 +21,6 @@ export enum ProjectRouteName { Projects = 'projects', /** Public projects */ ProjectsExplore = 'explore', - Projectsnamespace = 'namespace-projects', /** @deprecated */ Blob = 'blob', /** @@ -38,4 +41,33 @@ export enum ProjectRouteName { FileVersionDetail = 'file-version-detail' } +export const getProjectTitle = (route: RouteLocationNormalizedLoaded) => { + const name = route.name as ProjectRouteName + const params = route.params as ProjectRouteParams + const query = route.query as ProjectRouteQuery + const { projectName, path, version_id } = params as ProjectRouteParams + const titles: Record = { + [ProjectRouteName.Projects]: ['Projects'], + [ProjectRouteName.ProjectsExplore]: ['Public projects'], + [ProjectRouteName.Project]: ['Project details'], + [ProjectRouteName.ProjectTree]: [ + query.file_path || 'Files', + route.params.projectName as string + ], + [ProjectRouteName.ProjectSettings]: ['Settings', projectName], + [ProjectRouteName.ProjectHistory]: [ + query.version_id || 'History', + projectName + ].filter(Boolean), + [ProjectRouteName.ProjectCollaborators]: ['Collaborators', projectName], + [ProjectRouteName.FileVersionDetail]: [ + path, + `Version ${version_id as string}` + ], + [ProjectRouteName.VersionDetail]: DEFAULT_PAGE_TITLE, + [ProjectRouteName.Blob]: DEFAULT_PAGE_TITLE + } + return titles[name] +} + export default (): RouteRecord[] => [] diff --git a/web-app/packages/lib/src/modules/project/types.ts b/web-app/packages/lib/src/modules/project/types.ts index 844e776c..46e738b9 100644 --- a/web-app/packages/lib/src/modules/project/types.ts +++ b/web-app/packages/lib/src/modules/project/types.ts @@ -376,3 +376,17 @@ export interface ProjectCollaborator { project_role: ProjectRoleName | null role: ProjectRoleName } + + +// router related types +export interface ProjectRouteParams { + namespace?: string + projectName?: string + version_id?: string + path?: string +} + +export interface ProjectRouteQuery { + version_id?: string + file_path?: string +} diff --git a/web-app/packages/lib/src/modules/user/routes.ts b/web-app/packages/lib/src/modules/user/routes.ts index 67cc2247..d2a52edd 100644 --- a/web-app/packages/lib/src/modules/user/routes.ts +++ b/web-app/packages/lib/src/modules/user/routes.ts @@ -2,7 +2,11 @@ // // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial -import { RouteRecord } from 'vue-router' +import { RouteLocationNormalizedLoaded, RouteRecord } from 'vue-router' + +import { UserRouteParams } from './types' + +import { DEFAULT_PAGE_TITLE } from '@/common/route_utils' /** * Enum for user routes names @@ -15,4 +19,19 @@ export enum UserRouteName { UserProfile = 'user_profile' } +export const getUserTitle = (route: RouteLocationNormalizedLoaded) => { + const name = route.name as UserRouteName + const params = route.params as UserRouteParams + const titles: Record = { + [UserRouteName.Login]: [ + params.reset ? 'Reset password' : 'Sign in', + DEFAULT_PAGE_TITLE + ], + [UserRouteName.ConfirmEmail]: ['Confirm email address', DEFAULT_PAGE_TITLE], + [UserRouteName.ChangePassword]: ['Change password', DEFAULT_PAGE_TITLE], + [UserRouteName.UserProfile]: ['Your profile'] + } + return titles[name] +} + export default (): RouteRecord[] => [] diff --git a/web-app/packages/lib/src/modules/user/types.ts b/web-app/packages/lib/src/modules/user/types.ts index bb1f0b29..2990212a 100644 --- a/web-app/packages/lib/src/modules/user/types.ts +++ b/web-app/packages/lib/src/modules/user/types.ts @@ -147,4 +147,8 @@ export interface LastSeenWorkspace { lastSeen: number } +export interface UserRouteParams { + reset?: string +} + /* eslint-enable camelcase */ diff --git a/web-app/packages/lib/src/shims-vue-router.d.ts b/web-app/packages/lib/src/shims-vue-router.d.ts index bce2786d..10ed52d0 100644 --- a/web-app/packages/lib/src/shims-vue-router.d.ts +++ b/web-app/packages/lib/src/shims-vue-router.d.ts @@ -7,5 +7,6 @@ declare module 'vue-router' { breadcrump?: | { title: string; path: string }[] | ((route) => { title: string; path: string }[]) + title?: string | string[] | ((route) => string | string[]) } } From 81e925a4bc9170b62ffad00aa6ddbcdea239d775 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Thu, 5 Jun 2025 11:01:02 +0200 Subject: [PATCH 08/10] map dashboard to worksapce name Make use router title responsive to extended argument changes --- web-app/packages/admin-app/src/shims-vue-router.d.ts | 2 +- web-app/packages/admin-lib/src/shims-vue-router.d.ts | 2 +- web-app/packages/app/shims-vue-router.d.ts | 2 +- web-app/packages/app/src/App.vue | 2 +- web-app/packages/app/src/router.ts | 8 +++++--- .../lib/src/common/composables/use_router_title.ts | 10 +++++++--- web-app/packages/lib/src/modules/dashboard/routes.ts | 10 ++++++++-- web-app/packages/lib/src/shims-vue-router.d.ts | 2 +- 8 files changed, 25 insertions(+), 13 deletions(-) diff --git a/web-app/packages/admin-app/src/shims-vue-router.d.ts b/web-app/packages/admin-app/src/shims-vue-router.d.ts index 3a973ee0..1a925be0 100644 --- a/web-app/packages/admin-app/src/shims-vue-router.d.ts +++ b/web-app/packages/admin-app/src/shims-vue-router.d.ts @@ -2,6 +2,6 @@ import 'vue-router' declare module 'vue-router' { interface RouteMeta { - title?: string | string[] | ((route) => string | string[]) + title?: string | string[] | ((route, extened) => string | string[]) } } diff --git a/web-app/packages/admin-lib/src/shims-vue-router.d.ts b/web-app/packages/admin-lib/src/shims-vue-router.d.ts index 3a973ee0..a7a591d5 100644 --- a/web-app/packages/admin-lib/src/shims-vue-router.d.ts +++ b/web-app/packages/admin-lib/src/shims-vue-router.d.ts @@ -2,6 +2,6 @@ import 'vue-router' declare module 'vue-router' { interface RouteMeta { - title?: string | string[] | ((route) => string | string[]) + title?: string | string[] | ((route, extended) => string | string[]) } } diff --git a/web-app/packages/app/shims-vue-router.d.ts b/web-app/packages/app/shims-vue-router.d.ts index 10ed52d0..6c1bdb35 100644 --- a/web-app/packages/app/shims-vue-router.d.ts +++ b/web-app/packages/app/shims-vue-router.d.ts @@ -7,6 +7,6 @@ declare module 'vue-router' { breadcrump?: | { title: string; path: string }[] | ((route) => { title: string; path: string }[]) - title?: string | string[] | ((route) => string | string[]) + title?: string | string[] | ((route, extended) => string | string[]) } } diff --git a/web-app/packages/app/src/App.vue b/web-app/packages/app/src/App.vue index 94540b82..fe7593d2 100644 --- a/web-app/packages/app/src/App.vue +++ b/web-app/packages/app/src/App.vue @@ -160,7 +160,7 @@ export default defineComponent({ }, methods: { ...mapActions(useAppStore, ['setServerError']), - ...mapActions(useInstanceStore, ['fetchPing', 'fetchConfig', 'initApp']), + ...mapActions(useInstanceStore, ['fetchPing', 'fetchConfig']), ...mapActions(useNotificationStore, { notificationError: 'error' }), diff --git a/web-app/packages/app/src/router.ts b/web-app/packages/app/src/router.ts index 27383245..4fb00555 100644 --- a/web-app/packages/app/src/router.ts +++ b/web-app/packages/app/src/router.ts @@ -293,9 +293,11 @@ export const createRouter = (pinia: Pinia) => { const titleResolvers = [getProjectTitle, getUserTitle, getDashboardTitle] // Use `find` to get the first resolved title - to.meta.title = titleResolvers - .map((resolver) => resolver(to)) - .find((title) => !!title) + to.meta.title = (route) => { + return titleResolvers + .map((resolver) => resolver(route)) + .find((title) => title) + } next() }) diff --git a/web-app/packages/lib/src/common/composables/use_router_title.ts b/web-app/packages/lib/src/common/composables/use_router_title.ts index 1709eea1..89936854 100644 --- a/web-app/packages/lib/src/common/composables/use_router_title.ts +++ b/web-app/packages/lib/src/common/composables/use_router_title.ts @@ -1,4 +1,8 @@ -import { computed } from 'vue' +// Copyright (C) Lutra Consulting Limited +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial + +import { computed, toValue } from 'vue' import { useRoute } from 'vue-router' interface RouterTitleConfig { @@ -7,7 +11,7 @@ interface RouterTitleConfig { const SEPARATOR = ' \u2022 ' // Unicode for bullet character -const useRouterTitle = (config: RouterTitleConfig = {}) => { +const useRouterTitle = (config: RouterTitleConfig = {}, extended = {}) => { const route = useRoute() const metaTitle = computed(() => { @@ -17,7 +21,7 @@ const useRouterTitle = (config: RouterTitleConfig = {}) => { return defaultTitle } if (typeof metaTitle === 'function') { - return metaTitle(route) || defaultTitle + return metaTitle(route, toValue(extended)) || defaultTitle } return metaTitle }) diff --git a/web-app/packages/lib/src/modules/dashboard/routes.ts b/web-app/packages/lib/src/modules/dashboard/routes.ts index 3b0add1f..22b965e2 100644 --- a/web-app/packages/lib/src/modules/dashboard/routes.ts +++ b/web-app/packages/lib/src/modules/dashboard/routes.ts @@ -13,10 +13,16 @@ export enum DashboardRouteName { Dashboard = 'dashboard' } -export const getDashboardTitle = (route: RouteLocationNormalizedLoaded) => { +export const getDashboardTitle = ( + route: RouteLocationNormalizedLoaded, + extended?: { workspaceName: string } +) => { const name = route.name as DashboardRouteName const titles: Record = { - [DashboardRouteName.Dashboard]: 'Dashboard' + [DashboardRouteName.Dashboard]: [ + 'Dashboard', + extended?.workspaceName + ].filter(Boolean) } return titles[name] } diff --git a/web-app/packages/lib/src/shims-vue-router.d.ts b/web-app/packages/lib/src/shims-vue-router.d.ts index 10ed52d0..6c1bdb35 100644 --- a/web-app/packages/lib/src/shims-vue-router.d.ts +++ b/web-app/packages/lib/src/shims-vue-router.d.ts @@ -7,6 +7,6 @@ declare module 'vue-router' { breadcrump?: | { title: string; path: string }[] | ((route) => { title: string; path: string }[]) - title?: string | string[] | ((route) => string | string[]) + title?: string | string[] | ((route, extended) => string | string[]) } } From 98a09ed2d56a5c3210850e204e3907d24760044f Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Wed, 11 Jun 2025 13:15:48 +0200 Subject: [PATCH 09/10] bump version 2025.4.2 --- .gitignore | 5 +++++ server/mergin/version.py | 2 +- server/setup.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 94220ca0..7614b44b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ venv/ # production env .prod.env +.sso.env # generated documentation gen @@ -30,3 +31,7 @@ web-app/.node-version mergin-db-ce docker-compose.local.yml + +# SSO +*.pem +*.crt diff --git a/server/mergin/version.py b/server/mergin/version.py index a87af32c..d8556b12 100644 --- a/server/mergin/version.py +++ b/server/mergin/version.py @@ -4,4 +4,4 @@ def get_version(): - return "2025.4.1" + return "2025.4.2" diff --git a/server/setup.py b/server/setup.py index db732ec7..f6f32998 100644 --- a/server/setup.py +++ b/server/setup.py @@ -6,7 +6,7 @@ setup( name="mergin", - version="2025.4.1", + version="2025.4.2", url="https://github.com/MerginMaps/mergin", license="AGPL-3.0-only", author="Lutra Consulting Limited", From b84275eb28e0f3d192bc53c52f0b2e71de3f3c77 Mon Sep 17 00:00:00 2001 From: "marcel.kocisek" Date: Mon, 16 Jun 2025 11:16:41 +0200 Subject: [PATCH 10/10] Propagate ws name to projects - add public projects and mergin maps placeholder --- web-app/packages/lib/src/modules/project/routes.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/web-app/packages/lib/src/modules/project/routes.ts b/web-app/packages/lib/src/modules/project/routes.ts index a3322b1b..b4fbc0b6 100644 --- a/web-app/packages/lib/src/modules/project/routes.ts +++ b/web-app/packages/lib/src/modules/project/routes.ts @@ -41,14 +41,19 @@ export enum ProjectRouteName { FileVersionDetail = 'file-version-detail' } -export const getProjectTitle = (route: RouteLocationNormalizedLoaded) => { +export const getProjectTitle = ( + route: RouteLocationNormalizedLoaded, + extended?: { workspaceName: string } +) => { const name = route.name as ProjectRouteName const params = route.params as ProjectRouteParams const query = route.query as ProjectRouteQuery const { projectName, path, version_id } = params as ProjectRouteParams const titles: Record = { - [ProjectRouteName.Projects]: ['Projects'], - [ProjectRouteName.ProjectsExplore]: ['Public projects'], + [ProjectRouteName.Projects]: ['Projects', extended?.workspaceName].filter( + Boolean + ), + [ProjectRouteName.ProjectsExplore]: ['Public projects', DEFAULT_PAGE_TITLE], [ProjectRouteName.Project]: ['Project details'], [ProjectRouteName.ProjectTree]: [ query.file_path || 'Files',