diff --git a/packages/frontend-2/components/viewer/saved-views/panel/View.vue b/packages/frontend-2/components/viewer/saved-views/panel/View.vue index 2ca9fa192..07e96af38 100644 --- a/packages/frontend-2/components/viewer/saved-views/panel/View.vue +++ b/packages/frontend-2/components/viewer/saved-views/panel/View.vue @@ -26,6 +26,7 @@ :items="menuItems" :menu-id="menuId" mount-menu-on-body + :size="230" @chosen="({ item: actionItem }) => onActionChosen(actionItem)" > +const MenuItems = StringEnum(['Delete', 'LoadOriginalVersions', 'CopyLink']) +type MenuItems = StringEnumValues graphql(` fragment ViewerSavedViewsPanelView_SavedView on SavedView { @@ -103,9 +103,9 @@ const props = defineProps<{ view: ViewerSavedViewsPanelView_SavedViewFragment }>() -const eventBus = useEventBus() const deleteView = useDeleteSavedView() const isLoading = useMutationLoading() +const { copyLink, applyView } = useViewerSavedViewsUtils() const showEditDialog = ref(false) const showMenu = ref(false) @@ -115,7 +115,17 @@ const canUpdate = computed(() => props.view.permissions.canUpdate) const menuItems = computed((): LayoutMenuItem[][] => [ [ { - id: Menuitems.Delete, + id: MenuItems.LoadOriginalVersions, + title: 'Load with original model version' + }, + { + id: MenuItems.CopyLink, + title: 'Copy link' + } + ], + [ + { + id: MenuItems.Delete, title: 'Delete', disabled: !canUpdate.value?.authorized || isLoading.value, disabledTooltip: canUpdate.value.errorMessage @@ -125,19 +135,30 @@ const menuItems = computed((): LayoutMenuItem[][] => [ const onActionChosen = async (item: LayoutMenuItem) => { switch (item.id) { - case Menuitems.Delete: + case MenuItems.Delete: await deleteView({ view: props.view }) break + case MenuItems.CopyLink: + await copyLink({ + settings: { + id: props.view.id + } + }) + break + case MenuItems.LoadOriginalVersions: + applyView({ + id: props.view.id, + loadOriginal: true + }) + break default: throwUncoveredError(item.id) } } const apply = async () => { - // Force update, even if the view id is already set - // (in case this is a frustration click w/ the state not applying) - eventBus.emit(ViewerEventBusKeys.UpdateSavedView, { - viewId: props.view.id + applyView({ + id: props.view.id }) } diff --git a/packages/frontend-2/composables/routing.ts b/packages/frontend-2/composables/routing.ts new file mode 100644 index 000000000..925ce5887 --- /dev/null +++ b/packages/frontend-2/composables/routing.ts @@ -0,0 +1,25 @@ +import { until } from '@vueuse/core' + +/** + * Global state that tells you if the router is in the middle of a navigation + */ +export const useRouterNavigating = () => { + const { $isNavigating } = useNuxtApp() + const logger = useLogger() + + const waitUntilReady = async () => { + try { + await until($isNavigating).toBe(false, { throwOnTimeout: true, timeout: 500 }) + } catch (e) { + logger.warn(e, 'Wait for router ready failed w/ timeout') + } + } + + return { + isNavigating: $isNavigating, + /** + * Wait for router to flush active navigations + */ + waitUntilReady + } +} diff --git a/packages/frontend-2/lib/common/composables/async.ts b/packages/frontend-2/lib/common/composables/async.ts index 6030f5c70..ede59e335 100644 --- a/packages/frontend-2/lib/common/composables/async.ts +++ b/packages/frontend-2/lib/common/composables/async.ts @@ -27,3 +27,39 @@ export const writableAsyncComputed: typeof originalWritableAsyncComputed = (para : undefined }) } + +/** + * Like normal watch() except handles async callbacks in an ordered manner - previous + * watches must complete, before new ones can get processed + */ +export const watchAsync = ((...args: Parameters) => { + const [source, cb, options] = args + const logger = useLogger() + + const watches = shallowRef>>([]) + + const watchRet = watch( + source, + (newVal, oldVal, onCleanup) => { + // 1. Wait for all active processing to finish + // 2. Then run new processing + // 3. At the end - clean up watches array + const handlerPromise = Promise.allSettled(watches.value).finally(() => + Promise.resolve(cb(newVal, oldVal, onCleanup)) + .catch((e) => { + logger.error(e, 'Error occurred in watchAsync callback') + throw e + }) + .finally(() => { + watches.value = watches.value.filter((p) => p !== handlerPromise) + }) + ) + + // Add handler to array + watches.value = [...watches.value, handlerPromise] + }, + options + ) + + return watchRet +}) as unknown as typeof watch // ts typing difficulty diff --git a/packages/frontend-2/lib/common/composables/graphql.ts b/packages/frontend-2/lib/common/composables/graphql.ts index 4a705181e..a6c7e24d6 100644 --- a/packages/frontend-2/lib/common/composables/graphql.ts +++ b/packages/frontend-2/lib/common/composables/graphql.ts @@ -1,8 +1,9 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { - OperationVariables, - QueryOptions, - WatchQueryFetchPolicy +import { + NetworkStatus, + type OperationVariables, + type QueryOptions, + type WatchQueryFetchPolicy } from '@apollo/client/core' import type { DocumentParameter, @@ -265,3 +266,26 @@ export const usePageQueryStandardFetchPolicy = () => { return hasNavigatedInCSR.value ? 'cache-and-network' : undefined }) } + +/** + * By default 'variables' off useQuery updates the moment variables are updated. This returns the variables + * associated with the active result. So if the result is still loading, the variables are gonna be undefined too. + */ +export const useQueryResultVariables = < + TResult = any, + TVariables extends OperationVariables = OperationVariables +>( + useQueryRet: ReturnType> +) => { + const { variables, onResult } = useQueryRet + + const currentVariables = shallowRef<(typeof variables)['value']>() + onResult((res) => { + if (res.networkStatus !== NetworkStatus.ready) return + currentVariables.value = variables.value + }) + + const resultVariables = computed(() => currentVariables.value) + + return resultVariables +} diff --git a/packages/frontend-2/lib/common/composables/url.ts b/packages/frontend-2/lib/common/composables/url.ts index 48c948545..4a5d91f8d 100644 --- a/packages/frontend-2/lib/common/composables/url.ts +++ b/packages/frontend-2/lib/common/composables/url.ts @@ -37,6 +37,7 @@ export function deserializeHashState(hashString: string) { export function useRouteHashState() { const route = useRoute() const router = useRouter() + const { waitUntilReady } = useRouterNavigating() const hashState = writableAsyncComputed({ get: () => { @@ -44,6 +45,8 @@ export function useRouteHashState() { }, set: async (newVal) => { const hashString = serializeHashState(newVal) + + await waitUntilReady() await router.push({ query: route.query, hash: hashString @@ -55,3 +58,21 @@ export function useRouteHashState() { return { hashState } } + +export const useAppUrlUtils = () => { + const { + public: { baseUrl } + } = useRuntimeConfig() + + const buildUrl = (relativeUrl: string | URL): string => { + const url = new URL(relativeUrl, baseUrl) + return decodeURI(url.toString()) // url encoded looks ugly + } + + return { + /** + * Build full/absolute URL + */ + buildUrl + } +} diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts index 462b9fa15..71256676b 100644 --- a/packages/frontend-2/lib/common/generated/gql/gql.ts +++ b/packages/frontend-2/lib/common/generated/gql/gql.ts @@ -418,8 +418,7 @@ type Documents = { "\n mutation CreateCommentThread($input: CreateCommentInput!) {\n commentMutations {\n create(input: $input) {\n ...ViewerCommentThread\n }\n }\n }\n": typeof types.CreateCommentThreadDocument, "\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": typeof types.CreateCommentReplyDocument, "\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": typeof types.ArchiveCommentDocument, - "\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": typeof types.ProjectViewerResourcesDocument, - "\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n": typeof types.ViewerActiveSavedViewDocument, + "\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: ID\n $savedViewSettings: SavedViewsLoadSettings\n ) {\n project(id: $projectId) {\n id\n viewerResources(\n resourceIdString: $resourceUrlString\n savedViewId: $savedViewId\n savedViewSettings: $savedViewSettings\n ) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n savedViewIfExists(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n": typeof types.ProjectViewerResourcesDocument, "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerResourcesLimitAlert_Project\n ...ViewerSavedViewsPanel_Project\n }\n }\n": typeof types.ViewerLoadedResourcesDocument, "\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": typeof types.ViewerModelVersionsDocument, "\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": typeof types.ViewerDiffVersionsDocument, @@ -912,8 +911,7 @@ const documents: Documents = { "\n mutation CreateCommentThread($input: CreateCommentInput!) {\n commentMutations {\n create(input: $input) {\n ...ViewerCommentThread\n }\n }\n }\n": types.CreateCommentThreadDocument, "\n mutation CreateCommentReply($input: CreateCommentReplyInput!) {\n commentMutations {\n reply(input: $input) {\n ...ViewerCommentsReplyItem\n }\n }\n }\n": types.CreateCommentReplyDocument, "\n mutation ArchiveComment($input: ArchiveCommentInput!) {\n commentMutations {\n archive(input: $input)\n }\n }\n": types.ArchiveCommentDocument, - "\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n": types.ProjectViewerResourcesDocument, - "\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n": types.ViewerActiveSavedViewDocument, + "\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: ID\n $savedViewSettings: SavedViewsLoadSettings\n ) {\n project(id: $projectId) {\n id\n viewerResources(\n resourceIdString: $resourceUrlString\n savedViewId: $savedViewId\n savedViewSettings: $savedViewSettings\n ) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n savedViewIfExists(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n": types.ProjectViewerResourcesDocument, "\n query ViewerLoadedResources(\n $projectId: String!\n $modelIds: [String!]!\n $versionIds: [String!]\n ) {\n project(id: $projectId) {\n id\n role\n allowPublicComments\n models(filter: { ids: $modelIds }) {\n totalCount\n items {\n id\n name\n updatedAt\n loadedVersion: versions(\n filter: { priorityIds: $versionIds, priorityIdsOnly: true }\n ) {\n items {\n ...ViewerModelVersionCardItem\n automationsStatus {\n id\n automationRuns {\n ...AutomateViewerPanel_AutomateRun\n }\n }\n }\n }\n versions(limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n ...ProjectPageLatestItemsModels\n ...ModelPageProject\n ...HeaderNavShare_Project\n ...UseCheckViewerCommentingAccess_Project\n ...UseViewerUserActivityBroadcasting_Project\n ...ViewerResourcesLimitAlert_Project\n ...ViewerSavedViewsPanel_Project\n }\n }\n": types.ViewerLoadedResourcesDocument, "\n query ViewerModelVersions(\n $projectId: String!\n $modelId: String!\n $versionsCursor: String\n ) {\n project(id: $projectId) {\n id\n role\n model(id: $modelId) {\n id\n versions(cursor: $versionsCursor, limit: 5) {\n totalCount\n cursor\n items {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n }\n": types.ViewerModelVersionsDocument, "\n query ViewerDiffVersions(\n $projectId: String!\n $modelId: String!\n $versionAId: String!\n $versionBId: String!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n versionA: version(id: $versionAId) {\n ...ViewerModelVersionCardItem\n }\n versionB: version(id: $versionBId) {\n ...ViewerModelVersionCardItem\n }\n }\n }\n }\n": types.ViewerDiffVersionsDocument, @@ -2635,11 +2633,7 @@ export function graphql(source: "\n mutation ArchiveComment($input: ArchiveComm /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n"): (typeof documents)["\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: String\n ) {\n project(id: $projectId) {\n id\n viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n }\n }\n"]; -/** - * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. - */ -export function graphql(source: "\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n"): (typeof documents)["\n query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) {\n project(id: $projectId) {\n id\n savedView(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n"]; +export function graphql(source: "\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: ID\n $savedViewSettings: SavedViewsLoadSettings\n ) {\n project(id: $projectId) {\n id\n viewerResources(\n resourceIdString: $resourceUrlString\n savedViewId: $savedViewId\n savedViewSettings: $savedViewSettings\n ) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n savedViewIfExists(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n"): (typeof documents)["\n query ProjectViewerResources(\n $projectId: String!\n $resourceUrlString: String!\n $savedViewId: ID\n $savedViewSettings: SavedViewsLoadSettings\n ) {\n project(id: $projectId) {\n id\n viewerResources(\n resourceIdString: $resourceUrlString\n savedViewId: $savedViewId\n savedViewSettings: $savedViewSettings\n ) {\n identifier\n items {\n modelId\n versionId\n objectId\n }\n }\n savedViewIfExists(id: $savedViewId) {\n id\n ...UseViewerSavedViewSetup_SavedView\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/frontend-2/lib/common/generated/gql/graphql.ts b/packages/frontend-2/lib/common/generated/gql/graphql.ts index bf4dfd0d4..cf2694043 100644 --- a/packages/frontend-2/lib/common/generated/gql/graphql.ts +++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts @@ -2336,6 +2336,8 @@ export type Project = { savedView: SavedView; savedViewGroup: SavedViewGroup; savedViewGroups: SavedViewGroupCollection; + /** Same as savedView(), but won't throw if view isn't found */ + savedViewIfExists?: Maybe; /** Source apps used in any models of this project */ sourceApps: Array; team: Array; @@ -2478,6 +2480,11 @@ export type ProjectSavedViewGroupsArgs = { }; +export type ProjectSavedViewIfExistsArgs = { + id?: InputMaybe; +}; + + export type ProjectUngroupedViewGroupArgs = { input: GetUngroupedViewGroupInput; }; @@ -2497,7 +2504,8 @@ export type ProjectVersionsArgs = { export type ProjectViewerResourcesArgs = { loadedVersionsOnly?: InputMaybe; resourceIdString: Scalars['String']['input']; - savedViewId?: InputMaybe; + savedViewId?: InputMaybe; + savedViewSettings?: InputMaybe; }; @@ -3524,6 +3532,14 @@ export const SavedViewVisibility = { } as const; export type SavedViewVisibility = typeof SavedViewVisibility[keyof typeof SavedViewVisibility]; +export type SavedViewsLoadSettings = { + /** + * If true, load versions originally specified in the view, rather than the latest ones + * or ones already being loaded otherwise + */ + loadOriginal?: InputMaybe; +}; + /** Available scopes. */ export type Scope = { __typename?: 'Scope'; @@ -7487,19 +7503,12 @@ export type ArchiveCommentMutation = { __typename?: 'Mutation', commentMutations export type ProjectViewerResourcesQueryVariables = Exact<{ projectId: Scalars['String']['input']; resourceUrlString: Scalars['String']['input']; - savedViewId?: InputMaybe; + savedViewId?: InputMaybe; + savedViewSettings?: InputMaybe; }>; -export type ProjectViewerResourcesQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, viewerResources: Array<{ __typename?: 'ViewerResourceGroup', identifier: string, items: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> }> } }; - -export type ViewerActiveSavedViewQueryVariables = Exact<{ - projectId: Scalars['String']['input']; - savedViewId: Scalars['ID']['input']; -}>; - - -export type ViewerActiveSavedViewQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, savedView: { __typename?: 'SavedView', id: string, viewerState: {} } } }; +export type ProjectViewerResourcesQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, viewerResources: Array<{ __typename?: 'ViewerResourceGroup', identifier: string, items: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> }>, savedViewIfExists?: { __typename?: 'SavedView', id: string, viewerState: {} } | null } }; export type ViewerLoadedResourcesQueryVariables = Exact<{ projectId: Scalars['String']['input']; @@ -8425,8 +8434,7 @@ export const MarkCommentViewedDocument = {"kind":"Document","definitions":[{"kin export const CreateCommentThreadDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCommentThread"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCommentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ThreadCommentAttachment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"attachments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fileName"}},{"kind":"Field","name":{"kind":"Name","value":"fileType"}},{"kind":"Field","name":{"kind":"Name","value":"fileSize"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentsReplyItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ThreadCommentAttachment"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FormUsersSelectItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentsListItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"viewedAt"}},{"kind":"Field","name":{"kind":"Name","value":"replies"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsReplyItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"replyAuthors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"4"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"resources"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"resourceId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceType"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentBubblesData"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewedAt"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentThreadData"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canArchive"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentThread"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsListItem"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentBubblesData"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsReplyItem"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThreadData"}}]}}]} as unknown as DocumentNode; export const CreateCommentReplyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCommentReply"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCommentReplyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reply"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentsReplyItem"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ThreadCommentAttachment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"attachments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fileName"}},{"kind":"Field","name":{"kind":"Name","value":"fileType"}},{"kind":"Field","name":{"kind":"Name","value":"fileSize"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerCommentsReplyItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Comment"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"text"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"doc"}}]}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ThreadCommentAttachment"}}]}}]} as unknown as DocumentNode; export const ArchiveCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ArchiveComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ArchiveCommentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"commentMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"archive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]}}]} as unknown as DocumentNode; -export const ProjectViewerResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectViewerResources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"resourceUrlString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"savedViewId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewerResources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"resourceIdString"},"value":{"kind":"Variable","name":{"kind":"Name","value":"resourceUrlString"}}},{"kind":"Argument","name":{"kind":"Name","value":"savedViewId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"savedViewId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"versionId"}},{"kind":"Field","name":{"kind":"Name","value":"objectId"}}]}}]}}]}}]}}]} as unknown as DocumentNode; -export const ViewerActiveSavedViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerActiveSavedView"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"savedViewId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"savedView"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"savedViewId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseViewerSavedViewSetup_SavedView"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseViewerSavedViewSetup_SavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}}]}}]} as unknown as DocumentNode; +export const ProjectViewerResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectViewerResources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"resourceUrlString"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"savedViewId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"savedViewSettings"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewsLoadSettings"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewerResources"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"resourceIdString"},"value":{"kind":"Variable","name":{"kind":"Name","value":"resourceUrlString"}}},{"kind":"Argument","name":{"kind":"Name","value":"savedViewId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"savedViewId"}}},{"kind":"Argument","name":{"kind":"Name","value":"savedViewSettings"},"value":{"kind":"Variable","name":{"kind":"Name","value":"savedViewSettings"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"versionId"}},{"kind":"Field","name":{"kind":"Name","value":"objectId"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"savedViewIfExists"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"savedViewId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseViewerSavedViewSetup_SavedView"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseViewerSavedViewSetup_SavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}}]}}]} as unknown as DocumentNode; export const ViewerLoadedResourcesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedResources"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelIds"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","alias":{"kind":"Name","value":"loadedVersion"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIds"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionIds"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"priorityIdsOnly"},"value":{"kind":"BooleanValue","value":true}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}},{"kind":"Field","name":{"kind":"Name","value":"automationsStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"automationRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateViewerPanel_AutomateRun"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ModelPageProject"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"HeaderNavShare_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseCheckViewerCommentingAccess_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseViewerUserActivityBroadcasting_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerResourcesLimitAlert_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerSavedViewsPanel_Project"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateViewerPanelFunctionRunRow_AutomateFunctionRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateFunctionRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"results"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"statusMessage"}},{"kind":"Field","name":{"kind":"Name","value":"contextView"}},{"kind":"Field","name":{"kind":"Name","value":"function"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"logo"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomationsStatusOrderedRuns_AutomationRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"automation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"FullPermissionCheckResult"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PermissionCheckResult"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"authorized"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"payload"}},{"kind":"Field","name":{"kind":"Name","value":"errorMessage"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectsModelPageEmbed_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateEmbedTokens"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"embedOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hideSpeckleBranding"}}]}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canEditEmbedOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsActions_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsModelPageEmbed_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseFileImport_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectCardImportFileArea_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseFileImport_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseCanCreateModel_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsStructureItem_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsActions_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectCardImportFileArea_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseCanCreateModel_Project"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateModel"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseCanMoveProjectIntoWorkspace_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canMoveToWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspaceMoveProject_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canMoveToWorkspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseCanMoveProjectIntoWorkspace_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelsAdd_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"plan"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseCanCreateModel_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceMoveProject_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"WorkspacePlanLimits_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"plan"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseLoadLatestVersion_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerLimitsWorkspaceDialog_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspacePlanLimits_Workspace"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"UseLoadLatestVersion_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerLimitsDialog_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerLimitsWorkspaceDialog_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceMoveProject_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerResourcesWorkspaceLimitAlert_Workspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerModelVersionCardItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"previewUrl"}},{"kind":"Field","name":{"kind":"Name","value":"authorUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"AutomateViewerPanel_AutomateRun"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AutomateRun"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"functionRuns"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomateViewerPanelFunctionRunRow_AutomateFunctionRun"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"AutomationsStatusOrderedRuns_AutomationRun"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"modelCount"},"name":{"kind":"Name","value":"models"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageModelsStructureItem_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectCardImportFileArea_Project"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectModelsAdd_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ModelPageProject"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}},{"kind":"Field","name":{"kind":"Name","value":"embedOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hideSpeckleBranding"}}]}},{"kind":"Field","name":{"kind":"Name","value":"hasAccessToFeature"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"featureName"},"value":{"kind":"EnumValue","value":"hideSpeckleBranding"}}]},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerLimitsDialog_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"HeaderNavShare_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsModelPageEmbed_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseCheckViewerCommentingAccess_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateComment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"UseViewerUserActivityBroadcasting_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canBroadcastActivity"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerResourcesLimitAlert_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"workspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerResourcesWorkspaceLimitAlert_Workspace"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"WorkspaceMoveProject_Project"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerSavedViewsPanel_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"canCreateSavedView"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FullPermissionCheckResult"}}]}}]}}]}}]} as unknown as DocumentNode; export const ViewerModelVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerModelVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionsCursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"cursor"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerModelVersionCardItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"previewUrl"}},{"kind":"Field","name":{"kind":"Name","value":"authorUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}}]} as unknown as DocumentNode; export const ViewerDiffVersionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerDiffVersions"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"model"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","alias":{"kind":"Name","value":"versionA"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionAId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}},{"kind":"Field","alias":{"kind":"Name","value":"versionB"},"name":{"kind":"Name","value":"version"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"versionBId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerModelVersionCardItem"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LimitedUserAvatar"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LimitedUser"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatar"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ViewerModelVersionCardItem"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Version"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"referencedObject"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApplication"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"previewUrl"}},{"kind":"Field","name":{"kind":"Name","value":"authorUser"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}}]} as unknown as DocumentNode; @@ -9381,6 +9389,7 @@ export type ProjectFieldArgs = { savedView: ProjectSavedViewArgs, savedViewGroup: ProjectSavedViewGroupArgs, savedViewGroups: ProjectSavedViewGroupsArgs, + savedViewIfExists: ProjectSavedViewIfExistsArgs, sourceApps: {}, team: {}, ungroupedViewGroup: ProjectUngroupedViewGroupArgs, diff --git a/packages/frontend-2/lib/common/helpers/route.ts b/packages/frontend-2/lib/common/helpers/route.ts index 7e678a5c6..dee2a27b8 100644 --- a/packages/frontend-2/lib/common/helpers/route.ts +++ b/packages/frontend-2/lib/common/helpers/route.ts @@ -114,6 +114,8 @@ export const modelRoute = ( `/projects/${projectId}/models/${resourceIdString}${ hashState ? serializeHashState(hashState) || '' : '' }` +export const viewerRoute = modelRoute + export const modelVersionsRoute = (projectId: string, modelId: string) => `/projects/${projectId}/models/${modelId}/versions` diff --git a/packages/frontend-2/lib/viewer/composables/savedViews/general.ts b/packages/frontend-2/lib/viewer/composables/savedViews/general.ts index fb62bb0dd..0bbd6b0e0 100644 --- a/packages/frontend-2/lib/viewer/composables/savedViews/general.ts +++ b/packages/frontend-2/lib/viewer/composables/savedViews/general.ts @@ -1,3 +1,14 @@ +import { useAppUrlUtils } from '~/lib/common/composables/url' +import { viewerRoute } from '~/lib/common/helpers/route' +import { useEventBus } from '~/lib/core/composables/eventBus' +import { useInjectedViewerState } from '~/lib/viewer/composables/setup' +import { ViewerHashStateKeys } from '~/lib/viewer/composables/setup/urlHashState' +import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus' +import { + serializeSavedViewUrlSettings, + type SavedViewUrlSettings +} from '~/lib/viewer/helpers/savedViews' + export const useAreSavedViewsEnabled = () => { const { public: { FF_SAVED_VIEWS_ENABLED } @@ -5,3 +16,36 @@ export const useAreSavedViewsEnabled = () => { return FF_SAVED_VIEWS_ENABLED } + +export const useViewerSavedViewsUtils = () => { + const { + projectId, + resources: { + request: { resourceIdString } + } + } = useInjectedViewerState() + const { copy } = useClipboard() + const { buildUrl } = useAppUrlUtils() + const eventBus = useEventBus() + + const copyLink = async (params: { settings: SavedViewUrlSettings }) => { + const { settings } = params + const relativeUrl = viewerRoute(projectId.value, resourceIdString.value, { + [ViewerHashStateKeys.SavedView]: serializeSavedViewUrlSettings(settings) + }) + await copy(buildUrl(relativeUrl), { + successMessage: 'Copied link to view' + }) + } + + const applyView = (settings: SavedViewUrlSettings) => { + // Force update, even if the view id is already set + // (in case this is a frustration click w/ the state not applying) + eventBus.emit(ViewerEventBusKeys.UpdateSavedView, settings) + } + + return { + copyLink, + applyView + } +} diff --git a/packages/frontend-2/lib/viewer/composables/serialization.ts b/packages/frontend-2/lib/viewer/composables/serialization.ts index 6c64c2efd..bcfc14111 100644 --- a/packages/frontend-2/lib/viewer/composables/serialization.ts +++ b/packages/frontend-2/lib/viewer/composables/serialization.ts @@ -12,9 +12,14 @@ import { } from '~~/lib/viewer/composables/ui' import { CameraController, ViewMode, VisualDiffMode } from '@speckle/viewer' import type { NumericPropertyInfo } from '@speckle/viewer' -import type { PartialDeep } from 'type-fest' +import type { Merge, PartialDeep } from 'type-fest' import type { SectionBoxData } from '@speckle/shared/viewer/state' import { useViewerRealtimeActivityTracker } from '~/lib/viewer/composables/activity' +import { + isModelResource, + resourceBuilder, + type ViewerResource +} from '@speckle/shared/viewer/route' type SerializedViewerState = SpeckleViewer.ViewerState.SerializedViewerState @@ -138,6 +143,13 @@ export enum StateApplyMode { SavedView } +export type StateApplyOptions = Merge< + Record, + { + [StateApplyMode.SavedView]: { loadOriginal: boolean } + } +> + export function useApplySerializedState() { const { projectId, @@ -171,7 +183,11 @@ export function useApplySerializedState() { const logger = useLogger() const { update } = useViewerRealtimeActivityTracker() - return async (state: PartialDeep, mode: StateApplyMode) => { + return async ( + state: PartialDeep, + mode: Mode, + options?: StateApplyOptions[Mode] + ) => { if (mode === StateApplyMode.Reset) { resetState() update() // Trigger activity update @@ -186,6 +202,34 @@ export function useApplySerializedState() { [StateApplyMode.Spotlight, StateApplyMode.ThreadFullContextOpen].includes(mode) ) { await resourceIdString.update(state.resources?.request?.resourceIdString || '') + } else if (mode === StateApplyMode.SavedView) { + const { loadOriginal } = options || {} + + const current = resourceBuilder().addResources(resourceIdString.value) + const incoming = resourceBuilder().addResources( + state.resources?.request?.resourceIdString || '' + ) + + const finalItems: ViewerResource[] = [] + for (const incomingItem of incoming) { + if (!isModelResource(incomingItem)) { + finalItems.push(incomingItem) + continue + } + + // Update versionId based on loadOriginal + incomingItem.versionId = loadOriginal + ? incomingItem.versionId + : current + .filter(isModelResource) + .find((r) => r.modelId === incomingItem.modelId)?.versionId + finalItems.push(incomingItem) + } + const newResourceIdString = resourceBuilder() + .addResources(finalItems) + .addNew(current) // keeping other federated models around + .toString() + await resourceIdString.update(newResourceIdString) } position.value = new Vector3( diff --git a/packages/frontend-2/lib/viewer/composables/setup.ts b/packages/frontend-2/lib/viewer/composables/setup.ts index 548fda09b..90471589b 100644 --- a/packages/frontend-2/lib/viewer/composables/setup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup.ts @@ -26,7 +26,6 @@ import { isNonNullable } from '@speckle/shared' import { useApolloClient, useLazyQuery, useQuery } from '@vue/apollo-composable' import { projectViewerResourcesQuery, - viewerActiveSavedViewQuery, viewerLoadedResourcesQuery, viewerLoadedThreadsQuery, viewerModelVersionsQuery @@ -79,7 +78,7 @@ import { ViewerModelResource, type ViewerResource } from '@speckle/shared/viewer/route' -import { useAreSavedViewsEnabled } from '~/lib/viewer/composables/savedViews/general' +import type { SavedViewUrlSettings } from '~/lib/viewer/helpers/savedViews' export type LoadedModel = NonNullable< Get @@ -98,11 +97,6 @@ export type InjectableViewerState = Readonly<{ * The project which we're opening in the viewer (all loaded models should belong to it) */ projectId: AsyncWritableComputedRef - /** - * Core source of truth for the view id (other is in hash state). This allows you to - * set a view to load, without it showing up in the URL. - */ - savedViewId: Ref> /** * User viewer session ID. The same user will have different IDs in different tabs if multiple are open. * This is used to ignore user activity messages from the same tab. @@ -153,6 +147,17 @@ export type InjectableViewerState = Readonly<{ * State of resource identifiers that should be loaded (tied to the URL param) */ request: { + /** + * Saved view parameters, that affect what resources we're loading and how + */ + savedView: { + id: Ref> + /** + * By default we use latest or already loaded versions, but this allows + * us to load the versions originally specified when creating the view + */ + loadOriginal: Ref + } /** * All currently requested identifiers. You * can write to this to change which resources should be loaded. @@ -173,19 +178,12 @@ export type InjectableViewerState = Readonly<{ * Helper for switching model to a specific version (or just latest) */ switchModelToVersion: (modelId: string, versionId?: string) => Promise - // addModelVersion: (modelId: string, versionId: string) => void - // removeModelVersion: (modelId: string, versionId?: string) => void - // setModelVersions: (newResources: ViewerResource[]) => void } /** * State of resolved, validated & de-duplicated resources that are loaded in the viewer. These * are resolved from multiple GQL requests and update whenever resources.request updates. */ response: { - /** - * Resource id string w/ saved view applied, if any - */ - resolvedResourceIdString: ComputedRef /** * Metadata about loaded items */ @@ -319,7 +317,11 @@ export type InjectableViewerState = Readonly<{ urlHashState: { focusedThreadId: AsyncWritableComputedRef> diff: AsyncWritableComputedRef> - savedViewId: AsyncWritableComputedRef> + /** + * Core source of truth is under `resources.request.savedView`, but this allows + * the saved view settings to be URL controlled + */ + savedView: AsyncWritableComputedRef> } }> @@ -332,7 +334,7 @@ type CachedViewerState = Pick< type InitialSetupState = Pick< InjectableViewerState, - 'projectId' | 'viewer' | 'sessionId' | 'urlHashState' | 'savedViewId' + 'projectId' | 'viewer' | 'sessionId' | 'urlHashState' > type InitialStateWithRequest = InitialSetupState & { @@ -440,7 +442,6 @@ function setupInitialState(params: UseSetupViewerParams): InitialSetupState { return { projectId: params.projectId, - savedViewId: ref(null), sessionId, viewer: import.meta.server ? ({ @@ -478,12 +479,15 @@ function setupInitialState(params: UseSetupViewerParams): InitialSetupState { function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest { const route = useRoute() const router = useRouter() + const { waitUntilReady } = useRouterNavigating() const getParam = computed(() => route.params.modelId as string) const resources = writableAsyncComputed({ get: () => parseUrlParameters(getParam.value), set: async (newResources) => { const modelId = createGetParamFromResources(newResources) + + await waitUntilReady() await router.push({ params: { modelId }, query: route.query, @@ -553,6 +557,10 @@ function setupResourceRequest(state: InitialSetupState): InitialStateWithRequest ...state, resources: { request: { + savedView: { + id: ref(null), + loadOriginal: ref(false) + }, items: resources, resourceIdString, threadFilters, @@ -570,21 +578,29 @@ function setupResponseResourceItems( state: InitialStateWithRequest ): Pick< InjectableViewerState['resources']['response'], - | 'resourceItems' - | 'resourceItemsQueryVariables' - | 'resourceItemsLoaded' - | 'resolvedResourceIdString' + 'resourceItems' | 'resourceItemsQueryVariables' | 'resourceItemsLoaded' | 'savedView' > { const globalError = useError() const { projectId, - savedViewId, resources: { - request: { resourceIdString } + request: { + resourceIdString, + savedView: { id: savedViewId, loadOriginal } + } } } = state const initLoadDone = ref(import.meta.server ? false : true) + + /** + * Resolves actual resources to load: + * - Viewer Resource Groups and items + * - Saved View that was used, if any + * + * Both must be loaded together to avoid race conditions. They both change + * what exactly ends up being loaded, so its important they're in sync. + */ const { result: resolvedResourcesResult, variables: resourceItemsQueryVariables, @@ -595,7 +611,10 @@ function setupResponseResourceItems( () => ({ projectId: projectId.value, resourceUrlString: resourceIdString.value, - savedViewId: savedViewId.value + savedViewId: savedViewId.value, + savedViewSettings: { + loadOriginal: loadOriginal.value + } }), { keepPreviousResult: true } ) @@ -680,18 +699,19 @@ function setupResponseResourceItems( const resourceItemsLoaded = computed(() => initLoadDone.value) - const resolvedResourceIdString = computed(() => - resourceBuilder() - // Combined group identifiers should result in the final resource id string - .addFromString(resolvedResourceGroups.value.map((group) => group.identifier)) - .toString() + // Shows only the one matching the savedViewId. If the query is still loading/stale, it will return undefined + const savedView = computed(() => + savedViewId.value && + resolvedResourcesResult.value?.project?.savedViewIfExists?.id === savedViewId.value + ? resolvedResourcesResult.value?.project?.savedViewIfExists + : undefined ) return { resourceItems, resourceItemsQueryVariables: computed(() => resourceItemsQueryVariables.value), resourceItemsLoaded, - resolvedResourceIdString + savedView } } @@ -700,19 +720,14 @@ function setupResponseResourceData( resourceItemsData: ReturnType ): Omit< InjectableViewerState['resources']['response'], - | 'resourceItems' - | 'resourceItemsQueryVariables' - | 'resourceItemsLoaded' - | 'resolvedResourceIdString' + 'resourceItems' | 'resourceItemsQueryVariables' | 'resourceItemsLoaded' | 'savedView' > { const apollo = useApolloClient().client const globalError = useError() const { triggerNotification } = useGlobalToast() const logger = useLogger() - const savedViewsEnabled = useAreSavedViewsEnabled() const { - savedViewId, projectId, resources: { request: { resourceIdString, threadFilters } @@ -915,36 +930,6 @@ function setupResponseResourceData( logger.error(err) }) - // SAVED VIEW - const { result: viewerActiveSavedViewResult, onError: onViewerActiveSavedViewError } = - useQuery( - viewerActiveSavedViewQuery, - () => ({ - projectId: projectId.value, - savedViewId: savedViewId.value! - }), - { - enabled: computed(() => !!savedViewId.value && savedViewsEnabled) - } - ) - - onViewerActiveSavedViewError((err) => { - triggerNotification({ - type: ToastNotificationType.Danger, - title: 'Saved view loading failed', - description: `${err.message}` - }) - logger.error(err) - }) - - // Shows only the one matching the savedViewId. If the query is still loading/stale, it will return undefined - const savedView = computed(() => - savedViewId.value && - viewerActiveSavedViewResult.value?.project?.savedView.id === savedViewId.value - ? viewerActiveSavedViewResult.value?.project?.savedView - : undefined - ) - onServerPrefetch(async () => { await Promise.all([serverResourcesLoadedPromise.promise]) }) @@ -960,8 +945,7 @@ function setupResponseResourceData( threadsQueryVariables: computed(() => threadsQueryVariables.value), loadMoreVersions, resourcesLoaded: computed(() => initLoadDone.value), - resourcesLoading: computed(() => viewerLoadedResourcesLoading.value), - savedView + resourcesLoading: computed(() => viewerLoadedResourcesLoading.value) } } diff --git a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts index f37d102d5..fb08b8e69 100644 --- a/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts +++ b/packages/frontend-2/lib/viewer/composables/setup/postSetup.ts @@ -61,8 +61,7 @@ import { useEmbed } from '~/lib/viewer/composables/setup/embed' import { useMixpanel } from '~~/lib/core/composables/mp' import { isSerializedViewerState, - type SectionBoxData, - type SerializedViewerState + type SectionBoxData } from '@speckle/shared/viewer/state' import { graphql } from '~/lib/common/generated/gql' import { @@ -70,10 +69,10 @@ import { useApplySerializedState } from '~/lib/viewer/composables/serialization' import { useViewerRealtimeActivityTracker } from '~/lib/viewer/composables/activity' -import { resourceBuilder } from '@speckle/shared/viewer/route' import { useEventBus } from '~/lib/core/composables/eventBus' import { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus' import { useTreeManagement } from '~~/lib/viewer/composables/tree' +import type { SavedViewUrlSettings } from '~/lib/viewer/helpers/savedViews' function useViewerLoadCompleteEventHandler() { const state = useInjectedViewerState() @@ -938,12 +937,13 @@ graphql(` const useViewerSavedViewSetup = () => { const { - savedViewId, resources: { - request: { resourceIdString }, - response: { savedView, resolvedResourceIdString } + request: { + savedView: { id: savedViewId, loadOriginal } + }, + response: { savedView } }, - urlHashState: { savedViewId: urlHashSavedViewId } + urlHashState: { savedView: urlHashStateSavedViewSettings } } = useInjectedViewerState() const applyState = useApplySerializedState() const { serializedStateId } = useViewerRealtimeActivityTracker() @@ -955,68 +955,76 @@ const useViewerSavedViewSetup = () => { const validState = (state: unknown) => (isSerializedViewerState(state) ? state : null) - const apply = async (state: SerializedViewerState) => { - // Combine resolved w/ old, resolved taking precedence - we dont want to unload - // other federated resources that are not a part of the saved view - const combinedIdString = resourceBuilder() - .addResources(resolvedResourceIdString.value) - .addNew(resourceIdString.value) - .toString() + const apply = async () => { + const state = validState(savedView.value?.viewerState) + if (!state) return - await resourceIdString.update(combinedIdString) - await applyState(state, StateApplyMode.SavedView) + await applyState(state, StateApplyMode.SavedView, { + loadOriginal: loadOriginal.value + }) savedViewStateId.value = serializedStateId.value } - const update = (params: { viewId?: string }) => { + const update = async (params: { settings: SavedViewUrlSettings }) => { + const { settings } = params + + let reapplyState = true + // If passing in viewId and it differs, apply and wait for that to finish - if (params.viewId && params.viewId !== savedViewId.value) { - savedViewId.value = params.viewId - return + if (settings.id && settings.id !== savedViewId.value) { + // wipe hash state, if any exists, otherwise the state will be stale + await resetUrlHashState() + savedViewId.value = settings.id + reapplyState = false } - // Re-apply current state - const state = validState(savedView.value?.viewerState) - if (!state) return - apply(state) + // If changing loadOriginal value, apply and wait for that to finish + if ((settings.loadOriginal || false) !== loadOriginal.value) { + loadOriginal.value = settings.loadOriginal || false + } + + // Re-apply current state, if queued + if (reapplyState && settings.id === savedViewId.value) { + const state = validState(savedView.value?.viewerState) + if (!state) return + await apply() + } + } + + const resetUrlHashState = async () => { + await urlHashStateSavedViewSettings.update(null) + } + + const reset = async () => { + savedViewId.value = null + loadOriginal.value = false + savedViewStateId.value = undefined + await resetUrlHashState() } // Allow force update - on(ViewerEventBusKeys.UpdateSavedView, (params) => { - update(params) + on(ViewerEventBusKeys.UpdateSavedView, async (settings) => { + await update({ settings }) }) // Apply saved view state on initial load useOnViewerLoadComplete(async ({ isInitial }) => { - const state = validState(savedView.value?.viewerState) - - if (isInitial && state) { - await apply(state) + if (isInitial) { + await apply() } }) // Saved view changed, apply - watch(savedView, (newVal, oldVal) => { + watch(savedView, async (newVal, oldVal) => { if (!newVal || newVal.id === oldVal?.id) return const state = validState(newVal.viewerState) if (!state) return // If the saved view has changed, apply it - apply(state) + await apply() }) - // If the URL hash saved view ID has changed, update the saved view ID - watch( - urlHashSavedViewId, - async (newVal, oldVal) => { - if (newVal === oldVal) return - - savedViewId.value = newVal - }, - { immediate: true } - ) - // Did state change after applying saved view? Undo view watch( serializedStateId, @@ -1025,9 +1033,22 @@ const useViewerSavedViewSetup = () => { // If the saved view state ID is different from the current serialized state ID, reset the saved view if (savedViewStateId.value && newVal !== savedViewStateId.value) { - savedViewId.value = null - void urlHashSavedViewId.update(null) - savedViewStateId.value = undefined + void reset() + } + }, + { immediate: true } + ) + + // Url hash state -> core source of truth sync + watch( + urlHashStateSavedViewSettings, + async (newVal) => { + if ((newVal?.id || null) !== savedViewId.value) { + savedViewId.value = newVal?.id || null + } + + if ((newVal?.loadOriginal || false) !== loadOriginal.value) { + loadOriginal.value = newVal?.loadOriginal || false } }, { immediate: true } diff --git a/packages/frontend-2/lib/viewer/composables/setup/urlHashState.ts b/packages/frontend-2/lib/viewer/composables/setup/urlHashState.ts index ceaf1807b..13549dd04 100644 --- a/packages/frontend-2/lib/viewer/composables/setup/urlHashState.ts +++ b/packages/frontend-2/lib/viewer/composables/setup/urlHashState.ts @@ -1,3 +1,9 @@ +import type { Nullable } from '@speckle/shared' +import { + parseSavedViewUrlSettings, + serializeSavedViewUrlSettings, + type SavedViewUrlSettings +} from '~/lib/viewer/helpers/savedViews' import { writableAsyncComputed } from '~~/lib/common/composables/async' import { useRouteHashState } from '~~/lib/common/composables/url' import type { InjectableViewerState } from '~~/lib/viewer/composables/setup' @@ -7,7 +13,7 @@ export enum ViewerHashStateKeys { FocusedThreadId = 'threadId', Diff = 'diff', EmbedOptions = 'embed', - SavedViewId = 'savedViewId' + SavedView = 'savedView' } export function setupUrlHashState(): InjectableViewerState['urlHashState'] { @@ -41,12 +47,16 @@ export function setupUrlHashState(): InjectableViewerState['urlHashState'] { asyncRead: false }) - const savedViewId = writableAsyncComputed({ - get: () => hashState.value[ViewerHashStateKeys.SavedViewId] || null, + const savedView = writableAsyncComputed>({ + get: () => { + const urlVal = hashState.value[ViewerHashStateKeys.SavedView] + return parseSavedViewUrlSettings(urlVal) + }, set: async (newVal) => { + const serialized = newVal ? serializeSavedViewUrlSettings(newVal) : null await hashState.update({ ...hashState.value, - [ViewerHashStateKeys.SavedViewId]: newVal + [ViewerHashStateKeys.SavedView]: serialized }) }, initialState: null, @@ -56,6 +66,6 @@ export function setupUrlHashState(): InjectableViewerState['urlHashState'] { return { focusedThreadId, diff, - savedViewId + savedView } } diff --git a/packages/frontend-2/lib/viewer/graphql/queries.ts b/packages/frontend-2/lib/viewer/graphql/queries.ts index bbebc0bf5..8d3334bda 100644 --- a/packages/frontend-2/lib/viewer/graphql/queries.ts +++ b/packages/frontend-2/lib/viewer/graphql/queries.ts @@ -4,11 +4,16 @@ export const projectViewerResourcesQuery = graphql(` query ProjectViewerResources( $projectId: String! $resourceUrlString: String! - $savedViewId: String + $savedViewId: ID + $savedViewSettings: SavedViewsLoadSettings ) { project(id: $projectId) { id - viewerResources(resourceIdString: $resourceUrlString, savedViewId: $savedViewId) { + viewerResources( + resourceIdString: $resourceUrlString + savedViewId: $savedViewId + savedViewSettings: $savedViewSettings + ) { identifier items { modelId @@ -16,15 +21,7 @@ export const projectViewerResourcesQuery = graphql(` objectId } } - } - } -`) - -export const viewerActiveSavedViewQuery = graphql(` - query ViewerActiveSavedView($projectId: String!, $savedViewId: ID!) { - project(id: $projectId) { - id - savedView(id: $savedViewId) { + savedViewIfExists(id: $savedViewId) { id ...UseViewerSavedViewSetup_SavedView } diff --git a/packages/frontend-2/lib/viewer/helpers/eventBus.ts b/packages/frontend-2/lib/viewer/helpers/eventBus.ts index 6045fce66..7a27dd18c 100644 --- a/packages/frontend-2/lib/viewer/helpers/eventBus.ts +++ b/packages/frontend-2/lib/viewer/helpers/eventBus.ts @@ -1,8 +1,10 @@ +import type { ViewerSavedViewEventBusPayloads } from '~/lib/viewer/helpers/savedViews' + export enum ViewerEventBusKeys { UpdateSavedView = 'aaa' } // Add mappings between event keys and expected payloads here export type ViewerEventBusKeyPayloadMap = { - [ViewerEventBusKeys.UpdateSavedView]: { viewId?: string } + [ViewerEventBusKeys.UpdateSavedView]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.UpdateSavedView] } & { [k in ViewerEventBusKeys]: unknown } & Record diff --git a/packages/frontend-2/lib/viewer/helpers/savedViews.ts b/packages/frontend-2/lib/viewer/helpers/savedViews.ts index 875b5ac2a..9dc9fe50f 100644 --- a/packages/frontend-2/lib/viewer/helpers/savedViews.ts +++ b/packages/frontend-2/lib/viewer/helpers/savedViews.ts @@ -1,4 +1,6 @@ import type { StringEnumValues } from '@speckle/shared' +import { isObjectLike, isString } from 'lodash-es' +import type { ViewerEventBusKeys } from '~/lib/viewer/helpers/eventBus' export const ViewsType = { All: 'all-views', @@ -12,3 +14,38 @@ export const viewsTypeLabels: Record = { [ViewsType.My]: 'My Views', [ViewsType.Connector]: 'From connectors' } + +/** + * Url hash state struct for saved views + */ +export type SavedViewUrlSettings = { + id: string + loadOriginal?: boolean +} + +export type ViewerSavedViewEventBusPayloads = { + [ViewerEventBusKeys.UpdateSavedView]: SavedViewUrlSettings +} + +export const parseSavedViewUrlSettings = ( + settingsString: string | null +): SavedViewUrlSettings | null => { + if (!settingsString) return null + + try { + const parsed = JSON.parse(settingsString) + if (isObjectLike(parsed) && isString(parsed.id)) { + return parsed as SavedViewUrlSettings + } + } catch { + // suppress + } + + return null +} + +export const serializeSavedViewUrlSettings = ( + settings: SavedViewUrlSettings +): string => { + return JSON.stringify(settings) +} diff --git a/packages/frontend-2/plugins/021-url.ts b/packages/frontend-2/plugins/021-url.ts new file mode 100644 index 000000000..79dde72e9 --- /dev/null +++ b/packages/frontend-2/plugins/021-url.ts @@ -0,0 +1,43 @@ +import { until } from '@vueuse/core' + +/** + * Global state for if vue-router is in a pending navigation + * (helps avoid race conditions in environments w/ many concurrent navigations like the viewer) + */ +export default defineNuxtPlugin(() => { + const router = useRouter() + const route = useRoute() + const logger = useLogger() + + const isNavigating = ref(false) + + // Only drive on client + if (import.meta.client) { + router.beforeEach(() => { + isNavigating.value = true + }) + router.afterEach(async (to) => { + const newPath = to.fullPath + + try { + await until(() => route.fullPath === newPath).toBeTruthy({ + timeout: 500, + throwOnTimeout: true + }) + } catch (e) { + logger.warn(e, 'Waiting for navigation to finalize failed') + } + + isNavigating.value = false + }) + router.onError(() => { + isNavigating.value = false + }) + } + + return { + provide: { + isNavigating: computed(() => isNavigating.value) + } + } +}) diff --git a/packages/server/assets/core/typedefs/modelsAndVersions.graphql b/packages/server/assets/core/typedefs/modelsAndVersions.graphql index 52a6cba26..60e5f5601 100644 --- a/packages/server/assets/core/typedefs/modelsAndVersions.graphql +++ b/packages/server/assets/core/typedefs/modelsAndVersions.graphql @@ -25,17 +25,6 @@ extend type Project { """ modelChildrenTree(fullName: String!): [ModelsTreeItem!]! """ - Return metadata about resources being requested in the viewer - """ - viewerResources( - resourceIdString: String! - """ - If a saved view ID is specified, the returned resources will be adjusted to return the view's resources instead - """ - savedViewId: String - loadedVersionsOnly: Boolean = true - ): [ViewerResourceGroup!]! - """ Returns a flat list of all project versions """ versions(limit: Int! = 25, cursor: String): VersionCollection! @@ -77,29 +66,6 @@ input ProjectModelsTreeFilter { contributors: [String!] } -type ViewerResourceGroup { - """ - Resource identifier used to refer to a collection of resource items - """ - identifier: String! - """ - Viewer resources that the identifier refers to - """ - items: [ViewerResourceItem!]! -} - -type ViewerResourceItem { - """ - Null if resource represents an object - """ - modelId: String - """ - Null if resource represents an object - """ - versionId: String - objectId: String! -} - type Model { id: ID! """ diff --git a/packages/server/assets/viewer/typedefs/savedViews.graphql b/packages/server/assets/viewer/typedefs/savedViews.graphql index 90b103688..618f2459e 100644 --- a/packages/server/assets/viewer/typedefs/savedViews.graphql +++ b/packages/server/assets/viewer/typedefs/savedViews.graphql @@ -125,6 +125,10 @@ extend type Project { savedViewGroup(id: ID!): SavedViewGroup! ungroupedViewGroup(input: GetUngroupedViewGroupInput!): SavedViewGroup! savedView(id: ID!): SavedView! + """ + Same as savedView(), but won't throw if view isn't found + """ + savedViewIfExists(id: ID): SavedView } input CreateSavedViewInput { diff --git a/packages/server/assets/viewer/typedefs/viewerResources.graphql b/packages/server/assets/viewer/typedefs/viewerResources.graphql new file mode 100644 index 000000000..64c313f8b --- /dev/null +++ b/packages/server/assets/viewer/typedefs/viewerResources.graphql @@ -0,0 +1,45 @@ +input SavedViewsLoadSettings { + """ + If true, load versions originally specified in the view, rather than the latest ones + or ones already being loaded otherwise + """ + loadOriginal: Boolean +} + +type ViewerResourceGroup { + """ + Resource identifier used to refer to a collection of resource items + """ + identifier: String! + """ + Viewer resources that the identifier refers to + """ + items: [ViewerResourceItem!]! +} + +type ViewerResourceItem { + """ + Null if resource represents an object + """ + modelId: String + """ + Null if resource represents an object + """ + versionId: String + objectId: String! +} + +extend type Project { + """ + Return metadata about resources being requested in the viewer + """ + viewerResources( + resourceIdString: String! + """ + If a saved view ID is specified, the returned resources will be adjusted to return the view's resources instead + """ + savedViewId: ID + savedViewSettings: SavedViewsLoadSettings + loadedVersionsOnly: Boolean = true + ): [ViewerResourceGroup!]! +} diff --git a/packages/server/modules/cli/commands/download/commit.ts b/packages/server/modules/cli/commands/download/commit.ts index 870857b93..6e62dbd58 100644 --- a/packages/server/modules/cli/commands/download/commit.ts +++ b/packages/server/modules/cli/commands/download/commit.ts @@ -56,6 +56,7 @@ import { getViewerResourceGroupsFactory, getViewerResourceItemsUngroupedFactory } from '@/modules/viewer/services/viewerResources' +import { getSavedViewFactory } from '@/modules/viewer/repositories/savedViews' const command: CommandModule< unknown, @@ -118,7 +119,8 @@ const command: CommandModule< getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDb }), getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDb }), getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb }), - getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }) + getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }), + getSavedView: getSavedViewFactory({ db: projectDb }) }) }) const getViewerResourcesFromLegacyIdentifiers = diff --git a/packages/server/modules/cli/commands/download/project.ts b/packages/server/modules/cli/commands/download/project.ts index 49e043618..48801dbb5 100644 --- a/packages/server/modules/cli/commands/download/project.ts +++ b/packages/server/modules/cli/commands/download/project.ts @@ -73,6 +73,7 @@ import { getViewerResourceGroupsFactory, getViewerResourceItemsUngroupedFactory } from '@/modules/viewer/services/viewerResources' +import { getSavedViewFactory } from '@/modules/viewer/repositories/savedViews' const command: CommandModule< unknown, @@ -146,7 +147,8 @@ const command: CommandModule< getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDb }), getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDb }), getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb }), - getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }) + getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }), + getSavedView: getSavedViewFactory({ db: projectDb }) }) }) const getViewerResourcesFromLegacyIdentifiers = diff --git a/packages/server/modules/comments/graph/resolvers/comments.ts b/packages/server/modules/comments/graph/resolvers/comments.ts index 456c74c46..46d00a9c1 100644 --- a/packages/server/modules/comments/graph/resolvers/comments.ts +++ b/packages/server/modules/comments/graph/resolvers/comments.ts @@ -96,6 +96,8 @@ import { getViewerResourceGroupsFactory, getViewerResourceItemsUngroupedFactory } from '@/modules/viewer/services/viewerResources' +import type { RequestDataLoaders } from '@/modules/core/loaders' +import { getSavedViewFactory } from '@/modules/viewer/repositories/dataLoaders/savedViews' // We can use the main DB for these const getStream = getStreamFactory({ db }) @@ -114,7 +116,10 @@ const buildGetViewerResourcesFromLegacyIdentifiers = (deps: { db: Knex }) => { return getViewerResourcesFromLegacyIdentifiers } -const buildGetViewerResourceItemsUngrouped = (deps: { db: Knex }) => +const buildGetViewerResourceItemsUngrouped = (deps: { + db: Knex + loaders: RequestDataLoaders +}) => getViewerResourceItemsUngroupedFactory({ getViewerResourceGroups: getViewerResourceGroupsFactory({ getStreamObjects: getStreamObjectsFactory(deps), @@ -122,7 +127,8 @@ const buildGetViewerResourceItemsUngrouped = (deps: { db: Knex }) => getStreamBranchesByName: getStreamBranchesByNameFactory(deps), getSpecificBranchCommits: getSpecificBranchCommitsFactory(deps), getAllBranchCommits: getAllBranchCommitsFactory(deps), - getBranchesByIds: getBranchesByIdsFactory(deps) + getBranchesByIds: getBranchesByIdsFactory(deps), + getSavedView: getSavedViewFactory(deps) }) }) @@ -540,7 +546,8 @@ export default { const projectDb = await getProjectDbClient({ projectId }) const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({ - db: projectDb + db: projectDb, + loaders: ctx.loaders }) const validateInputAttachments = validateInputAttachmentsFactory({ @@ -709,7 +716,8 @@ export default { const projectDb = await getProjectDbClient({ projectId }) const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({ - db: projectDb + db: projectDb, + loaders: context.loaders }) await publish(ViewerSubscriptions.UserActivityBroadcasted, { @@ -1058,7 +1066,8 @@ export default { const projectDb = await getProjectDbClient({ projectId: payload.projectId }) const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({ - db: projectDb + db: projectDb, + loaders: context.loaders }) const requestedResourceItems = await getViewerResourceItemsUngrouped(target) @@ -1094,7 +1103,8 @@ export default { const projectDb = await getProjectDbClient({ projectId: payload.projectId }) const getViewerResourceItemsUngrouped = buildGetViewerResourceItemsUngrouped({ - db: projectDb + db: projectDb, + loaders: context.loaders }) const requestedResourceItems = await getViewerResourceItemsUngrouped(target) diff --git a/packages/server/modules/core/graph/generated/graphql.ts b/packages/server/modules/core/graph/generated/graphql.ts index 3f23989bb..79222e8f8 100644 --- a/packages/server/modules/core/graph/generated/graphql.ts +++ b/packages/server/modules/core/graph/generated/graphql.ts @@ -2362,6 +2362,8 @@ export type Project = { savedView: SavedView; savedViewGroup: SavedViewGroup; savedViewGroups: SavedViewGroupCollection; + /** Same as savedView(), but won't throw if view isn't found */ + savedViewIfExists?: Maybe; /** Source apps used in any models of this project */ sourceApps: Array; team: Array; @@ -2504,6 +2506,11 @@ export type ProjectSavedViewGroupsArgs = { }; +export type ProjectSavedViewIfExistsArgs = { + id?: InputMaybe; +}; + + export type ProjectUngroupedViewGroupArgs = { input: GetUngroupedViewGroupInput; }; @@ -2523,7 +2530,8 @@ export type ProjectVersionsArgs = { export type ProjectViewerResourcesArgs = { loadedVersionsOnly?: InputMaybe; resourceIdString: Scalars['String']['input']; - savedViewId?: InputMaybe; + savedViewId?: InputMaybe; + savedViewSettings?: InputMaybe; }; @@ -3550,6 +3558,14 @@ export const SavedViewVisibility = { } as const; export type SavedViewVisibility = typeof SavedViewVisibility[keyof typeof SavedViewVisibility]; +export type SavedViewsLoadSettings = { + /** + * If true, load versions originally specified in the view, rather than the latest ones + * or ones already being loaded otherwise + */ + loadOriginal?: InputMaybe; +}; + /** Available scopes. */ export type Scope = { __typename?: 'Scope'; @@ -6027,6 +6043,7 @@ export type ResolversTypes = { SavedViewMutations: ResolverTypeWrapper; SavedViewPermissionChecks: ResolverTypeWrapper; SavedViewVisibility: SavedViewVisibility; + SavedViewsLoadSettings: SavedViewsLoadSettings; Scope: ResolverTypeWrapper; ServerApp: ResolverTypeWrapper; ServerAppListItem: ResolverTypeWrapper; @@ -6384,6 +6401,7 @@ export type ResolversParentTypes = { SavedViewGroupsInput: SavedViewGroupsInput; SavedViewMutations: MutationsObjectGraphQLReturn; SavedViewPermissionChecks: SavedViewPermissionChecksGraphQLReturn; + SavedViewsLoadSettings: SavedViewsLoadSettings; Scope: Scope; ServerApp: ServerAppGraphQLReturn; ServerAppListItem: ServerAppListItemGraphQLReturn; @@ -7428,6 +7446,7 @@ export type ProjectResolvers>; savedViewGroup?: Resolver>; savedViewGroups?: Resolver>; + savedViewIfExists?: Resolver, ParentType, ContextType, Partial>; sourceApps?: Resolver, ParentType, ContextType>; team?: Resolver, ParentType, ContextType>; ungroupedViewGroup?: Resolver>; diff --git a/packages/server/modules/core/graph/resolvers/models.ts b/packages/server/modules/core/graph/resolvers/models.ts index 1d56821d4..b14c9d70b 100644 --- a/packages/server/modules/core/graph/resolvers/models.ts +++ b/packages/server/modules/core/graph/resolvers/models.ts @@ -22,8 +22,6 @@ import { createBranchFactory, deleteBranchByIdFactory, getBranchByIdFactory, - getBranchesByIdsFactory, - getBranchLatestCommitsFactory, getModelTreeItemsFactory, getModelTreeItemsFilteredFactory, getModelTreeItemsFilteredTotalCountFactory, @@ -31,14 +29,11 @@ import { getPaginatedProjectModelsItemsFactory, getPaginatedProjectModelsTotalCountFactory, getStreamBranchByNameFactory, - getStreamBranchesByNameFactory, updateBranchFactory } from '@/modules/core/repositories/branches' import { BranchNotFoundError } from '@/modules/core/errors/branch' import { CommitNotFoundError } from '@/modules/core/errors/commit' -import { getStreamObjectsFactory } from '@/modules/core/repositories/objects' import { - getAllBranchCommitsFactory, getBranchCommitsTotalCountFactory, getPaginatedBranchCommitsItemsFactory, getSpecificBranchCommitsFactory, @@ -59,13 +54,6 @@ import { throwIfAuthNotOk } from '@/modules/shared/helpers/errorHelper' import { throwIfResourceAccessNotAllowed } from '@/modules/core/helpers/token' import { TokenResourceIdentifierType } from '@/modules/core/domain/tokens/types' import { withOperationLogging } from '@/observability/domain/businessLogging' -import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources' -import { NotFoundError } from '@/modules/shared/errors' -import { - isModelResource, - resourceBuilder, - ViewerModelResource -} from '@speckle/shared/viewer/route' export default { User: { @@ -170,63 +158,6 @@ export default { } ) }, - async viewerResources( - parent, - { resourceIdString, loadedVersionsOnly, savedViewId }, - ctx - ) { - const projectDB = await getProjectDbClient({ projectId: parent.id }) - const getStreamObjects = getStreamObjectsFactory({ db: projectDB }) - const getViewerResourceGroups = getViewerResourceGroupsFactory({ - getStreamObjects, - getBranchLatestCommits: getBranchLatestCommitsFactory({ db: projectDB }), - getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDB }), - getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDB }), - getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDB }), - getBranchesByIds: getBranchesByIdsFactory({ db: projectDB }) - }) - - // Saved View: By default load already specified versions were available, - // otherwise load latest versions - if (savedViewId) { - const savedView = await ctx.loaders - .forRegion({ db: projectDB }) - .savedViews.getSavedView.load({ viewId: savedViewId, projectId: parent.id }) - if (!savedView) { - throw new NotFoundError( - `Saved view with ID ${savedViewId} not found in project ${parent.id}` - ) - } - - const savedViewResources = resourceBuilder().addFromString( - savedView.resourceIds - ) - const baseResources = resourceBuilder().addFromString(resourceIdString) - const finalSavedViewResources = savedViewResources.map((r) => { - if (!isModelResource(r) || !r.versionId) { - return r - } - - const matchingBaseResource = baseResources - .filter(isModelResource) - .find((r2) => { - return r2.modelId === r.modelId - }) - - return new ViewerModelResource(r.modelId, matchingBaseResource?.versionId) - }) - - resourceIdString = resourceBuilder() - .addResources(finalSavedViewResources) - .toString() - } - - return await getViewerResourceGroups({ - projectId: parent.id, - resourceIdString, - loadedVersionsOnly - }) - }, async versions(parent, args, ctx) { const projectDB = await getProjectDbClient({ projectId: parent.id }) // If limit=0, short-cut full execution and use data loader diff --git a/packages/server/modules/cross-server-sync/index.ts b/packages/server/modules/cross-server-sync/index.ts index 78eb4c47e..9657868e8 100644 --- a/packages/server/modules/cross-server-sync/index.ts +++ b/packages/server/modules/cross-server-sync/index.ts @@ -72,6 +72,7 @@ import { getViewerResourceGroupsFactory, getViewerResourceItemsUngroupedFactory } from '@/modules/viewer/services/viewerResources' +import { getSavedViewFactory } from '@/modules/viewer/repositories/savedViews' const crossServerSyncModule: SpeckleModule = { init() { @@ -100,7 +101,8 @@ const crossServerSyncModule: SpeckleModule = { getStreamBranchesByName: getStreamBranchesByNameFactory({ db }), getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db }), getAllBranchCommits: getAllBranchCommitsFactory({ db }), - getBranchesByIds: getBranchesByIdsFactory({ db }) + getBranchesByIds: getBranchesByIdsFactory({ db }), + getSavedView: getSavedViewFactory({ db }) }) }) const getViewerResourcesFromLegacyIdentifiers = diff --git a/packages/server/modules/viewer/domain/operations/resources.ts b/packages/server/modules/viewer/domain/operations/resources.ts index b9b736295..1c0a699e9 100644 --- a/packages/server/modules/viewer/domain/operations/resources.ts +++ b/packages/server/modules/viewer/domain/operations/resources.ts @@ -1,13 +1,30 @@ -import type { ViewerUpdateTrackingTarget } from '@/modules/core/graph/generated/graphql' +import type { + SavedViewsLoadSettings, + ViewerUpdateTrackingTarget +} from '@/modules/core/graph/generated/graphql' import type { ViewerResourceGroup, ViewerResourceItem } from '@/modules/viewer/domain/types/resources' +import type { MaybeNullOrUndefined } from '@speckle/shared' + +export type GetViewerResourceGroupsParams = ViewerUpdateTrackingTarget & { + /** + * By default this only returns groups w/ resources in them. W/ this flag set, it will also + * return valid model groups that have no resources in them + */ + allowEmptyModels?: boolean + /** + * Saved view being applied makes the resources be loaded differently + */ + savedViewId?: MaybeNullOrUndefined + savedViewSettings?: MaybeNullOrUndefined +} export type GetViewerResourceGroups = ( - target: ViewerUpdateTrackingTarget & { allowEmptyModels?: boolean } + target: GetViewerResourceGroupsParams ) => Promise export type GetViewerResourceItemsUngrouped = ( - target: ViewerUpdateTrackingTarget + target: GetViewerResourceGroupsParams ) => Promise diff --git a/packages/server/modules/viewer/graph/resolvers/savedViews.ts b/packages/server/modules/viewer/graph/resolvers/savedViews.ts index 517a3a3f6..9cd324cb9 100644 --- a/packages/server/modules/viewer/graph/resolvers/savedViews.ts +++ b/packages/server/modules/viewer/graph/resolvers/savedViews.ts @@ -48,8 +48,12 @@ import { getSavedViewFactory, getSavedViewGroupFactory } from '@/modules/viewer/repositories/dataLoaders/savedViews' +import type { RequestDataLoaders } from '@/modules/core/loaders' -const buildGetViewerResourceGroups = (params: { projectDb: Knex }) => { +const buildGetViewerResourceGroups = (params: { + projectDb: Knex + loaders: RequestDataLoaders +}) => { const { projectDb } = params return getViewerResourceGroupsFactory({ getStreamObjects: getStreamObjectsFactory({ db: projectDb }), @@ -57,7 +61,8 @@ const buildGetViewerResourceGroups = (params: { projectDb: Knex }) => { getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDb }), getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDb }), getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb }), - getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }) + getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }), + getSavedView: getSavedViewFactory({ loaders: params.loaders }) }) } @@ -125,6 +130,19 @@ const resolvers: Resolvers = { ) } + return view + }, + savedViewIfExists: async (parent, args, ctx) => { + if (!args.id?.length) return null + + const projectDb = await getProjectDbClient({ projectId: parent.id }) + const view = await ctx.loaders + .forRegion({ db: projectDb }) + .savedViews.getSavedView.load({ + viewId: args.id, + projectId: parent.id + }) + return view } }, @@ -221,7 +239,10 @@ const resolvers: Resolvers = { const projectDb = await getProjectDbClient({ projectId }) const createSavedView = createSavedViewFactory({ - getViewerResourceGroups: buildGetViewerResourceGroups({ projectDb }), + getViewerResourceGroups: buildGetViewerResourceGroups({ + projectDb, + loaders: ctx.loaders + }), getStoredViewCount: getStoredViewCountFactory({ db: projectDb }), storeSavedView: storeSavedViewFactory({ db: projectDb }), getSavedViewGroup: getSavedViewGroupFactory({ loaders: ctx.loaders }), @@ -278,7 +299,10 @@ const resolvers: Resolvers = { throwIfAuthNotOk(canUpdate) const updateSavedView = updateSavedViewFactory({ - getViewerResourceGroups: buildGetViewerResourceGroups({ projectDb }), + getViewerResourceGroups: buildGetViewerResourceGroups({ + projectDb, + loaders: ctx.loaders + }), getSavedView: getSavedViewFactory({ loaders: ctx.loaders }), getSavedViewGroup: getSavedViewGroupFactory({ loaders: ctx.loaders }), updateSavedViewRecord: updateSavedViewRecordFactory({ @@ -325,7 +349,10 @@ const resolvers: Resolvers = { const projectDb = await getProjectDbClient({ projectId }) const createSavedViewGroup = createSavedViewGroupFactory({ storeSavedViewGroup: storeSavedViewGroupFactory({ db: projectDb }), - getViewerResourceGroups: buildGetViewerResourceGroups({ projectDb }) + getViewerResourceGroups: buildGetViewerResourceGroups({ + projectDb, + loaders: ctx.loaders + }) }) return await createSavedViewGroup({ input: args.input, @@ -359,6 +386,9 @@ const disabledResolvers: Resolvers = { }, savedView: () => { throw new NotImplementedError(disabledMessage) + }, + savedViewIfExists: () => { + return null // intentional - so we dont have to FF guard the query } }, ProjectMutations: { diff --git a/packages/server/modules/viewer/graph/resolvers/viewerResources.ts b/packages/server/modules/viewer/graph/resolvers/viewerResources.ts new file mode 100644 index 000000000..1351d52a9 --- /dev/null +++ b/packages/server/modules/viewer/graph/resolvers/viewerResources.ts @@ -0,0 +1,46 @@ +import type { Resolvers } from '@/modules/core/graph/generated/graphql' +import { + getBranchesByIdsFactory, + getBranchLatestCommitsFactory, + getStreamBranchesByNameFactory +} from '@/modules/core/repositories/branches' +import { + getAllBranchCommitsFactory, + getSpecificBranchCommitsFactory +} from '@/modules/core/repositories/commits' +import { getStreamObjectsFactory } from '@/modules/core/repositories/objects' +import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' +import { getSavedViewFactory } from '@/modules/viewer/repositories/dataLoaders/savedViews' +import { getViewerResourceGroupsFactory } from '@/modules/viewer/services/viewerResources' + +const resolvers: Resolvers = { + Project: { + async viewerResources( + parent, + { resourceIdString, loadedVersionsOnly, savedViewId, savedViewSettings }, + ctx + ) { + const projectDB = await getProjectDbClient({ projectId: parent.id }) + const getStreamObjects = getStreamObjectsFactory({ db: projectDB }) + const getViewerResourceGroups = getViewerResourceGroupsFactory({ + getStreamObjects, + getBranchLatestCommits: getBranchLatestCommitsFactory({ db: projectDB }), + getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDB }), + getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDB }), + getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDB }), + getBranchesByIds: getBranchesByIdsFactory({ db: projectDB }), + getSavedView: getSavedViewFactory({ loaders: ctx.loaders }) + }) + + return await getViewerResourceGroups({ + projectId: parent.id, + resourceIdString, + loadedVersionsOnly, + savedViewId, + savedViewSettings + }) + } + } +} + +export default resolvers diff --git a/packages/server/modules/viewer/repositories/savedViews.ts b/packages/server/modules/viewer/repositories/savedViews.ts index 75c373306..e5011db48 100644 --- a/packages/server/modules/viewer/repositories/savedViews.ts +++ b/packages/server/modules/viewer/repositories/savedViews.ts @@ -16,7 +16,8 @@ import type { StoreSavedViewGroup, GetSavedViews, DeleteSavedViewRecord, - UpdateSavedViewRecord + UpdateSavedViewRecord, + GetSavedView } from '@/modules/viewer/domain/operations/savedViews' import { SavedViewVisibility, @@ -486,6 +487,14 @@ export const getSavedViewsFactory = return viewsMap } +export const getSavedViewFactory = + (deps: { db: Knex }): GetSavedView => + async ({ id, projectId }) => { + const getSavedViews = getSavedViewsFactory(deps) + const savedViews = await getSavedViews({ viewIds: [{ viewId: id, projectId }] }) + return savedViews[id] + } + export const deleteSavedViewRecordFactory = (deps: { db: Knex }): DeleteSavedViewRecord => async (params) => { diff --git a/packages/server/modules/viewer/services/viewerResources.ts b/packages/server/modules/viewer/services/viewerResources.ts index ab2ec10fb..0066c5e97 100644 --- a/packages/server/modules/viewer/services/viewerResources.ts +++ b/packages/server/modules/viewer/services/viewerResources.ts @@ -9,18 +9,25 @@ import type { } from '@/modules/core/domain/commits/operations' import type { GetStreamObjects } from '@/modules/core/domain/objects/operations' import type { + SavedViewsLoadSettings, ViewerResourceGroup, - ViewerResourceItem, - ViewerUpdateTrackingTarget + ViewerResourceItem } from '@/modules/core/graph/generated/graphql' import type { CommitRecord } from '@/modules/core/helpers/types' +import { NotFoundError } from '@/modules/shared/errors' +import type { DependenciesOf } from '@/modules/shared/helpers/factory' import type { GetViewerResourceGroups, GetViewerResourceItemsUngrouped } from '@/modules/viewer/domain/operations/resources' -import type { Optional } from '@speckle/shared' +import type { GetSavedView } from '@/modules/viewer/domain/operations/savedViews' +import type { MaybeNullOrUndefined, Optional } from '@speckle/shared' import { SpeckleViewer } from '@speckle/shared' -import type { ViewerModelResource } from '@speckle/shared/viewer/route' +import { + isModelResource, + resourceBuilder, + ViewerModelResource +} from '@speckle/shared/viewer/route' import { flatten, keyBy, uniq, uniqWith } from 'lodash-es' export function isResourceItemEqual(a: ViewerResourceItem, b: ViewerResourceItem) { @@ -324,24 +331,78 @@ const getVersionResourceGroupsFactory = return [...(allModelsGroup ? [allModelsGroup] : []), ...groups] } +/** + * Resolve final resourceIdString based on the saved view and its load settings + */ +const adjustResourceIdStringWithSavedViewSettingsFactory = + (deps: { getSavedView: GetSavedView }) => + async (params: { + projectId: string + resourceIdString: string + savedViewId: string + savedViewSettings: MaybeNullOrUndefined + }): Promise => { + const { resourceIdString, projectId, savedViewId, savedViewSettings } = params + const { loadOriginal } = savedViewSettings || {} + + const savedView = await deps.getSavedView({ + id: savedViewId, + projectId + }) + if (!savedView) { + throw new NotFoundError( + `Saved view with ID ${savedViewId} not found in project ${projectId}` + ) + } + + const savedViewResources = resourceBuilder().addFromString(savedView.resourceIds) + const baseResources = resourceBuilder().addFromString(resourceIdString) + const finalSavedViewResources = savedViewResources.map((r) => { + if (!isModelResource(r) || !r.versionId) { + return r + } + + const matchingBaseResource = baseResources.filter(isModelResource).find((r2) => { + return r2.modelId === r.modelId + }) + const versionId = loadOriginal ? r.versionId : matchingBaseResource?.versionId + return new ViewerModelResource(r.modelId, versionId) + }) + + return resourceBuilder().addResources(finalSavedViewResources).toString() + } + /** * Validate requested resource identifiers and build viewer resource groups & items with * the metadata that the viewer needs to work with these */ export const getViewerResourceGroupsFactory = ( - deps: GetObjectResourceGroupsDeps & GetVersionResourceGroupsDeps + deps: GetObjectResourceGroupsDeps & + GetVersionResourceGroupsDeps & + DependenciesOf ): GetViewerResourceGroups => - async ( - target: ViewerUpdateTrackingTarget & { - /** - * By default this only returns groups w/ resources in them. W/ this flag set, it will also - * return valid model groups that have no resources in them - */ - allowEmptyModels?: boolean + async (params): Promise => { + const { + projectId, + loadedVersionsOnly, + allowEmptyModels, + savedViewId, + savedViewSettings + } = params + + let resourceIdString = params.resourceIdString + if (savedViewId) { + resourceIdString = await adjustResourceIdStringWithSavedViewSettingsFactory(deps)( + { + resourceIdString, + projectId, + savedViewId, + savedViewSettings + } + ) } - ): Promise => { - const { resourceIdString, projectId, loadedVersionsOnly, allowEmptyModels } = target + if (!resourceIdString?.trim().length) return [] const resources = SpeckleViewer.ViewerRoute.parseUrlParameters(resourceIdString) @@ -374,12 +435,12 @@ export const getViewerResourceItemsUngroupedFactory = (deps: { getViewerResourceGroups: GetViewerResourceGroups }): GetViewerResourceItemsUngrouped => - async (target: ViewerUpdateTrackingTarget): Promise => { - const { resourceIdString } = target + async (params): Promise => { + const { resourceIdString } = params if (!resourceIdString?.trim().length) return [] let results: ViewerResourceItem[] = [] - const groups = await deps.getViewerResourceGroups(target) + const groups = await deps.getViewerResourceGroups(params) for (const group of groups) { results = results.concat(group.items) } diff --git a/packages/server/modules/viewer/tests/integration/viewerResources.spec.ts b/packages/server/modules/viewer/tests/integration/viewerResources.spec.ts index 154d6c68c..c2eec3e0b 100644 --- a/packages/server/modules/viewer/tests/integration/viewerResources.spec.ts +++ b/packages/server/modules/viewer/tests/integration/viewerResources.spec.ts @@ -10,6 +10,7 @@ import { } from '@/modules/core/repositories/commits' import { getStreamObjectsFactory } from '@/modules/core/repositories/objects' import { buildBasicTestProject } from '@/modules/core/tests/helpers/creation' +import { getSavedViewFactory } from '@/modules/viewer/repositories/savedViews' import { doViewerResourcesFit, getViewerResourceGroupsFactory, @@ -52,7 +53,8 @@ describe('Viewer Resources Collection Service', () => { getStreamBranchesByName: getStreamBranchesByNameFactory({ db }), getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db }), getAllBranchCommits: getAllBranchCommitsFactory({ db }), - getBranchesByIds: getBranchesByIdsFactory({ db }) + getBranchesByIds: getBranchesByIdsFactory({ db }), + getSavedView: getSavedViewFactory({ db }) }) const allVersions = (): BasicTestCommit[] => { diff --git a/packages/server/test/speckle-helpers/commentHelper.ts b/packages/server/test/speckle-helpers/commentHelper.ts index c36e9f5e8..dabb295c8 100644 --- a/packages/server/test/speckle-helpers/commentHelper.ts +++ b/packages/server/test/speckle-helpers/commentHelper.ts @@ -18,6 +18,7 @@ import { } from '@/modules/core/repositories/commits' import { getStreamObjectsFactory } from '@/modules/core/repositories/objects' import { getProjectDbClient } from '@/modules/multiregion/utils/dbSelector' +import { getSavedViewFactory } from '@/modules/viewer/repositories/savedViews' import { getViewerResourceGroupsFactory, getViewerResourceItemsUngroupedFactory @@ -44,7 +45,8 @@ export const createTestComment = async ( getStreamBranchesByName: getStreamBranchesByNameFactory({ db: projectDb }), getSpecificBranchCommits: getSpecificBranchCommitsFactory({ db: projectDb }), getAllBranchCommits: getAllBranchCommitsFactory({ db: projectDb }), - getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }) + getBranchesByIds: getBranchesByIdsFactory({ db: projectDb }), + getSavedView: getSavedViewFactory({ db: projectDb }) }) }), validateInputAttachments: validateInputAttachmentsFactory({ diff --git a/packages/shared/src/viewer/helpers/route.ts b/packages/shared/src/viewer/helpers/route.ts index ee9080c7a..3747a3c3f 100644 --- a/packages/shared/src/viewer/helpers/route.ts +++ b/packages/shared/src/viewer/helpers/route.ts @@ -256,6 +256,12 @@ class ViewerResourceBuilder implements Iterable { get length() { return this.#resources.length } + + isEqualTo(resource: ViewerResourcesTarget) { + const incomingBuilder = resourceBuilder().addResources(resource) + return this.toString() === incomingBuilder.toString() + } + forEach(callback: (resource: ViewerResource) => void) { this.#resources.forEach(callback) return this diff --git a/packages/ui-components/.storybook/main.ts b/packages/ui-components/.storybook/main.ts index 1978a7237..6239bebeb 100644 --- a/packages/ui-components/.storybook/main.ts +++ b/packages/ui-components/.storybook/main.ts @@ -1,6 +1,6 @@ import { dirname, join } from 'path' import type { StorybookConfig } from '@storybook/vue3-vite' -import { get, isObjectLike } from 'lodash' +import { get, isObjectLike } from '#lodash' function getAbsolutePath(value: V): V { return dirname(require.resolve(join(value, 'package.json'))) as V diff --git a/packages/ui-components/eslint.config.mjs b/packages/ui-components/eslint.config.mjs index e794192e2..1357a976b 100644 --- a/packages/ui-components/eslint.config.mjs +++ b/packages/ui-components/eslint.config.mjs @@ -7,7 +7,7 @@ import { import tseslint from 'typescript-eslint' import pluginVue from 'eslint-plugin-vue' import pluginVueA11y from 'eslint-plugin-vuejs-accessibility' -import { omit } from 'lodash-es' +import { omit } from '#lodash' const tsParserOptions = { tsconfigRootDir: getESMDirname(import.meta.url), diff --git a/packages/ui-components/src/components/common/Alert.vue b/packages/ui-components/src/components/common/Alert.vue index 3043a71c9..9bfc6ed11 100644 --- a/packages/ui-components/src/components/common/Alert.vue +++ b/packages/ui-components/src/components/common/Alert.vue @@ -48,7 +48,7 @@ import { InformationCircleIcon, ExclamationCircleIcon } from '@heroicons/vue/24/outline' -import { noop } from 'lodash' +import { noop } from '#lodash' import { computed, useSlots, type SetupContext } from 'vue' import FormButton from '~~/src/components/form/Button.vue' import type { diff --git a/packages/ui-components/src/components/form/Button.vue b/packages/ui-components/src/components/form/Button.vue index 4114eed04..a1d96b106 100644 --- a/packages/ui-components/src/components/form/Button.vue +++ b/packages/ui-components/src/components/form/Button.vue @@ -21,7 +21,7 @@