diff --git a/docker-compose-speckle.yml b/docker-compose-speckle.yml
index e49a08de4..a115c90aa 100644
--- a/docker-compose-speckle.yml
+++ b/docker-compose-speckle.yml
@@ -24,6 +24,7 @@ services:
NUXT_PUBLIC_SERVER_NAME: 'local'
NUXT_PUBLIC_API_ORIGIN: 'http://127.0.0.1'
NUXT_PUBLIC_BACKEND_API_ORIGIN: 'http://speckle-server:3000'
+ NUXT_REDIS_URL: 'redis://redis'
speckle-server:
build:
diff --git a/packages/frontend-2/.env.example b/packages/frontend-2/.env.example
index a9da14203..fd0efdcfb 100644
--- a/packages/frontend-2/.env.example
+++ b/packages/frontend-2/.env.example
@@ -9,4 +9,6 @@ NUXT_PUBLIC_API_ORIGIN=http://127.0.0.1:3000
NUXT_PUBLIC_MIXPANEL_TOKEN_ID=acd87c5a50b56df91a795e999812a3a4
NUXT_PUBLIC_MIXPANEL_API_HOST=https://analytics.speckle.systems
-NUXT_PUBLIC_SERVER_NAME=local
\ No newline at end of file
+NUXT_PUBLIC_SERVER_NAME=local
+
+NUXT_REDIS_URL=redis://localhost:6379
diff --git a/packages/frontend-2/components/connectors/Card.vue b/packages/frontend-2/components/connectors/Card.vue
index 0f3654fc8..0c9ac2afb 100644
--- a/packages/frontend-2/components/connectors/Card.vue
+++ b/packages/frontend-2/components/connectors/Card.vue
@@ -48,7 +48,9 @@
Downloads
- Tutorials
+
+ Tutorials
+
diff --git a/packages/frontend-2/components/project/page/latest-items/Comments.vue b/packages/frontend-2/components/project/page/latest-items/Comments.vue
index 057bfc9ef..c9232750e 100644
--- a/packages/frontend-2/components/project/page/latest-items/Comments.vue
+++ b/packages/frontend-2/components/project/page/latest-items/Comments.vue
@@ -62,7 +62,6 @@ graphql(`
...FormUsersSelectItem
}
}
- ...LinkableComment
}
`)
diff --git a/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue b/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue
index c02ca21fd..48c578008 100644
--- a/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue
+++ b/packages/frontend-2/components/project/page/latest-items/comments/GridItem.vue
@@ -52,7 +52,7 @@ import dayjs from 'dayjs'
import type { ProjectPageLatestItemsCommentItemFragment } from '~~/lib/common/generated/gql/graphql'
import { useCommentScreenshotImage } from '~~/lib/projects/composables/previewImage'
import { times } from 'lodash-es'
-import { getLinkToThread } from '~~/lib/viewer/helpers/comments'
+import { getLightLinkToThread } from '~~/lib/viewer/helpers/comments'
import { CheckCircleIcon } from '@heroicons/vue/24/solid'
import type { AvatarUserWithId } from '@speckle/ui-components'
@@ -90,5 +90,7 @@ const allAvatars = computed((): AvatarUserWithId[] => [
)
])
-const threadLink = computed(() => getLinkToThread(props.projectId, props.thread))
+const threadLink = computed(() =>
+ getLightLinkToThread(props.projectId, props.thread.id)
+)
diff --git a/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue b/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue
index fa7446f43..ea91d5814 100644
--- a/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue
+++ b/packages/frontend-2/components/project/page/latest-items/comments/ListItem.vue
@@ -44,7 +44,7 @@ import dayjs from 'dayjs'
import { times } from 'lodash-es'
import type { ProjectPageLatestItemsCommentItemFragment } from '~~/lib/common/generated/gql/graphql'
import { useCommentScreenshotImage } from '~~/lib/projects/composables/previewImage'
-import { getLinkToThread } from '~~/lib/viewer/helpers/comments'
+import { getLightLinkToThread } from '~~/lib/viewer/helpers/comments'
import { CheckCircleIcon } from '@heroicons/vue/24/solid'
import type { AvatarUserWithId } from '@speckle/ui-components'
@@ -81,5 +81,7 @@ const allAvatars = computed((): AvatarUserWithId[] => [
)
])
-const threadLink = computed(() => getLinkToThread(props.projectId, props.thread))
+const threadLink = computed(() =>
+ getLightLinkToThread(props.projectId, props.thread.id)
+)
diff --git a/packages/frontend-2/components/project/page/models/CardView.vue b/packages/frontend-2/components/project/page/models/CardView.vue
index 46588d09b..93e878a71 100644
--- a/packages/frontend-2/components/project/page/models/CardView.vue
+++ b/packages/frontend-2/components/project/page/models/CardView.vue
@@ -81,20 +81,31 @@ const logger = useLogger()
const areQueriesLoading = useQueryLoading()
const latestModelsQueryVariables = computed(
- (): ProjectLatestModelsPaginationQueryVariables => ({
- projectId: props.projectId,
- filter: {
- search: props.search || null,
- excludeIds: props.excludedIds || null,
- onlyWithVersions: !!props.excludeEmptyModels,
- sourceApps: props.sourceApps?.length
- ? props.sourceApps.map((a) => a.searchKey)
- : null,
- contributors: props.contributors?.length
- ? props.contributors.map((c) => c.id)
+ (): ProjectLatestModelsPaginationQueryVariables => {
+ const shouldHaveFilter =
+ props.search?.length ||
+ props.excludedIds?.length ||
+ props.sourceApps?.length ||
+ props.contributors?.length ||
+ !!props.excludeEmptyModels
+
+ return {
+ projectId: props.projectId,
+ filter: shouldHaveFilter
+ ? {
+ search: props.search || null,
+ excludeIds: props.excludedIds || null,
+ onlyWithVersions: !!props.excludeEmptyModels,
+ sourceApps: props.sourceApps?.length
+ ? props.sourceApps.map((a) => a.searchKey)
+ : null,
+ contributors: props.contributors?.length
+ ? props.contributors.map((c) => c.id)
+ : null
+ }
: null
}
- })
+ }
)
const infiniteLoaderId = ref('')
diff --git a/packages/frontend-2/composables/cache.ts b/packages/frontend-2/composables/cache.ts
new file mode 100644
index 000000000..293da96ba
--- /dev/null
+++ b/packages/frontend-2/composables/cache.ts
@@ -0,0 +1,28 @@
+import type { MaybeAsync } from '@speckle/shared'
+
+/**
+ * In SSR: Provides a redis cache that is shared across app processes and requests
+ * In CSR: Provides an in-memory cache that is shared across the app session
+ */
+export function useAppCache() {
+ const app = useNuxtApp()
+ return app.$appCache
+}
+
+/**
+ * Get value from app cache or resolve and set it
+ */
+export async function useAppCached(
+ key: string,
+ resolver: () => MaybeAsync,
+ options?: Parameters['set']>['2']
+): Promise {
+ const cache = useAppCache()
+ if (await cache.has(key)) {
+ return (await cache.get(key)) as V
+ }
+
+ const data = await Promise.resolve(resolver())
+ await cache.set(key, data, options)
+ return data
+}
diff --git a/packages/frontend-2/lib/common/generated/gql/gql.ts b/packages/frontend-2/lib/common/generated/gql/gql.ts
index d047b0255..563b948fa 100644
--- a/packages/frontend-2/lib/common/generated/gql/gql.ts
+++ b/packages/frontend-2/lib/common/generated/gql/gql.ts
@@ -35,7 +35,7 @@ const documents = {
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": types.ProjectModelsPageResults_ProjectFragmentDoc,
"\n fragment ProjectPageProjectHeader on Project {\n id\n role\n name\n description\n visibility\n allowPublicComments\n }\n": types.ProjectPageProjectHeaderFragmentDoc,
"\n fragment ProjectPageLatestItemsComments on Project {\n id\n commentThreadCount: commentThreads(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsCommentsFragmentDoc,
- "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n": types.ProjectPageLatestItemsCommentItemFragmentDoc,
+ "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n": types.ProjectPageLatestItemsCommentItemFragmentDoc,
"\n fragment ProjectPageLatestItemsModels on Project {\n id\n role\n modelCount: models(limit: 0) {\n totalCount\n }\n }\n": types.ProjectPageLatestItemsModelsFragmentDoc,
"\n fragment ProjectPageModelsActions on Model {\n id\n name\n }\n": types.ProjectPageModelsActionsFragmentDoc,
"\n fragment ProjectPageModelsCardProject on Project {\n id\n role\n }\n": types.ProjectPageModelsCardProjectFragmentDoc,
@@ -163,6 +163,7 @@ const documents = {
"\n subscription OnViewerUserActivityBroadcasted(\n $target: ViewerUpdateTrackingTarget!\n $sessionId: String!\n ) {\n viewerUserActivityBroadcasted(target: $target, sessionId: $sessionId) {\n userName\n userId\n user {\n ...LimitedUserAvatar\n }\n state\n status\n sessionId\n }\n }\n": types.OnViewerUserActivityBroadcastedDocument,
"\n subscription OnViewerCommentsUpdated($target: ViewerUpdateTrackingTarget!) {\n projectCommentsUpdated(target: $target) {\n id\n type\n comment {\n id\n parent {\n id\n }\n ...ViewerCommentThread\n }\n }\n }\n": types.OnViewerCommentsUpdatedDocument,
"\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n": types.LinkableCommentFragmentDoc,
+ "\n query ResolveCommentLink($commentId: String!, $projectId: String!) {\n comment(id: $commentId, streamId: $projectId) {\n ...LinkableComment\n }\n }\n": types.ResolveCommentLinkDocument,
"\n fragment ProjectPageProject on Project {\n id\n createdAt\n ...ProjectPageProjectHeader\n ...ProjectPageStatsBlockTeam\n ...ProjectPageTeamDialog\n ...ProjectPageStatsBlockVersions\n ...ProjectPageStatsBlockModels\n ...ProjectPageStatsBlockComments\n ...ProjectPageLatestItemsModels\n ...ProjectPageLatestItemsComments\n }\n": types.ProjectPageProjectFragmentDoc,
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n }\n": types.ModelPageProjectFragmentDoc,
};
@@ -272,7 +273,7 @@ export function graphql(source: "\n fragment ProjectPageLatestItemsComments on
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
-export function graphql(source: "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n ...LinkableComment\n }\n"];
+export function graphql(source: "\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n"): (typeof documents)["\n fragment ProjectPageLatestItemsCommentItem on Comment {\n id\n author {\n ...FormUsersSelectItem\n }\n screenshot\n rawText\n createdAt\n updatedAt\n archived\n repliesCount: replies(limit: 0) {\n totalCount\n }\n replyAuthors(limit: 4) {\n totalCount\n items {\n ...FormUsersSelectItem\n }\n }\n }\n"];
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
@@ -781,6 +782,10 @@ export function graphql(source: "\n subscription OnViewerCommentsUpdated($targe
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*/
export function graphql(source: "\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\n }\n }\n"): (typeof documents)["\n fragment LinkableComment on Comment {\n id\n viewerResources {\n modelId\n versionId\n objectId\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 ResolveCommentLink($commentId: String!, $projectId: String!) {\n comment(id: $commentId, streamId: $projectId) {\n ...LinkableComment\n }\n }\n"): (typeof documents)["\n query ResolveCommentLink($commentId: String!, $projectId: String!) {\n comment(id: $commentId, streamId: $projectId) {\n ...LinkableComment\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 d14f614a0..ccf8172c2 100644
--- a/packages/frontend-2/lib/common/generated/gql/graphql.ts
+++ b/packages/frontend-2/lib/common/generated/gql/graphql.ts
@@ -2953,7 +2953,7 @@ export type ProjectPageProjectHeaderFragment = { __typename?: 'Project', id: str
export type ProjectPageLatestItemsCommentsFragment = { __typename?: 'Project', id: string, commentThreadCount: { __typename?: 'ProjectCommentCollection', totalCount: number } };
-export type ProjectPageLatestItemsCommentItemFragment = { __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> }, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> };
+export type ProjectPageLatestItemsCommentItemFragment = { __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> } };
export type ProjectPageLatestItemsModelsFragment = { __typename?: 'Project', id: string, role?: string | null, modelCount: { __typename?: 'ModelCollection', totalCount: number } };
@@ -3348,7 +3348,7 @@ export type ProjectLatestCommentThreadsQueryVariables = Exact<{
}>;
-export type ProjectLatestCommentThreadsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, commentThreads: { __typename?: 'ProjectCommentCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> }, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> }> } } };
+export type ProjectLatestCommentThreadsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, commentThreads: { __typename?: 'ProjectCommentCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'Comment', id: string, screenshot?: string | null, rawText: string, createdAt: string, updatedAt: string, archived: boolean, author: { __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }, repliesCount: { __typename?: 'CommentCollection', totalCount: number }, replyAuthors: { __typename?: 'CommentReplyAuthorCollection', totalCount: number, items: Array<{ __typename?: 'LimitedUser', id: string, name: string, avatar?: string | null }> } }> } } };
export type ProjectInviteQueryVariables = Exact<{
projectId: Scalars['String'];
@@ -3682,6 +3682,14 @@ export type OnViewerCommentsUpdatedSubscription = { __typename?: 'Subscription',
export type LinkableCommentFragment = { __typename?: 'Comment', id: string, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> };
+export type ResolveCommentLinkQueryVariables = Exact<{
+ commentId: Scalars['String'];
+ projectId: Scalars['String'];
+}>;
+
+
+export type ResolveCommentLinkQuery = { __typename?: 'Query', comment?: { __typename?: 'Comment', id: string, viewerResources: Array<{ __typename?: 'ViewerResourceItem', modelId?: string | null, versionId?: string | null, objectId: string }> } | null };
+
export type ProjectPageProjectFragment = { __typename?: 'Project', id: string, createdAt: string, role?: string | null, name: string, description?: string | null, visibility: ProjectVisibility, allowPublicComments: boolean, team: Array<{ __typename?: 'ProjectCollaborator', role: string, user: { __typename?: 'LimitedUser', role?: string | null, id: string, name: string, avatar?: string | null } }>, invitedTeam?: Array<{ __typename?: 'PendingStreamCollaborator', id: string, title: string, inviteId: string, role: string, user?: { __typename?: 'LimitedUser', role?: string | null, id: string, name: string, avatar?: string | null } | null }> | null, versionCount: { __typename?: 'VersionCollection', totalCount: number }, modelCount: { __typename?: 'ModelCollection', totalCount: number }, commentThreadCount: { __typename?: 'ProjectCommentCollection', totalCount: number } };
export type ModelPageProjectFragment = { __typename?: 'Project', id: string, createdAt: string, name: string };
@@ -3709,8 +3717,7 @@ export const FormUsersSelectItemFragmentDoc = {"kind":"Document","definitions":[
export const ProjectModelsPageHeader_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelsPageHeader_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":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sourceApps"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}}]}}]}}]} as unknown as DocumentNode;
export const ProjectPageLatestItemsModelsFragmentDoc = {"kind":"Document","definitions":[{"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","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"}}]}}]}}]} as unknown as DocumentNode;
export const ProjectModelsPageResults_ProjectFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectModelsPageResults_Project"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageLatestItemsModels"}}]}}]} as unknown as DocumentNode;
-export const LinkableCommentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LinkableComment"},"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":"viewerResources"},"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 ProjectPageLatestItemsCommentItemFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageLatestItemsCommentItem"},"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":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","alias":{"kind":"Name","value":"repliesCount"},"name":{"kind":"Name","value":"replies"},"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":"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":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]} as unknown as DocumentNode;
+export const ProjectPageLatestItemsCommentItemFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageLatestItemsCommentItem"},"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":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"FormUsersSelectItem"}}]}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"rawText"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"archived"}},{"kind":"Field","alias":{"kind":"Name","value":"repliesCount"},"name":{"kind":"Name","value":"replies"},"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":"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"}}]}}]}}]}}]} as unknown as DocumentNode;
export const ModelPreviewFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ModelPreview"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"previewUrl"}}]}}]} as unknown as DocumentNode;
export const ProjectPageModelsCardRenameDialogFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsCardRenameDialog"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}}]}}]} as unknown as DocumentNode;
export const ProjectPageModelsCardDeleteDialogFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageModelsCardDeleteDialog"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Model"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]} as unknown as DocumentNode;
@@ -3737,6 +3744,7 @@ export const ViewerCommentsReplyItemFragmentDoc = {"kind":"Document","definition
export const ViewerCommentsListItemFragmentDoc = {"kind":"Document","definitions":[{"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"}}]}}]}}]} as unknown as DocumentNode;
export const ViewerCommentBubblesDataFragmentDoc = {"kind":"Document","definitions":[{"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"}}]}}]} as unknown as DocumentNode;
export const ViewerCommentThreadFragmentDoc = {"kind":"Document","definitions":[{"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"}}]}}]} as unknown as DocumentNode;
+export const LinkableCommentFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LinkableComment"},"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":"viewerResources"},"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 ProjectPageTeamDialogFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageTeamDialog"},"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":"name"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"allowPublicComments"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}},{"kind":"Field","name":{"kind":"Name","value":"role"}}]}}]}}]}}]} as unknown as DocumentNode;
export const ProjectPageStatsBlockTeamFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageStatsBlockTeam"},"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":"team"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectPageTeamDialog"}}]}}]} as unknown as DocumentNode;
export const ProjectPageStatsBlockVersionsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ProjectPageStatsBlockVersions"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Project"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","alias":{"kind":"Name","value":"versionCount"},"name":{"kind":"Name","value":"versions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"0"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}}]}}]}}]} as unknown as DocumentNode;
@@ -3795,7 +3803,7 @@ export const ProjectLatestModelsPaginationDocument = {"kind":"Document","definit
export const ProjectModelsTreeTopLevelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelsTreeTopLevel"},"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":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectModelsTreeFilter"}}}],"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":"modelsTree"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"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":"SingleLevelModelTreeItem"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pendingImportedModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"PendingFileUpload"}}]}}]}}]}},...SingleLevelModelTreeItemFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode;
export const ProjectModelsTreeTopLevelPaginationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelsTreeTopLevelPagination"},"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":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectModelsTreeFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}}],"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":"modelsTree"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"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":"SingleLevelModelTreeItem"}}]}}]}}]}}]}},...SingleLevelModelTreeItemFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode;
export const ProjectModelChildrenTreeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelChildrenTree"},"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":"parentName"}},"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":"modelChildrenTree"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"fullName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"parentName"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SingleLevelModelTreeItem"}}]}}]}}]}},...SingleLevelModelTreeItemFragmentDoc.definitions,...ProjectPageLatestItemsModelItemFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectPageModelsCardRenameDialogFragmentDoc.definitions,...ProjectPageModelsCardDeleteDialogFragmentDoc.definitions,...ProjectPageModelsActionsFragmentDoc.definitions,...ModelCardAutomationStatus_ModelFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode;
-export const ProjectLatestCommentThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectLatestCommentThreads"},"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":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}},"defaultValue":{"kind":"NullValue"}}],"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":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"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":"ProjectPageLatestItemsCommentItem"}}]}}]}}]}}]}},...ProjectPageLatestItemsCommentItemFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode;
+export const ProjectLatestCommentThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectLatestCommentThreads"},"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":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}},"defaultValue":{"kind":"NullValue"}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"filter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}},"defaultValue":{"kind":"NullValue"}}],"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":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"8"}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}}],"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":"ProjectPageLatestItemsCommentItem"}}]}}]}}]}}]}},...ProjectPageLatestItemsCommentItemFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions]} as unknown as DocumentNode;
export const ProjectInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectInvite"},"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":"token"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectsInviteBanner"}}]}}]}},...ProjectsInviteBannerFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode;
export const ProjectModelCheckDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelCheck"},"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"}}}}],"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":"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"}}]}}]}}]}}]} as unknown as DocumentNode;
export const ProjectModelPageDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ProjectModelPage"},"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":"FragmentSpread","name":{"kind":"Name","value":"ProjectModelPageHeaderProject"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ProjectModelPageVersionsProject"}}]}}]}},...ProjectModelPageHeaderProjectFragmentDoc.definitions,...ProjectModelPageVersionsProjectFragmentDoc.definitions,...ProjectPageProjectHeaderFragmentDoc.definitions,...PendingFileUploadFragmentDoc.definitions,...ProjectModelPageVersionsPaginationFragmentDoc.definitions,...ProjectModelPageVersionsCardVersionFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ProjectModelPageDialogDeleteVersionFragmentDoc.definitions,...ProjectModelPageDialogMoveToVersionFragmentDoc.definitions,...ModelCardAutomationStatus_VersionFragmentDoc.definitions,...ModelCardAutomationStatus_AutomationsStatusFragmentDoc.definitions]} as unknown as DocumentNode;
@@ -3837,4 +3845,5 @@ export const ViewerModelVersionsDocument = {"kind":"Document","definitions":[{"k
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"}}]}}]}}]}}]}},...ViewerModelVersionCardItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode;
export const ViewerLoadedThreadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ViewerLoadedThreads"},"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":"filter"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectCommentsFilter"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"25"}}],"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":"commentThreads"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"filter"}}},{"kind":"Argument","name":{"kind":"Name","value":"cursor"},"value":{"kind":"Variable","name":{"kind":"Name","value":"cursor"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalArchivedCount"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions,...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode;
export const OnViewerUserActivityBroadcastedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerUserActivityBroadcasted"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"viewerUserActivityBroadcasted"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}},{"kind":"Argument","name":{"kind":"Name","value":"sessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userName"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LimitedUserAvatar"}}]}},{"kind":"Field","name":{"kind":"Name","value":"state"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"sessionId"}}]}}]}},...LimitedUserAvatarFragmentDoc.definitions]} as unknown as DocumentNode;
-export const OnViewerCommentsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerCommentsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCommentsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"comment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions]} as unknown as DocumentNode;
\ No newline at end of file
+export const OnViewerCommentsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"OnViewerCommentsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"target"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ViewerUpdateTrackingTarget"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectCommentsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"target"},"value":{"kind":"Variable","name":{"kind":"Name","value":"target"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"comment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"parent"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"FragmentSpread","name":{"kind":"Name","value":"ViewerCommentThread"}}]}}]}}]}},...ViewerCommentThreadFragmentDoc.definitions,...ViewerCommentsListItemFragmentDoc.definitions,...LimitedUserAvatarFragmentDoc.definitions,...ViewerCommentsReplyItemFragmentDoc.definitions,...ThreadCommentAttachmentFragmentDoc.definitions,...FormUsersSelectItemFragmentDoc.definitions,...ViewerCommentBubblesDataFragmentDoc.definitions]} as unknown as DocumentNode;
+export const ResolveCommentLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ResolveCommentLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"comment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"commentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"streamId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LinkableComment"}}]}}]}},...LinkableCommentFragmentDoc.definitions]} as unknown as DocumentNode;
\ No newline at end of file
diff --git a/packages/frontend-2/lib/common/helpers/route.ts b/packages/frontend-2/lib/common/helpers/route.ts
index 8eadedeba..5a18175ec 100644
--- a/packages/frontend-2/lib/common/helpers/route.ts
+++ b/packages/frontend-2/lib/common/helpers/route.ts
@@ -30,6 +30,9 @@ export const projectWebhooksRoute = (projectId: string) =>
export const automationDataPageRoute = (baseUrl: string, automationId: string) =>
new URL(`/automations/${automationId}`, baseUrl).toString()
+export const threadRedirectRoute = (projectId: string, threadId: string) =>
+ `/projects/${projectId}/threads/${threadId}`
+
/**
* TODO: Page doesn't exist
*/
diff --git a/packages/frontend-2/lib/viewer/helpers/comments.ts b/packages/frontend-2/lib/viewer/helpers/comments.ts
index eedec1e51..fd88be6c8 100644
--- a/packages/frontend-2/lib/viewer/helpers/comments.ts
+++ b/packages/frontend-2/lib/viewer/helpers/comments.ts
@@ -5,7 +5,7 @@ import type {
CommentContentInput,
LinkableCommentFragment
} from '~~/lib/common/generated/gql/graphql'
-import { modelRoute } from '~~/lib/common/helpers/route'
+import { modelRoute, threadRedirectRoute } from '~~/lib/common/helpers/route'
import type { CommentEditorValue } from '~~/lib/viewer/composables/commentManagement'
import { ViewerHashStateKeys } from '~~/lib/viewer/composables/setup/urlHashState'
@@ -41,6 +41,14 @@ graphql(`
}
`)
+/**
+ * Resolving the actual full link requires viewerResources which are pretty heavy to fetch.
+ * This link defers viewerResources resolution to when the link is actually clicked
+ */
+export function getLightLinkToThread(projectId: string, threadId: string) {
+ return threadRedirectRoute(projectId, threadId)
+}
+
export function getLinkToThread(projectId: string, thread: LinkableCommentFragment) {
if (!thread.viewerResources.length) return undefined
const sortedResources = sortBy(thread.viewerResources, (r) => {
diff --git a/packages/frontend-2/middleware/thread.ts b/packages/frontend-2/middleware/thread.ts
new file mode 100644
index 000000000..bf027c250
--- /dev/null
+++ b/packages/frontend-2/middleware/thread.ts
@@ -0,0 +1,50 @@
+import { useApolloClientFromNuxt } from '~/lib/common/composables/graphql'
+import { graphql } from '~/lib/common/generated/gql'
+import { convertThrowIntoFetchResult } from '~/lib/common/helpers/graphql'
+import { getLinkToThread } from '~/lib/viewer/helpers/comments'
+
+const resolveLinkQuery = graphql(`
+ query ResolveCommentLink($commentId: String!, $projectId: String!) {
+ comment(id: $commentId, streamId: $projectId) {
+ ...LinkableComment
+ }
+ }
+`)
+
+export default defineNuxtRouteMiddleware(async (to) => {
+ const client = useApolloClientFromNuxt()
+ const threadId = to.params.threadId as string
+ const projectId = to.params.id as string
+
+ const res = await client
+ .query({
+ query: resolveLinkQuery,
+ variables: {
+ commentId: threadId,
+ projectId
+ }
+ })
+ .catch(convertThrowIntoFetchResult)
+
+ const comment = res.data?.comment
+ if (!comment) {
+ return abortNavigation(
+ createError({
+ message: 'Comment thread not found',
+ statusCode: 404
+ })
+ )
+ }
+
+ const link = getLinkToThread(projectId, comment)
+ if (!link) {
+ return abortNavigation(
+ createError({
+ message: 'Comment thread not found',
+ statusCode: 404
+ })
+ )
+ }
+
+ return navigateTo(link)
+})
diff --git a/packages/frontend-2/nuxt.config.ts b/packages/frontend-2/nuxt.config.ts
index d0d7f30c4..1d9b2088f 100644
--- a/packages/frontend-2/nuxt.config.ts
+++ b/packages/frontend-2/nuxt.config.ts
@@ -39,6 +39,7 @@ export default defineNuxtConfig({
'@artmizu/nuxt-prometheus'
],
runtimeConfig: {
+ redisUrl: '',
public: {
apiOrigin: 'UNDEFINED',
backendApiOrigin: '',
@@ -105,8 +106,10 @@ export default defineNuxtConfig({
},
// older chrome version for CEF 65 support. all identifiers except the chrome one are default ones.
target: ['es2020', 'edge88', 'firefox78', 'chrome65', 'safari14']
- // optionally disable minification for debugging
- // minify: false
+ // // optionally disable minification for debugging
+ // minify: false,
+ // // optionally enable sourcemaps for debugging
+ // sourcemap: 'inline'
},
plugins: [
// again - only for CEF 65
diff --git a/packages/frontend-2/package.json b/packages/frontend-2/package.json
index fffd7dc81..73c9f0375 100644
--- a/packages/frontend-2/package.json
+++ b/packages/frontend-2/package.json
@@ -50,6 +50,7 @@
"apollo-upload-client": "^17.0.0",
"dayjs": "^1.11.7",
"graphql": "^16.6.0",
+ "ioredis": "^5.3.2",
"js-cookie": "^3.0.1",
"lodash-es": "^4.17.21",
"mitt": "^3.0.0",
diff --git a/packages/frontend-2/pages/downloads.vue b/packages/frontend-2/pages/downloads.vue
index e301a336f..2d4111afd 100644
--- a/packages/frontend-2/pages/downloads.vue
+++ b/packages/frontend-2/pages/downloads.vue
@@ -54,7 +54,6 @@
diff --git a/packages/frontend-2/plugins/004-redis.server.ts b/packages/frontend-2/plugins/004-redis.server.ts
new file mode 100644
index 000000000..2b4a37838
--- /dev/null
+++ b/packages/frontend-2/plugins/004-redis.server.ts
@@ -0,0 +1,15 @@
+import { Redis } from 'ioredis'
+
+/**
+ * Provide redis (only in SSR)
+ */
+export default defineNuxtPlugin(() => {
+ const { redisUrl } = useRuntimeConfig()
+ const redis = redisUrl?.length ? new Redis(redisUrl) : undefined
+
+ return {
+ provide: {
+ redis
+ }
+ }
+})
diff --git a/packages/frontend-2/plugins/cache.ts b/packages/frontend-2/plugins/cache.ts
new file mode 100644
index 000000000..53b16bd06
--- /dev/null
+++ b/packages/frontend-2/plugins/cache.ts
@@ -0,0 +1,179 @@
+/* eslint-disable @typescript-eslint/require-await */
+import type { Optional } from '@speckle/shared'
+import { has as objectHas } from 'lodash-es'
+import type { Redis } from 'ioredis'
+
+type AsyncCacheInterface = {
+ has(key: string): Promise
+ get(key: string): Promise
+ set(key: string, val: V, options?: { expiryMs: number }): Promise
+ setMultiple(
+ keyVals: Record,
+ options?: { expiryMs: number }
+ ): Promise
+ getMultiple(keys: string[]): Promise>
+}
+
+let internalCache: Optional = undefined
+
+const getOrInitInternalCache = async (params: { redis: Optional }) => {
+ if (internalCache) return internalCache
+
+ if (params.redis) {
+ const client = params.redis
+ const redisKeyPrefix = 'fe2-app-cache:'
+ const finalKey = (key: string) => redisKeyPrefix + key
+
+ internalCache = {
+ has: async (key) => {
+ const exists = await client.exists(finalKey(key))
+ return !!exists
+ },
+ set: async (key, val, options) => {
+ if (options?.expiryMs) {
+ await client.set(finalKey(key), JSON.stringify(val), 'PX', options.expiryMs)
+ } else {
+ await client.set(finalKey(key), JSON.stringify(val))
+ }
+ },
+ get: async (key: string) => {
+ const val = await client.get(finalKey(key))
+ if (!val) return undefined
+
+ return JSON.parse(val) as V
+ },
+ setMultiple: async (keyVals, options) => {
+ const entries = Object.entries(keyVals).map(([key, val]) => [
+ finalKey(key),
+ JSON.stringify(val)
+ ])
+
+ if (options?.expiryMs) {
+ await client.mset(...entries.flat(), 'PX', options.expiryMs)
+ } else {
+ await client.mset(...entries.flat())
+ }
+ },
+ getMultiple: async (keys) => {
+ if (!keys?.length) return {}
+
+ const finalKeys = keys.map(finalKey)
+ const vals = await client.mget(...finalKeys)
+ const keyVals = {} as Record
+ for (let i = 0; i < keys.length; i++) {
+ const key = keys[i]
+ const val = vals[i]
+ if (!val) continue
+
+ keyVals[key] = JSON.parse(val)
+ }
+
+ return keyVals
+ }
+ }
+ } else {
+ const cache: Record = {}
+
+ internalCache = {
+ has: async (key) => objectHas(cache, key),
+ set: async (key, val, options) => {
+ cache[key] = val
+
+ if (options?.expiryMs) {
+ setTimeout(() => {
+ delete cache[key]
+ }, options.expiryMs)
+ }
+ },
+ get: async (key: string) => {
+ if (!objectHas(cache, key)) return undefined
+
+ const val = cache[key] as V
+ return val
+ },
+ setMultiple: async (keyVals, options) => {
+ Object.assign(cache, keyVals)
+
+ if (options?.expiryMs) {
+ setTimeout(() => {
+ for (const key of Object.keys(keyVals)) {
+ delete cache[key]
+ }
+ }, options.expiryMs)
+ }
+ },
+ getMultiple: async (keys) => {
+ const keyVals = {} as Record
+ for (const key of keys) {
+ if (!objectHas(cache, key)) continue
+
+ keyVals[key] = cache[key]
+ }
+
+ return keyVals
+ }
+ }
+ }
+
+ return internalCache
+}
+
+/**
+ * In SSR: Provides a redis cache that is shared across app processes and requests
+ * In CSR: Provides an in-memory cache that is shared across the app session
+ */
+export default defineNuxtPlugin(async (nuxtApp) => {
+ const internalCache = await getOrInitInternalCache({
+ redis: nuxtApp.$redis as Redis
+ })
+ const reqTouched: Record = {}
+
+ if (process.server) {
+ nuxtApp.hook('app:rendered', async () => {
+ const touchedKeys = Object.keys(reqTouched)
+ const cacheToSend = await internalCache.getMultiple(touchedKeys)
+
+ nuxtApp.ssrContext!.payload.appCache = cacheToSend
+ })
+ } else if (process.client) {
+ const restorable = window.__NUXT__?.appCache as Optional>
+ if (restorable) {
+ await internalCache.setMultiple(restorable)
+ }
+ }
+
+ const finalCache: AsyncCacheInterface = {
+ has: async (key) => {
+ const has = await internalCache.has(key)
+ return has
+ },
+ set: async (key, val, options) => {
+ await internalCache.set(key, val, options)
+ reqTouched[key] = true
+ },
+ get: async (key: string) => {
+ const val = await internalCache.get(key)
+ reqTouched[key] = true
+ return val
+ },
+ setMultiple: async (keyVals, options) => {
+ await internalCache.setMultiple(keyVals, options)
+ for (const key of Object.keys(keyVals)) {
+ reqTouched[key] = true
+ }
+ },
+ getMultiple: async (keys) => {
+ const keyVals = await internalCache.getMultiple(keys)
+ for (const key of keys) {
+ reqTouched[key] = true
+ }
+ return keyVals
+ }
+ }
+
+ return {
+ provide: {
+ appCache: finalCache
+ }
+ }
+})
diff --git a/packages/server/modules/comments/graph/resolvers/comments.js b/packages/server/modules/comments/graph/resolvers/comments.js
index 739642099..469df106d 100644
--- a/packages/server/modules/comments/graph/resolvers/comments.js
+++ b/packages/server/modules/comments/graph/resolvers/comments.js
@@ -86,7 +86,16 @@ module.exports = {
}
},
Comment: {
- async replies(parent, args) {
+ async replies(parent, args, ctx) {
+ // If limit=0, short-cut full execution and use data loader
+ if (args.limit === 0) {
+ return {
+ totalCount: await ctx.loaders.comments.getReplyCount.load(parent.id),
+ items: [],
+ cursor: null
+ }
+ }
+
const resources = [{ resourceId: parent.id, resourceType: 'comment' }]
return await getComments({
resources,
diff --git a/packages/server/modules/core/graph/resolvers/models.ts b/packages/server/modules/core/graph/resolvers/models.ts
index 03d9738b5..d229ca2ed 100644
--- a/packages/server/modules/core/graph/resolvers/models.ts
+++ b/packages/server/modules/core/graph/resolvers/models.ts
@@ -28,7 +28,16 @@ import { CommitNotFoundError } from '@/modules/core/errors/commit'
export = {
Project: {
- async models(parent, args) {
+ async models(parent, args, ctx) {
+ // If limit=0 & no filter, short-cut full execution and use data loader
+ if (args.limit === 0 && !args.filter) {
+ return {
+ totalCount: await ctx.loaders.streams.getBranchCount.load(parent.id),
+ items: [],
+ cursor: null
+ }
+ }
+
return await getPaginatedProjectModels(parent.id, args)
},
async model(_parent, args, ctx) {
@@ -58,7 +67,18 @@ export = {
loadedVersionsOnly
})
},
- async versions(parent, args) {
+ async versions(parent, args, ctx) {
+ // If limit=0, short-cut full execution and use data loader
+ if (args.limit === 0) {
+ return {
+ totalCount: await ctx.loaders.streams.getCommitCountWithoutGlobals.load(
+ parent.id
+ ),
+ items: [],
+ cursor: null
+ }
+ }
+
return await getPaginatedStreamCommits(parent.id, args)
}
},
@@ -83,7 +103,16 @@ export = {
async displayName(parent) {
return last(parent.name.split('/'))
},
- async versions(parent, args) {
+ async versions(parent, args, ctx) {
+ // If limit=0 & no filter, short-cut full execution and use data loader
+ if (!args.filter && args.limit === 0) {
+ return {
+ totalCount: await ctx.loaders.branches.getCommitCount.load(parent.id),
+ items: [],
+ cursor: null
+ }
+ }
+
return await getPaginatedBranchCommits({
branchId: parent.id,
cursor: args.cursor,
diff --git a/packages/shared/src/observability/index.ts b/packages/shared/src/observability/index.ts
index 93ca112fd..914f44207 100644
--- a/packages/shared/src/observability/index.ts
+++ b/packages/shared/src/observability/index.ts
@@ -37,7 +37,7 @@ export function getLogger(
if (pretty) {
pinoOptions.transport = {
- target: '../pinoPrettyTransport.js',
+ target: '@speckle/shared/pinoPrettyTransport.js',
options: {
colorize: true,
destination: 2, //stderr
diff --git a/utils/1click_image_scripts/template-docker-compose.yml b/utils/1click_image_scripts/template-docker-compose.yml
index 4ab95219b..754e2700e 100644
--- a/utils/1click_image_scripts/template-docker-compose.yml
+++ b/utils/1click_image_scripts/template-docker-compose.yml
@@ -45,6 +45,7 @@ services:
NUXT_PUBLIC_SERVER_NAME: 'TODO: change' # e.g. 'my-speckle-server'
NUXT_PUBLIC_API_ORIGIN: 'TODO: change' # e.g. 'http://127.0.0.1'
NUXT_PUBLIC_BACKEND_API_ORIGIN: 'http://speckle-server:3000'
+ NUXT_REDIS_URL: 'redis://redis'
speckle-server:
image: speckle/speckle-server:2
diff --git a/utils/helm/speckle-server/templates/frontend_2/deployment.yml b/utils/helm/speckle-server/templates/frontend_2/deployment.yml
index ac8b46126..321fdb57c 100644
--- a/utils/helm/speckle-server/templates/frontend_2/deployment.yml
+++ b/utils/helm/speckle-server/templates/frontend_2/deployment.yml
@@ -79,6 +79,11 @@ spec:
value: {{ .Values.frontend_2.logClientApiEndpoint }}
- name: NODE_TLS_REJECT_UNAUTHORIZED
value: {{ .Values.tlsRejectUnauthorized | quote }}
+ - name: NUXT_REDIS_URL
+ valueFrom:
+ secretKeyRef:
+ name: {{ default .Values.secretName .Values.redis.connectionString.secretName }}
+ key: {{ default "redis_url" .Values.redis.connectionString.secretKey }}
priorityClassName: high-priority
{{- if .Values.frontend_2.affinity }}
diff --git a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml
index d3895dd2b..e187a5bf0 100644
--- a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml
+++ b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.cilium.yml
@@ -43,4 +43,6 @@ spec:
- ports:
- port: {{ printf "%s" ( include "server.port" $ | quote ) }}
{{- end -}}
+ # redis
+{{ include "speckle.networkpolicy.egress.redis.cilium" $ | indent 4 }}
{{- end -}}
diff --git a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml
index f43a82874..0d46180ea 100644
--- a/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml
+++ b/utils/helm/speckle-server/templates/frontend_2/networkpolicy.kubernetes.yml
@@ -42,4 +42,6 @@ spec:
ports:
- port: 3000
{{- end -}}
+ # redis
+{{ include "speckle.networkpolicy.egress.redis" $ | indent 4 }}
{{- end -}}
diff --git a/yarn.lock b/yarn.lock
index 86e33601e..57bcfb3b5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -13626,6 +13626,7 @@ __metadata:
eslint-plugin-vue: ^9.18.1
eslint-plugin-vuejs-accessibility: ^1.2.0
graphql: ^16.6.0
+ ioredis: ^5.3.2
jest: 27
js-cookie: ^3.0.1
lodash-es: ^4.17.21