feat(fe2): updated saved view tabs (#5332)

This commit is contained in:
Kristaps Fabians Geikins
2025-08-28 13:06:55 +03:00
committed by GitHub
parent 8dbd342a40
commit 74ebb21594
6 changed files with 69 additions and 65 deletions
@@ -25,10 +25,10 @@ const props = defineProps<{
isGenericErrorPage?: boolean
}>()
const route = useRoute()
const route = useCurrentRouteTillNavigated()
const isProjectRoute = computed(() => route.path.match(/\/projects\/[^/]+/))
const isWorkspaceRoute = computed(() => route.path.match(/\/workspaces\/[^/]+/))
const isProjectRoute = computed(() => route.value.path.match(/\/projects\/[^/]+/))
const isWorkspaceRoute = computed(() => route.value.path.match(/\/workspaces\/[^/]+/))
const finalError = computed(() => formatAppError(props.error))
const isNoProjectAccessError = computed(
@@ -106,10 +106,7 @@ import { useMutationLoading } from '@vue/apollo-composable'
import { Search, FolderPlus, Plus, X } from 'lucide-vue-next'
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
WorkspaceSeatType
} from '~/lib/common/generated/gql/graphql'
import { WorkspaceSeatType } from '~/lib/common/generated/gql/graphql'
import { useCreateSavedView } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { ViewsType, viewsTypeLabels } from '~/lib/viewer/helpers/savedViews'
@@ -147,7 +144,7 @@ const createSavedView = useCreateSavedView()
const isLoading = useMutationLoading()
const { on, bind, value: search } = useDebouncedTextInput()
const selectedViewsType = ref<ViewsType>(ViewsType.Personal)
const selectedViewsType = ref<ViewsType>(ViewsType.All)
const hideViewerSeatDisclaimer = useSynchronizedCookie<boolean>(
'hideViewerSeatSavedViewsDisclaimer',
{
@@ -167,12 +164,7 @@ const isLowerPlan = computed(() => !project.value?.workspace?.planSupportsSavedV
const onAddView = async () => {
if (isLoading.value) return
const view = await createSavedView({
visibility:
selectedViewsType.value === ViewsType.Shared
? SavedViewVisibility.Public
: undefined
})
const view = await createSavedView({})
if (view) {
// Auto-open the group that the view created to
openedGroupState.value.set(view.group.id, true)
@@ -63,18 +63,17 @@ import type { LayoutMenuItem } from '@speckle/ui-components'
import { useMutationLoading } from '@vue/apollo-composable'
import { Ellipsis, Plus } from 'lucide-vue-next'
import { graphql } from '~/lib/common/generated/gql'
import {
SavedViewVisibility,
type UseUpdateSavedViewGroup_SavedViewGroupFragment,
type ViewerSavedViewsPanelViewsGroup_ProjectFragment,
type ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment,
type ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
import type {
UseUpdateSavedViewGroup_SavedViewGroupFragment,
ViewerSavedViewsPanelViewsGroup_ProjectFragment,
ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment,
ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment
} from '~/lib/common/generated/gql/graphql'
import {
useCreateSavedView,
useUpdateSavedViewGroup
} from '~/lib/viewer/composables/savedViews/management'
import { ViewsType } from '~/lib/viewer/helpers/savedViews'
import type { ViewsType } from '~/lib/viewer/helpers/savedViews'
const MenuItems = StringEnum(['Delete', 'Rename'])
type MenuItems = StringEnumValues<typeof MenuItems>
@@ -182,9 +181,7 @@ const onActionChosen = async (item: LayoutMenuItem<MenuItems>) => {
const onAddGroupView = async () => {
await createView({
groupId: props.group.id,
visibility:
props.viewsType === ViewsType.Shared ? SavedViewVisibility.Public : undefined
groupId: props.group.id
})
open.value = true
}
+36 -19
View File
@@ -4,9 +4,10 @@ import type {
RouteLocationAsPathGeneric
} from '#vue-router'
import { buildManualPromise } from '@speckle/shared'
import { useScopedState } from '~/lib/common/composables/scopedState'
const useRouterNavigatingState = () =>
useState('use_router_navigating_state', () => ({
useScopedState('use_router_navigating_state', () => ({
allActiveWaits: <Array<Promise<unknown>>>[],
/**
* Used for debugging to assign an incrementing id to each invocation
@@ -20,8 +21,8 @@ const useRouterNavigatingDevUtils = () => {
const ret = {
getLogId: () => {
const newVal = state.value.logId + 1
state.value.logId = newVal
const newVal = state.logId + 1
state.logId = newVal
return newVal + ''
},
@@ -40,6 +41,16 @@ const useRouterNavigatingDevUtils = () => {
devTrace(...args)
})
},
waitForNavigationsClear: async () =>
await until($isNavigating)
.toBe(false, {
throwOnTimeout: true,
timeout: 500
})
.catch((err) => {
// Swallow throw, just log and continue
$logger.error({ err }, 'Waiting for nuxt navigations to clear timed out')
}),
isNuxtNavigating: $isNavigating,
logger: $logger
}
@@ -69,7 +80,7 @@ type SafeRouterNavigationOptions<
* Supports debugRoutes=1 query param for debug logs
*/
export const useSafeRouter = () => {
const { getLogId, debugLog, debugTrace, isNuxtNavigating, logger } =
const { getLogId, debugLog, debugTrace, waitForNavigationsClear } =
useRouterNavigatingDevUtils()
const router = useRouter()
const state = useRouterNavigatingState()
@@ -87,27 +98,16 @@ export const useSafeRouter = () => {
const waitPromise = buildManualPromise<void>()
const logId = getLogId()
const waitForNavigationsClear = async () =>
await until(isNuxtNavigating)
.toBe(false, {
throwOnTimeout: true,
timeout: 500
})
.catch((err) => {
// Swallow throw, just log and continue
logger.error({ err }, 'Waiting for nuxt navigations to clear timed out')
})
debugTrace(`[{logId}] Safe router ${action} registered`, {
initialTo: to(),
logId
})
try {
const activeWaits = state.value.allActiveWaits.slice()
const activeWaits = state.allActiveWaits.slice()
// Queue up another wait
state.value.allActiveWaits = [...state.value.allActiveWaits, waitPromise.promise]
state.allActiveWaits = [...state.allActiveWaits, waitPromise.promise]
// Wait for all previously queued up waits
await Promise.allSettled(activeWaits)
@@ -150,7 +150,7 @@ export const useSafeRouter = () => {
logId,
navResult
})
state.value.allActiveWaits = state.value.allActiveWaits.filter(
state.allActiveWaits = state.allActiveWaits.filter(
(p) => p !== waitPromise.promise
)
waitPromise.resolve()
@@ -160,7 +160,7 @@ export const useSafeRouter = () => {
waitPromise.reject(e)
throw e
} finally {
state.value.allActiveWaits = state.value.allActiveWaits.filter(
state.allActiveWaits = state.allActiveWaits.filter(
(p) => p !== waitPromise.promise
)
}
@@ -186,3 +186,20 @@ export const useSafeRouter = () => {
return { ...router, push, replace }
}
/**
* Similar to useRoute, but will not change the value until the new/incoming route has fully finished navigating
*/
export const useCurrentRouteTillNavigated = () => {
const baseRoute = useRoute()
const { $isNavigating } = useNuxtApp()
const route = shallowRef({ ...toRaw(baseRoute) })
watch($isNavigating, (newVal, oldVal) => {
if (!newVal && oldVal) {
route.value = { ...toRaw(baseRoute) }
}
})
return route
}
@@ -240,7 +240,7 @@ export const useUpdateSavedView = () => {
const { input } = params
const oldGroupId = params.view.group.id
const oldVisibility = params.view.visibility
// const oldVisibility = params.view.visibility
const result = await mutate(
{ input },
@@ -267,17 +267,18 @@ export const useUpdateSavedView = () => {
})
}
const newVisibility = update.visibility
const visibilityChanged = oldVisibility !== newVisibility
if (visibilityChanged) {
// Update all SavedViewGroup.views to see if it now should appear in there or not
modifyObjectField(
cache,
getCacheId('SavedViewGroup', newGroupId),
'views',
({ helpers: { evict } }) => evict()
)
}
// W/ current filter setup, if u can change visibility, you're gonna see it in all filtered groups
// const newVisibility = update.visibility
// const visibilityChanged = oldVisibility !== newVisibility
// if (visibilityChanged) {
// // Update all SavedViewGroup.views to see if it now should appear in there or not
// modifyObjectField(
// cache,
// getCacheId('SavedViewGroup', newGroupId),
// 'views',
// ({ helpers: { evict } }) => evict()
// )
// }
}
}
).catch(convertThrowIntoFetchResult)
@@ -1,16 +1,15 @@
import { throwUncoveredError, type StringEnumValues } from '@speckle/shared'
import { isObjectLike, isString } from 'lodash-es'
import { SavedViewVisibility } from '~/lib/common/generated/gql/graphql'
export const ViewsType = {
Personal: 'personal',
Shared: 'shared'
All: 'all',
Mine: 'mine'
} as const
export type ViewsType = StringEnumValues<typeof ViewsType>
export const viewsTypeLabels: Record<ViewsType, string> = {
[ViewsType.Personal]: 'Personal',
[ViewsType.Shared]: 'Shared'
[ViewsType.All]: 'All views',
[ViewsType.Mine]: 'My views'
}
/**
@@ -45,14 +44,12 @@ export const serializeSavedViewUrlSettings = (
}
export const viewsTypeToFilters = (type: ViewsType) => {
if (type === ViewsType.Personal) {
if (type === ViewsType.Mine) {
return {
onlyAuthored: true
}
} else if (type === ViewsType.Shared) {
return {
onlyVisibility: SavedViewVisibility.Public
}
} else if (type === ViewsType.All) {
return {}
} else {
throwUncoveredError(type)
}