feat: show historical model uploads (#4954)
* ensure modelId is always filled * BE seems fine * frontendd * pagination fix * table max height * lint fix * unset tiny limit * test fix
This commit is contained in:
committed by
GitHub
parent
4738b97091
commit
2be1592341
@@ -30,10 +30,15 @@
|
||||
@deleted="$emit('model-updated')"
|
||||
/>
|
||||
<ProjectModelPageDialogEmbed
|
||||
v-model:open="embedDialogOpen"
|
||||
v-model:open="isEmbedDialogOpen"
|
||||
:project="project"
|
||||
:model-id="model.id"
|
||||
/>
|
||||
<ProjectPageModelsUploadsDialog
|
||||
v-model:open="isUploadsDialogOpen"
|
||||
:project-id="project.id"
|
||||
:model-id="model.id"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -88,7 +93,8 @@ enum ActionTypes {
|
||||
ViewVersions = 'view-versions',
|
||||
UploadVersion = 'upload-version',
|
||||
CopyId = 'copy-id',
|
||||
Embed = 'embed'
|
||||
Embed = 'embed',
|
||||
ViewUploads = 'view-uploads'
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -115,7 +121,6 @@ const { statusIsCanceled } = useWorkspacePlan(props.project.workspace?.slug || '
|
||||
|
||||
const showActionsMenu = ref(false)
|
||||
const openDialog = ref(null as Nullable<ActionTypes>)
|
||||
const embedDialogOpen = ref(false)
|
||||
|
||||
const canEdit = computed(() => props.model.permissions.canUpdate)
|
||||
const canDelete = computed(() => props.model.permissions.canDelete)
|
||||
@@ -166,6 +171,10 @@ const actionsItems = computed<LayoutMenuItem[][]>(() => [
|
||||
title: 'View versions',
|
||||
id: ActionTypes.ViewVersions
|
||||
},
|
||||
{
|
||||
title: 'View uploads',
|
||||
id: ActionTypes.ViewUploads
|
||||
},
|
||||
...(isLoggedIn.value
|
||||
? [
|
||||
{
|
||||
@@ -205,6 +214,14 @@ const isDeleteDialogOpen = computed({
|
||||
get: () => openDialog.value === ActionTypes.Delete,
|
||||
set: (isOpen) => (openDialog.value = isOpen ? ActionTypes.Delete : null)
|
||||
})
|
||||
const isEmbedDialogOpen = computed({
|
||||
get: () => openDialog.value === ActionTypes.Embed,
|
||||
set: (isOpen) => (openDialog.value = isOpen ? ActionTypes.Embed : null)
|
||||
})
|
||||
const isUploadsDialogOpen = computed({
|
||||
get: () => openDialog.value === ActionTypes.ViewUploads,
|
||||
set: (isOpen) => (openDialog.value = isOpen ? ActionTypes.ViewUploads : null)
|
||||
})
|
||||
|
||||
const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) => {
|
||||
const { item } = params
|
||||
@@ -212,6 +229,8 @@ const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) =>
|
||||
switch (item.id) {
|
||||
case ActionTypes.Rename:
|
||||
case ActionTypes.Delete:
|
||||
case ActionTypes.Embed:
|
||||
case ActionTypes.ViewUploads:
|
||||
openDialog.value = item.id
|
||||
break
|
||||
case ActionTypes.Share:
|
||||
@@ -227,9 +246,6 @@ const onActionChosen = (params: { item: LayoutMenuItem; event: MouseEvent }) =>
|
||||
case ActionTypes.CopyId:
|
||||
copy(props.model.id, { successMessage: 'Copied model ID to clipboard' })
|
||||
break
|
||||
case ActionTypes.Embed:
|
||||
embedDialogOpen.value = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<LayoutDialog v-model:open="open" title="Model upload history" :buttons="buttons">
|
||||
<LayoutTable
|
||||
:columns="[
|
||||
{ id: 'file', header: 'File', classes: 'col-span-5' },
|
||||
{ id: 'size', header: 'Size', classes: 'col-span-2' },
|
||||
{ id: 'status', header: 'Status', classes: 'col-span-2' },
|
||||
{ id: 'date', header: 'Date', classes: 'col-span-2' },
|
||||
{
|
||||
id: 'actions',
|
||||
header: '',
|
||||
classes: 'col-span-1 flex items-center justify-end'
|
||||
}
|
||||
]"
|
||||
:items="items"
|
||||
:loading="isVeryFirstLoading"
|
||||
empty-message="This model has no uploads"
|
||||
:max-height="300"
|
||||
>
|
||||
<template #file="{ item }">
|
||||
<div
|
||||
v-tippy="{
|
||||
content: item.fileName.length > 35 ? item.fileName : undefined,
|
||||
placement: 'top-start',
|
||||
delay: 300
|
||||
}"
|
||||
class="truncate text-foreground"
|
||||
>
|
||||
{{ item.fileName }}
|
||||
</div>
|
||||
</template>
|
||||
<template #size="{ item }">
|
||||
<span class="text-foreground-2">{{ prettyFileSize(item.fileSize) }}</span>
|
||||
</template>
|
||||
<template #status="{ item }">
|
||||
<CommonBadge
|
||||
v-tippy="getStatusOptions(item).tooltip"
|
||||
:color-classes="getStatusOptions(item).colorClasses"
|
||||
>
|
||||
{{ getStatusOptions(item).label }}
|
||||
</CommonBadge>
|
||||
</template>
|
||||
<template #date="{ item }">
|
||||
<span
|
||||
v-tippy="formattedFullDate(item.convertedLastUpdate || item.uploadDate)"
|
||||
class="text-foreground-2"
|
||||
>
|
||||
{{ formattedRelativeDate(item.convertedLastUpdate || item.uploadDate) }}
|
||||
</span>
|
||||
</template>
|
||||
<template #actions="{ item }">
|
||||
<FormButton
|
||||
:icon-left="ArrowDownTrayIcon"
|
||||
hide-text
|
||||
size="sm"
|
||||
color="outline"
|
||||
@click="onDownload(item)"
|
||||
/>
|
||||
</template>
|
||||
<template #loader>
|
||||
<InfiniteLoading
|
||||
v-if="items?.length"
|
||||
:settings="{ identifier }"
|
||||
@infinite="onInfiniteLoad"
|
||||
/>
|
||||
</template>
|
||||
</LayoutTable>
|
||||
</LayoutDialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ArrowDownTrayIcon } from '@heroicons/vue/24/outline'
|
||||
import {
|
||||
FileUploadConvertedStatus,
|
||||
fileUploadConvertedStatusLabels
|
||||
} from '@speckle/shared/blobs'
|
||||
import type { LayoutDialogButton } from '@speckle/ui-components'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type { ProjectPageModelsUploadsDialog_FileUploadFragment } from '~/lib/common/generated/gql/graphql'
|
||||
import { useFileDownload } from '~/lib/core/composables/fileUpload'
|
||||
import { prettyFileSize } from '~~/lib/core/helpers/file'
|
||||
|
||||
graphql(`
|
||||
fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {
|
||||
id
|
||||
convertedStatus
|
||||
convertedMessage
|
||||
fileName
|
||||
fileSize
|
||||
convertedLastUpdate
|
||||
uploadDate
|
||||
uploadComplete
|
||||
branchName
|
||||
}
|
||||
`)
|
||||
|
||||
const getModelUploadsQuery = graphql(`
|
||||
query GetModelUploads(
|
||||
$projectId: String!
|
||||
$modelId: String!
|
||||
$input: GetModelUploadsInput!
|
||||
) {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
model(id: $modelId) {
|
||||
id
|
||||
uploads(input: $input) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
id
|
||||
...ProjectPageModelsUploadsDialog_FileUpload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
const props = defineProps<{
|
||||
projectId: string
|
||||
modelId: string
|
||||
}>()
|
||||
|
||||
const open = defineModel<boolean>('open', { required: true })
|
||||
|
||||
const {
|
||||
identifier,
|
||||
onInfiniteLoad,
|
||||
query: { result },
|
||||
isVeryFirstLoading
|
||||
} = usePaginatedQuery({
|
||||
query: getModelUploadsQuery,
|
||||
baseVariables: computed(() => ({
|
||||
projectId: props.projectId,
|
||||
modelId: props.modelId,
|
||||
input: {
|
||||
cursor: null as string | null
|
||||
}
|
||||
})),
|
||||
options: {
|
||||
enabled: open
|
||||
},
|
||||
resolveKey: (vars) => [vars.projectId, vars.modelId],
|
||||
resolveCurrentResult: (res) => res?.project.model.uploads,
|
||||
resolveNextPageVariables: (baseVars, cursor) => ({
|
||||
...baseVars,
|
||||
input: {
|
||||
...baseVars.input,
|
||||
cursor
|
||||
}
|
||||
}),
|
||||
resolveCursorFromVariables: (vars) => vars.input.cursor
|
||||
})
|
||||
|
||||
const { download } = useFileDownload()
|
||||
|
||||
const items = computed(() => result.value?.project.model.uploads.items)
|
||||
|
||||
const buttons = computed((): LayoutDialogButton[] => [
|
||||
{
|
||||
text: 'Close',
|
||||
onClick: () => {
|
||||
open.value = false
|
||||
}
|
||||
}
|
||||
])
|
||||
|
||||
const getStatusOptions = (item: ProjectPageModelsUploadsDialog_FileUploadFragment) => {
|
||||
let colorClasses: string | undefined = undefined
|
||||
switch (item.convertedStatus) {
|
||||
case FileUploadConvertedStatus.Error:
|
||||
colorClasses = 'bg-danger text-foundation'
|
||||
break
|
||||
case FileUploadConvertedStatus.Converting:
|
||||
colorClasses = 'bg-primary text-foundation'
|
||||
break
|
||||
case FileUploadConvertedStatus.Completed:
|
||||
colorClasses = 'bg-success text-foundation'
|
||||
break
|
||||
case FileUploadConvertedStatus.Queued:
|
||||
colorClasses = 'bg-info text-foundation'
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
label:
|
||||
fileUploadConvertedStatusLabels[
|
||||
item.convertedStatus as FileUploadConvertedStatus
|
||||
],
|
||||
tooltip:
|
||||
item.convertedStatus === FileUploadConvertedStatus.Error
|
||||
? item.convertedMessage
|
||||
: undefined,
|
||||
colorClasses
|
||||
}
|
||||
}
|
||||
|
||||
const onDownload = async (item: ProjectPageModelsUploadsDialog_FileUploadFragment) => {
|
||||
await download({
|
||||
blobId: item.id,
|
||||
fileName: item.fileName,
|
||||
projectId: props.projectId
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -136,10 +136,10 @@ export const usePaginatedQuery = <
|
||||
options,
|
||||
resolveCurrentResult,
|
||||
resolveNextPageVariables,
|
||||
resolveInitialResult,
|
||||
resolveCursorFromVariables
|
||||
resolveInitialResult
|
||||
} = params
|
||||
const cacheBusterKey = ref(0)
|
||||
const loadingCompleted = ref(false)
|
||||
|
||||
// can't be a computed, because we have to invoke it on the result of the fetchMore call,
|
||||
// before the result has been merged into the cache and the results become merged with results
|
||||
@@ -164,6 +164,10 @@ export const usePaginatedQuery = <
|
||||
resolveCurrentResult(useQueryReturn.result.value)
|
||||
)
|
||||
|
||||
const isVeryFirstLoading = computed(
|
||||
() => useQueryReturn.loading.value && !currentResult.value?.items.length
|
||||
)
|
||||
|
||||
const getCursorForNextPage = () => {
|
||||
const currRes = currentResult.value
|
||||
const initRes = resolveInitialResult?.()
|
||||
@@ -174,9 +178,14 @@ export const usePaginatedQuery = <
|
||||
}
|
||||
|
||||
const onInfiniteLoad = async (state: InfiniteLoaderState) => {
|
||||
const loadComplete = () => {
|
||||
state.complete()
|
||||
loadingCompleted.value = true
|
||||
}
|
||||
|
||||
const cursor = getCursorForNextPage()
|
||||
let loadMore = hasMoreToLoad(currentResult.value)
|
||||
if (!loadMore || !cursor) return state.complete()
|
||||
if (!loadMore || !cursor) return loadComplete()
|
||||
|
||||
try {
|
||||
const res = await useQueryReturn.fetchMore({
|
||||
@@ -191,22 +200,24 @@ export const usePaginatedQuery = <
|
||||
|
||||
state.loaded()
|
||||
if (!loadMore) {
|
||||
state.complete()
|
||||
loadComplete()
|
||||
}
|
||||
}
|
||||
|
||||
const bustCache = () => {
|
||||
cacheBusterKey.value++
|
||||
loadingCompleted.value = false
|
||||
}
|
||||
|
||||
// If for some reason the query is invoked w/ baseVariables & null cursor, we should bust the cache,
|
||||
// & reset loader state, cause a refetch was triggered for some reason (maybe a cache eviction)
|
||||
useQueryReturn.onResult(() => {
|
||||
const vars = useQueryReturn.variables.value
|
||||
const cursor = vars ? resolveCursorFromVariables(vars) : undefined
|
||||
// If after the query runs there is still more to load, but loading is marked as complete (which can happen
|
||||
// if cache is evicted and initial query reruns) - we should bust the cache,
|
||||
// & reset loader state, so infinite loader restarts
|
||||
useQueryReturn.onResult((res) => {
|
||||
if (res.loading) return
|
||||
|
||||
if (!cursor) {
|
||||
// TODO: Maybe add check to skip this on initial result? Lets see how well this works first
|
||||
// If more to load & loading completed, bust cache
|
||||
const moreToLoad = hasMoreToLoad(resolveCurrentResult(res?.data))
|
||||
if (moreToLoad && loadingCompleted.value) {
|
||||
bustCache()
|
||||
}
|
||||
})
|
||||
@@ -215,7 +226,8 @@ export const usePaginatedQuery = <
|
||||
query: useQueryReturn,
|
||||
identifier: queryKey,
|
||||
onInfiniteLoad,
|
||||
bustCache
|
||||
bustCache,
|
||||
isVeryFirstLoading
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,6 +92,8 @@ type Documents = {
|
||||
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": typeof types.ProjectModelsPageResults_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n ...UseCanCreateModel_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\n }\n": typeof types.SingleLevelModelTreeItemFragmentDoc,
|
||||
"\n fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n uploadDate\n uploadComplete\n branchName\n }\n": typeof types.ProjectPageModelsUploadsDialog_FileUploadFragmentDoc,
|
||||
"\n query GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n": typeof types.GetModelUploadsDocument,
|
||||
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": typeof types.ProjectPageModelsCardDeleteDialogFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCardRenameDialog on Model {\n id\n name\n description\n }\n": typeof types.ProjectPageModelsCardRenameDialogFragmentDoc,
|
||||
"\n query ProjectPageSettingsGeneral($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageSettingsGeneralBlockProjectInfo_Project\n ...ProjectPageSettingsGeneralBlockAccess_Project\n ...ProjectPageSettingsGeneralBlockDiscussions_Project\n ...ProjectPageSettingsGeneralBlockLeave_Project\n ...ProjectPageSettingsGeneralBlockDelete_Project\n ...ProjectPageTeamInternals_Project\n }\n }\n": typeof types.ProjectPageSettingsGeneralDocument,
|
||||
@@ -540,6 +542,8 @@ const documents: Documents = {
|
||||
"\n fragment ProjectModelsPageResults_Project on Project {\n ...ProjectPageLatestItemsModels\n }\n": types.ProjectModelsPageResults_ProjectFragmentDoc,
|
||||
"\n fragment ProjectPageModelsStructureItem_Project on Project {\n id\n ...ProjectPageModelsActions_Project\n ...ProjectCardImportFileArea_Project\n ...UseCanCreateModel_Project\n permissions {\n canCreateModel {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ProjectPageModelsStructureItem_ProjectFragmentDoc,
|
||||
"\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\n }\n": types.SingleLevelModelTreeItemFragmentDoc,
|
||||
"\n fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n uploadDate\n uploadComplete\n branchName\n }\n": types.ProjectPageModelsUploadsDialog_FileUploadFragmentDoc,
|
||||
"\n query GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n": types.GetModelUploadsDocument,
|
||||
"\n fragment ProjectPageModelsCardDeleteDialog on Model {\n id\n name\n }\n": types.ProjectPageModelsCardDeleteDialogFragmentDoc,
|
||||
"\n fragment ProjectPageModelsCardRenameDialog on Model {\n id\n name\n description\n }\n": types.ProjectPageModelsCardRenameDialogFragmentDoc,
|
||||
"\n query ProjectPageSettingsGeneral($projectId: String!) {\n project(id: $projectId) {\n id\n ...ProjectPageSettingsGeneralBlockProjectInfo_Project\n ...ProjectPageSettingsGeneralBlockAccess_Project\n ...ProjectPageSettingsGeneralBlockDiscussions_Project\n ...ProjectPageSettingsGeneralBlockLeave_Project\n ...ProjectPageSettingsGeneralBlockDelete_Project\n ...ProjectPageTeamInternals_Project\n }\n }\n": types.ProjectPageSettingsGeneralDocument,
|
||||
@@ -1236,6 +1240,14 @@ export function graphql(source: "\n fragment ProjectPageModelsStructureItem_Pro
|
||||
* 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 SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\n }\n"): (typeof documents)["\n fragment SingleLevelModelTreeItem on ModelsTreeItem {\n id\n name\n fullName\n model {\n ...ProjectPageLatestItemsModelItem\n ...ProjectCardImportFileArea_Model\n }\n hasChildren\n updatedAt\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 fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n uploadDate\n uploadComplete\n branchName\n }\n"): (typeof documents)["\n fragment ProjectPageModelsUploadsDialog_FileUpload on FileUpload {\n id\n convertedStatus\n convertedMessage\n fileName\n fileSize\n convertedLastUpdate\n uploadDate\n uploadComplete\n branchName\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 GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n"): (typeof documents)["\n query GetModelUploads(\n $projectId: String!\n $modelId: String!\n $input: GetModelUploadsInput!\n ) {\n project(id: $projectId) {\n id\n model(id: $modelId) {\n id\n uploads(input: $input) {\n totalCount\n cursor\n items {\n id\n ...ProjectPageModelsUploadsDialog_FileUpload\n }\n }\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -230,6 +230,10 @@ function createCache(): InMemoryCache {
|
||||
},
|
||||
permissions: {
|
||||
merge: mergeAsObjectsFunction
|
||||
},
|
||||
uploads: {
|
||||
keyArgs: ['input', ['limit']],
|
||||
merge: buildAbstractCollectionMergeFunction('FileUploadCollection')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"datadog:publish-sourcemaps:dev": "DATADOG_SITE=\"datadoghq.eu\" datadog-ci sourcemaps upload ./.output/public/_nuxt --service=\"fe2-dev/test\" --release-version=\"unknown\" --minified-path-prefix=/_nuxt"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.12.4",
|
||||
"@apollo/client": "^3.13.8",
|
||||
"@artmizu/nuxt-prometheus": "^2.2.1",
|
||||
"@datadog/browser-rum": "^5.11.0",
|
||||
"@headlessui/vue": "^1.7.13",
|
||||
@@ -55,7 +55,7 @@
|
||||
"@tiptap/suggestion": "2.10.3",
|
||||
"@tiptap/vue-3": "2.10.3",
|
||||
"@tryghost/content-api": "^1.11.21",
|
||||
"@vue/apollo-composable": "npm:@speckle/apollo-composable@4.2.1-patch.1",
|
||||
"@vue/apollo-composable": "^4.2.2",
|
||||
"@vue/apollo-ssr": "4.0.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"apollo-upload-client": "^18.0.1",
|
||||
|
||||
@@ -23,11 +23,33 @@ extend type Project {
|
||||
pendingImportedModels(limit: Int = 25): [FileUpload!]!
|
||||
}
|
||||
|
||||
type FileUploadCollection {
|
||||
totalCount: Int!
|
||||
cursor: String
|
||||
items: [FileUpload!]!
|
||||
}
|
||||
|
||||
input GetModelUploadsInput {
|
||||
"""
|
||||
The maximum number of uploads to return.
|
||||
"""
|
||||
limit: Int = 25
|
||||
"""
|
||||
The cursor for pagination.
|
||||
"""
|
||||
cursor: String
|
||||
}
|
||||
|
||||
extend type Model {
|
||||
"""
|
||||
Returns a list of versions that are being created from a file import
|
||||
"""
|
||||
pendingImportedVersions(limit: Int = 25): [FileUpload!]!
|
||||
|
||||
"""
|
||||
Get all file uploads ever done in this model
|
||||
"""
|
||||
uploads(input: GetModelUploadsInput): FileUploadCollection!
|
||||
}
|
||||
|
||||
type FileUpload {
|
||||
|
||||
@@ -1052,6 +1052,13 @@ export type FileUpload = {
|
||||
userId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type FileUploadCollection = {
|
||||
__typename?: 'FileUploadCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<FileUpload>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type FileUploadMutations = {
|
||||
__typename?: 'FileUploadMutations';
|
||||
/**
|
||||
@@ -1124,6 +1131,13 @@ export type GenerateFileUploadUrlOutput = {
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type GetModelUploadsInput = {
|
||||
/** The cursor for pagination. */
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
/** The maximum number of uploads to return. */
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type InvitableCollaboratorsFilter = {
|
||||
search?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
@@ -1342,6 +1356,8 @@ export type Model = {
|
||||
permissions: ModelPermissionChecks;
|
||||
previewUrl?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** Get all file uploads ever done in this model */
|
||||
uploads: FileUploadCollection;
|
||||
version: Version;
|
||||
versions: VersionCollection;
|
||||
};
|
||||
@@ -1358,6 +1374,11 @@ export type ModelPendingImportedVersionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ModelUploadsArgs = {
|
||||
input?: InputMaybe<GetModelUploadsInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ModelVersionArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
@@ -5417,6 +5438,7 @@ export type ResolversTypes = {
|
||||
EditCommentInput: EditCommentInput;
|
||||
EmailVerificationRequestInput: EmailVerificationRequestInput;
|
||||
FileUpload: ResolverTypeWrapper<FileUploadGraphQLReturn>;
|
||||
FileUploadCollection: ResolverTypeWrapper<Omit<FileUploadCollection, 'items'> & { items: Array<ResolversTypes['FileUpload']> }>;
|
||||
FileUploadMutations: ResolverTypeWrapper<MutationsObjectGraphQLReturn>;
|
||||
Float: ResolverTypeWrapper<Scalars['Float']['output']>;
|
||||
GendoAIRender: ResolverTypeWrapper<GendoAIRenderGraphQLReturn>;
|
||||
@@ -5424,6 +5446,7 @@ export type ResolversTypes = {
|
||||
GendoAIRenderInput: GendoAiRenderInput;
|
||||
GenerateFileUploadUrlInput: GenerateFileUploadUrlInput;
|
||||
GenerateFileUploadUrlOutput: ResolverTypeWrapper<GenerateFileUploadUrlOutput>;
|
||||
GetModelUploadsInput: GetModelUploadsInput;
|
||||
ID: ResolverTypeWrapper<Scalars['ID']['output']>;
|
||||
Int: ResolverTypeWrapper<Scalars['Int']['output']>;
|
||||
InvitableCollaboratorsFilter: InvitableCollaboratorsFilter;
|
||||
@@ -5757,6 +5780,7 @@ export type ResolversParentTypes = {
|
||||
EditCommentInput: EditCommentInput;
|
||||
EmailVerificationRequestInput: EmailVerificationRequestInput;
|
||||
FileUpload: FileUploadGraphQLReturn;
|
||||
FileUploadCollection: Omit<FileUploadCollection, 'items'> & { items: Array<ResolversParentTypes['FileUpload']> };
|
||||
FileUploadMutations: MutationsObjectGraphQLReturn;
|
||||
Float: Scalars['Float']['output'];
|
||||
GendoAIRender: GendoAIRenderGraphQLReturn;
|
||||
@@ -5764,6 +5788,7 @@ export type ResolversParentTypes = {
|
||||
GendoAIRenderInput: GendoAiRenderInput;
|
||||
GenerateFileUploadUrlInput: GenerateFileUploadUrlInput;
|
||||
GenerateFileUploadUrlOutput: GenerateFileUploadUrlOutput;
|
||||
GetModelUploadsInput: GetModelUploadsInput;
|
||||
ID: Scalars['ID']['output'];
|
||||
Int: Scalars['Int']['output'];
|
||||
InvitableCollaboratorsFilter: InvitableCollaboratorsFilter;
|
||||
@@ -6457,6 +6482,13 @@ export type FileUploadResolvers<ContextType = GraphQLContext, ParentType extends
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type FileUploadCollectionResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['FileUploadCollection'] = ResolversParentTypes['FileUploadCollection']> = {
|
||||
cursor?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
items?: Resolver<Array<ResolversTypes['FileUpload']>, ParentType, ContextType>;
|
||||
totalCount?: Resolver<ResolversTypes['Int'], ParentType, ContextType>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
};
|
||||
|
||||
export type FileUploadMutationsResolvers<ContextType = GraphQLContext, ParentType extends ResolversParentTypes['FileUploadMutations'] = ResolversParentTypes['FileUploadMutations']> = {
|
||||
generateUploadUrl?: Resolver<ResolversTypes['GenerateFileUploadUrlOutput'], ParentType, ContextType, RequireFields<FileUploadMutationsGenerateUploadUrlArgs, 'input'>>;
|
||||
startFileImport?: Resolver<ResolversTypes['FileUpload'], ParentType, ContextType, RequireFields<FileUploadMutationsStartFileImportArgs, 'input'>>;
|
||||
@@ -6577,6 +6609,7 @@ export type ModelResolvers<ContextType = GraphQLContext, ParentType extends Reso
|
||||
permissions?: Resolver<ResolversTypes['ModelPermissionChecks'], ParentType, ContextType>;
|
||||
previewUrl?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
updatedAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>;
|
||||
uploads?: Resolver<ResolversTypes['FileUploadCollection'], ParentType, ContextType, Partial<ModelUploadsArgs>>;
|
||||
version?: Resolver<ResolversTypes['Version'], ParentType, ContextType, RequireFields<ModelVersionArgs, 'id'>>;
|
||||
versions?: Resolver<ResolversTypes['VersionCollection'], ParentType, ContextType, RequireFields<ModelVersionsArgs, 'limit'>>;
|
||||
__isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>;
|
||||
@@ -7871,6 +7904,7 @@ export type Resolvers<ContextType = GraphQLContext> = {
|
||||
CurrencyBasedPrices?: CurrencyBasedPricesResolvers<ContextType>;
|
||||
DateTime?: GraphQLScalarType;
|
||||
FileUpload?: FileUploadResolvers<ContextType>;
|
||||
FileUploadCollection?: FileUploadCollectionResolvers<ContextType>;
|
||||
FileUploadMutations?: FileUploadMutationsResolvers<ContextType>;
|
||||
GendoAIRender?: GendoAiRenderResolvers<ContextType>;
|
||||
GendoAIRenderCollection?: GendoAiRenderCollectionResolvers<ContextType>;
|
||||
|
||||
@@ -1032,6 +1032,13 @@ export type FileUpload = {
|
||||
userId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type FileUploadCollection = {
|
||||
__typename?: 'FileUploadCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<FileUpload>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type FileUploadMutations = {
|
||||
__typename?: 'FileUploadMutations';
|
||||
/**
|
||||
@@ -1104,6 +1111,13 @@ export type GenerateFileUploadUrlOutput = {
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type GetModelUploadsInput = {
|
||||
/** The cursor for pagination. */
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
/** The maximum number of uploads to return. */
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type InvitableCollaboratorsFilter = {
|
||||
search?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
@@ -1322,6 +1336,8 @@ export type Model = {
|
||||
permissions: ModelPermissionChecks;
|
||||
previewUrl?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** Get all file uploads ever done in this model */
|
||||
uploads: FileUploadCollection;
|
||||
version: Version;
|
||||
versions: VersionCollection;
|
||||
};
|
||||
@@ -1338,6 +1354,11 @@ export type ModelPendingImportedVersionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ModelUploadsArgs = {
|
||||
input?: InputMaybe<GetModelUploadsInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ModelVersionArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
@@ -18,7 +18,13 @@ export type GetFileInfoV2 = (args: {
|
||||
|
||||
export type SaveUploadFileInput = Pick<
|
||||
FileUploadRecord,
|
||||
'streamId' | 'branchName' | 'userId' | 'fileName' | 'fileType' | 'fileSize'
|
||||
| 'streamId'
|
||||
| 'branchName'
|
||||
| 'userId'
|
||||
| 'fileName'
|
||||
| 'fileType'
|
||||
| 'fileSize'
|
||||
| 'modelId'
|
||||
> & { fileId: string }
|
||||
|
||||
export type SaveUploadFileInputV2 = Pick<
|
||||
@@ -40,6 +46,11 @@ export type SaveUploadFileV2 = (
|
||||
args: SaveUploadFileInputV2
|
||||
) => Promise<FileUploadRecordV2>
|
||||
|
||||
export type UpdateFileUpload = (args: {
|
||||
id: string
|
||||
upload: Partial<FileUploadRecord>
|
||||
}) => Promise<FileUploadRecord>
|
||||
|
||||
export type GarbageCollectPendingUploadedFiles = (args: {
|
||||
timeoutThresholdSeconds: number
|
||||
}) => Promise<FileUploadRecord[]>
|
||||
@@ -79,3 +90,28 @@ export type RegisterUploadCompleteAndStartFileImport = (args: {
|
||||
expectedETag: string
|
||||
maximumFileSize: number
|
||||
}) => Promise<FileUploadRecordV2 & { modelName: string }>
|
||||
|
||||
export type GetModelUploadsBaseArgs = {
|
||||
projectId: string
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export type GetModelUploadsArgs = GetModelUploadsBaseArgs & {
|
||||
limit?: number
|
||||
cursor?: string | null
|
||||
}
|
||||
|
||||
export type GetModelUploadsItems = (params: GetModelUploadsArgs) => Promise<{
|
||||
items: FileUploadRecord[]
|
||||
cursor: string | null
|
||||
}>
|
||||
|
||||
export type GetModelUploadsTotalCount = (
|
||||
params: GetModelUploadsBaseArgs
|
||||
) => Promise<number>
|
||||
|
||||
export type GetModelUploads = (params: GetModelUploadsArgs) => Promise<{
|
||||
items: FileUploadRecord[]
|
||||
totalCount: number
|
||||
cursor: string | null
|
||||
}>
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
getBranchPendingVersionsFactory,
|
||||
getFileInfoFactory,
|
||||
getFileInfoFactoryV2,
|
||||
getModelUploadsItemsFactory,
|
||||
getModelUploadsTotalCountFactory,
|
||||
getStreamFileUploadsFactory,
|
||||
getStreamPendingModelsFactory,
|
||||
saveUploadFileFactory,
|
||||
@@ -232,6 +234,7 @@ const fileUploadMutations: Resolvers['FileUploadMutations'] = {
|
||||
}
|
||||
}
|
||||
}
|
||||
import { getModelUploadsFactory } from '@/modules/fileuploads/services/management'
|
||||
|
||||
export = {
|
||||
Stream: {
|
||||
@@ -260,6 +263,20 @@ export = {
|
||||
parent.name,
|
||||
args
|
||||
)
|
||||
},
|
||||
async uploads(parent, args) {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
||||
const getModelUploads = getModelUploadsFactory({
|
||||
getModelUploadsItems: getModelUploadsItemsFactory({ db: projectDb }),
|
||||
getModelUploadsTotalCount: getModelUploadsTotalCountFactory({ db: projectDb })
|
||||
})
|
||||
|
||||
return await getModelUploads({
|
||||
modelId: parent.id,
|
||||
projectId: parent.streamId,
|
||||
limit: args.input?.limit ?? 25,
|
||||
cursor: args.input?.cursor
|
||||
})
|
||||
}
|
||||
},
|
||||
FileUpload: {
|
||||
@@ -267,11 +284,19 @@ export = {
|
||||
modelName: (parent) => parent.branchName,
|
||||
convertedVersionId: (parent) => parent.convertedCommitId,
|
||||
async model(parent, _args, ctx) {
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.streamId })
|
||||
const { streamId, modelId, branchName } = parent
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: streamId })
|
||||
if (modelId) {
|
||||
return await ctx.loaders
|
||||
.forRegion({ db: projectDb })
|
||||
.branches.getById.load(modelId)
|
||||
}
|
||||
|
||||
return await ctx.loaders
|
||||
.forRegion({ db: projectDb })
|
||||
.streams.getStreamBranchByName.forStream(parent.streamId)
|
||||
.load(parent.branchName.toLowerCase())
|
||||
.streams.getStreamBranchByName.forStream(streamId)
|
||||
.load(branchName.toLowerCase())
|
||||
}
|
||||
},
|
||||
Mutation: {
|
||||
|
||||
@@ -18,7 +18,8 @@ import { listenFor } from '@/modules/core/utils/dbNotificationListener'
|
||||
import { getEventBus } from '@/modules/shared/services/eventBus'
|
||||
import {
|
||||
expireOldPendingUploadsFactory,
|
||||
getFileInfoFactory
|
||||
getFileInfoFactory,
|
||||
updateFileUploadFactory
|
||||
} from '@/modules/fileuploads/repositories/fileUploads'
|
||||
import { db } from '@/db/knex'
|
||||
import { getFileImportTimeLimitMinutes } from '@/modules/shared/helpers/envHelper'
|
||||
@@ -137,6 +138,7 @@ export const init: SpeckleModule['init'] = async ({ app, isInitial }) => {
|
||||
getFileInfo: getFileInfoFactory({ db: projectDb }),
|
||||
publish,
|
||||
getStreamBranchByName: getStreamBranchByNameFactory({ db: projectDb }),
|
||||
updateFileUpload: updateFileUploadFactory({ db: projectDb }),
|
||||
eventEmit: getEventBus().emit
|
||||
})(parsedMessage)
|
||||
})
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import { Knex } from 'knex'
|
||||
|
||||
const TABLE_NAME = 'file_uploads'
|
||||
const METADATA_FIELD = 'metadata'
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
await knex.schema.table(TABLE_NAME, (table) => {
|
||||
table.dropColumn(METADATA_FIELD)
|
||||
})
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
await knex.schema.table(TABLE_NAME, (table) => {
|
||||
table.json(METADATA_FIELD).nullable().defaultTo(null)
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Branches, FileUploads, knex } from '@/modules/core/dbSchema'
|
||||
import {
|
||||
UpdateFileStatus,
|
||||
GarbageCollectPendingUploadedFiles,
|
||||
GetFileInfo,
|
||||
SaveUploadFile,
|
||||
SaveUploadFileV2,
|
||||
SaveUploadFileInput,
|
||||
SaveUploadFileInputV2,
|
||||
GetFileInfoV2
|
||||
GetFileInfoV2,
|
||||
UpdateFileUpload,
|
||||
GetModelUploadsItems,
|
||||
GetModelUploadsBaseArgs,
|
||||
GetModelUploadsTotalCount
|
||||
} from '@/modules/fileuploads/domain/operations'
|
||||
import {
|
||||
FileUploadConvertedStatus,
|
||||
@@ -16,11 +19,19 @@ import {
|
||||
} from '@/modules/fileuploads/helpers/types'
|
||||
import { Knex } from 'knex'
|
||||
import { FileImportJobNotFoundError } from '@/modules/fileuploads/helpers/errors'
|
||||
import { compositeCursorTools } from '@/modules/shared/helpers/graphqlHelper'
|
||||
import { clamp } from 'lodash'
|
||||
|
||||
const tables = {
|
||||
fileUploads: (db: Knex) => db<FileUploadRecord>(FileUploads.name)
|
||||
}
|
||||
|
||||
const getCursorTools = () =>
|
||||
compositeCursorTools({
|
||||
schema: FileUploads,
|
||||
cols: ['convertedLastUpdate', 'id']
|
||||
})
|
||||
|
||||
export const getFileInfoFactory =
|
||||
(deps: { db: Knex }): GetFileInfo =>
|
||||
async (params) => {
|
||||
@@ -100,7 +111,8 @@ export const saveUploadFileFactory =
|
||||
userId,
|
||||
fileName,
|
||||
fileType,
|
||||
fileSize
|
||||
fileSize,
|
||||
modelId
|
||||
}: SaveUploadFileInput) => {
|
||||
const dbFile: Partial<FileUploadRecord> = {
|
||||
id: fileId,
|
||||
@@ -110,7 +122,8 @@ export const saveUploadFileFactory =
|
||||
fileName,
|
||||
fileType,
|
||||
fileSize,
|
||||
uploadComplete: true
|
||||
uploadComplete: true,
|
||||
modelId
|
||||
}
|
||||
const [newRecord] = await tables.fileUploads(deps.db).insert(dbFile, '*')
|
||||
return newRecord as FileUploadRecord
|
||||
@@ -232,23 +245,61 @@ export const getBranchPendingVersionsFactory =
|
||||
return await q
|
||||
}
|
||||
|
||||
export const updateFileStatusFactory =
|
||||
(deps: { db: Knex }): UpdateFileStatus =>
|
||||
export const updateFileUploadFactory =
|
||||
(deps: { db: Knex }): UpdateFileUpload =>
|
||||
async (params) => {
|
||||
const { fileId, status, convertedMessage, convertedCommitId } = params
|
||||
const fileInfos = await tables
|
||||
const { id, upload } = params
|
||||
const updatedFile = await tables
|
||||
.fileUploads(deps.db)
|
||||
.update<FileUploadRecord[]>({
|
||||
[FileUploads.withoutTablePrefix.col.convertedStatus]: status,
|
||||
[FileUploads.withoutTablePrefix.col.convertedLastUpdate]: knex.fn.now(),
|
||||
[FileUploads.withoutTablePrefix.col.convertedMessage]: convertedMessage,
|
||||
[FileUploads.withoutTablePrefix.col.convertedCommitId]: convertedCommitId
|
||||
})
|
||||
.where({ [FileUploads.withoutTablePrefix.col.id]: fileId })
|
||||
.update(upload)
|
||||
.where({ [FileUploads.col.id]: id })
|
||||
.returning<FileUploadRecord[]>('*')
|
||||
|
||||
if (fileInfos.length === 0) {
|
||||
throw new FileImportJobNotFoundError(`File with id ${fileId} not found`)
|
||||
if (updatedFile.length === 0) {
|
||||
throw new FileImportJobNotFoundError(`File with id ${id} not found`)
|
||||
}
|
||||
return fileInfos[0]
|
||||
return updatedFile[0]
|
||||
}
|
||||
|
||||
const getModelUploadsBaseQueryFactory =
|
||||
(deps: { db: Knex }) => (params: GetModelUploadsBaseArgs) => {
|
||||
const { projectId, modelId } = params
|
||||
const q = tables
|
||||
.fileUploads(deps.db)
|
||||
.where(FileUploads.col.streamId, projectId)
|
||||
.andWhere(FileUploads.col.modelId, modelId)
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
export const getModelUploadsItemsFactory =
|
||||
(deps: { db: Knex }): GetModelUploadsItems =>
|
||||
async (params) => {
|
||||
const limit = clamp(params.limit || 0, 0, 100)
|
||||
const { filterByCursor, resolveNewCursor } = getCursorTools()
|
||||
|
||||
const q = getModelUploadsBaseQueryFactory(deps)(params)
|
||||
.orderBy(FileUploads.col.convertedLastUpdate, 'desc')
|
||||
.limit(limit)
|
||||
|
||||
filterByCursor({
|
||||
query: q,
|
||||
cursor: params.cursor
|
||||
})
|
||||
|
||||
const rows = await q
|
||||
const newCursor = resolveNewCursor(rows)
|
||||
|
||||
return {
|
||||
items: rows,
|
||||
cursor: newCursor
|
||||
}
|
||||
}
|
||||
|
||||
export const getModelUploadsTotalCountFactory =
|
||||
(deps: { db: Knex }): GetModelUploadsTotalCount =>
|
||||
async (params) => {
|
||||
const q = getModelUploadsBaseQueryFactory(deps)(params)
|
||||
const [{ count }] = await q.count()
|
||||
return parseInt(count + '')
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { fileImportResultPayload } from '@speckle/shared/workers/fileimport'
|
||||
import { onFileImportResultFactory } from '@/modules/fileuploads/services/resultHandler'
|
||||
import {
|
||||
saveUploadFileFactoryV2,
|
||||
updateFileStatusFactory
|
||||
updateFileUploadFactory
|
||||
} from '@/modules/fileuploads/repositories/fileUploads'
|
||||
import { validateRequest } from 'zod-express'
|
||||
import { z } from 'zod'
|
||||
@@ -170,7 +170,7 @@ export const nextGenFileImporterRouterFactory = (): Router => {
|
||||
|
||||
const onFileImportResult = onFileImportResultFactory({
|
||||
logger: logger.child({ fileUploadStatus: jobResult.status }),
|
||||
updateFileStatus: updateFileStatusFactory({ db: projectDb }),
|
||||
updateFileUpload: updateFileUploadFactory({ db: projectDb }),
|
||||
publish
|
||||
})
|
||||
|
||||
|
||||
@@ -43,8 +43,11 @@ export const fileuploadRouterFactory = (): Router => {
|
||||
})
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId })
|
||||
const getStreamBranchByName = getStreamBranchByNameFactory({ db: projectDb })
|
||||
const branch = await getStreamBranchByName(projectId, branchName)
|
||||
|
||||
const insertNewUploadAndNotify = insertNewUploadAndNotifyFactory({
|
||||
getStreamBranchByName: getStreamBranchByNameFactory({ db: projectDb }),
|
||||
getStreamBranchByName,
|
||||
saveUploadFile: saveUploadFileFactory({ db: projectDb }),
|
||||
publish,
|
||||
emit: getEventBus().emit
|
||||
@@ -63,11 +66,12 @@ export const fileuploadRouterFactory = (): Router => {
|
||||
await insertNewUploadAndNotify({
|
||||
fileId: upload.blobId,
|
||||
streamId: projectId,
|
||||
branchName,
|
||||
branchName: branch?.name || branchName,
|
||||
userId,
|
||||
fileName: upload.fileName,
|
||||
fileType: upload.fileName?.split('.').pop() || '', //FIXME
|
||||
fileSize: upload.fileSize
|
||||
fileSize: upload.fileSize,
|
||||
modelId: branch?.id || null
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
NotifyChangeInFileStatus,
|
||||
SaveUploadFileV2,
|
||||
PushJobToFileImporter,
|
||||
GetModelUploads,
|
||||
GetModelUploadsItems,
|
||||
GetModelUploadsTotalCount,
|
||||
InsertNewUploadAndNotifyV2,
|
||||
InsertNewUploadAndNotify
|
||||
} from '@/modules/fileuploads/domain/operations'
|
||||
@@ -159,3 +162,23 @@ export const notifyChangeInFileStatus =
|
||||
projectId: streamId
|
||||
})
|
||||
}
|
||||
|
||||
export const getModelUploadsFactory =
|
||||
(deps: {
|
||||
getModelUploadsItems: GetModelUploadsItems
|
||||
getModelUploadsTotalCount: GetModelUploadsTotalCount
|
||||
}): GetModelUploads =>
|
||||
async (params) => {
|
||||
const [{ items, cursor }, totalCount] = await Promise.all([
|
||||
params.limit === 0
|
||||
? { items: [], cursor: null }
|
||||
: deps.getModelUploadsItems(params),
|
||||
deps.getModelUploadsTotalCount(params)
|
||||
])
|
||||
|
||||
return {
|
||||
items,
|
||||
totalCount,
|
||||
cursor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Logger } from '@/observability/logging'
|
||||
import {
|
||||
ProcessFileImportResult,
|
||||
UpdateFileStatus
|
||||
UpdateFileUpload
|
||||
} from '@/modules/fileuploads/domain/operations'
|
||||
import {
|
||||
FileImportSubscriptions,
|
||||
@@ -16,9 +16,10 @@ import {
|
||||
jobResultToConvertedMessage
|
||||
} from '@/modules/fileuploads/helpers/convert'
|
||||
import { ensureError } from '@speckle/shared'
|
||||
import { FileUploadRecord } from '@/modules/fileuploads/helpers/types'
|
||||
|
||||
type OnFileImportResultDeps = {
|
||||
updateFileStatus: UpdateFileStatus
|
||||
updateFileUpload: UpdateFileUpload
|
||||
publish: PublishSubscription
|
||||
logger: Logger
|
||||
}
|
||||
@@ -42,13 +43,16 @@ export const onFileImportResultFactory =
|
||||
convertedCommitId = jobResult.result.versionId
|
||||
}
|
||||
|
||||
let updatedFile
|
||||
let updatedFile: FileUploadRecord
|
||||
try {
|
||||
updatedFile = await deps.updateFileStatus({
|
||||
fileId: jobId,
|
||||
status,
|
||||
convertedMessage,
|
||||
convertedCommitId
|
||||
updatedFile = await deps.updateFileUpload({
|
||||
id: jobId,
|
||||
upload: {
|
||||
convertedStatus: status,
|
||||
convertedLastUpdate: new Date(),
|
||||
convertedMessage,
|
||||
convertedCommitId
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
const err = ensureError(e)
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ProjectPendingModelsUpdatedMessageType,
|
||||
ProjectPendingVersionsUpdatedMessageType
|
||||
} from '@/modules/core/graph/generated/graphql'
|
||||
import { GetFileInfo } from '@/modules/fileuploads/domain/operations'
|
||||
import { GetFileInfo, UpdateFileUpload } from '@/modules/fileuploads/domain/operations'
|
||||
import { GetStreamBranchByName } from '@/modules/core/domain/branches/operations'
|
||||
import { EventBusEmit } from '@/modules/shared/services/eventBus'
|
||||
import { ModelEvents } from '@/modules/core/domain/branches/events'
|
||||
@@ -19,6 +19,7 @@ import { FileUploadInternalError } from '@/modules/fileuploads/helpers/errors'
|
||||
type OnFileImportProcessedDeps = {
|
||||
getFileInfo: GetFileInfo
|
||||
getStreamBranchByName: GetStreamBranchByName
|
||||
updateFileUpload: UpdateFileUpload
|
||||
publish: PublishSubscription
|
||||
eventEmit: EventBusEmit
|
||||
}
|
||||
@@ -45,11 +46,21 @@ export const onFileImportProcessedFactory =
|
||||
|
||||
const [upload, branch] = await Promise.all([
|
||||
deps.getFileInfo({ fileId: uploadId }),
|
||||
isNewBranch ? deps.getStreamBranchByName(streamId, branchName) : null
|
||||
deps.getStreamBranchByName(streamId, branchName)
|
||||
])
|
||||
if (!upload) return
|
||||
if (upload.streamId !== streamId) return
|
||||
|
||||
// Update upload to reference the actual model/branch created
|
||||
if (branch) {
|
||||
await deps.updateFileUpload({
|
||||
id: upload.id,
|
||||
upload: {
|
||||
modelId: branch.id
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (upload.convertedStatus === FileUploadConvertedStatus.Error) {
|
||||
//TODO in future differentiate between internal server errors and user errors
|
||||
const err = new FileUploadInternalError(
|
||||
|
||||
@@ -16,7 +16,8 @@ export const createFileUploadJob = (params: { projectId: string; userId: string
|
||||
userId,
|
||||
fileName: cryptoRandomString({ length: 10 }),
|
||||
fileType: cryptoRandomString({ length: 3 }),
|
||||
fileSize: randomInt(1, 1e6)
|
||||
fileSize: randomInt(1, 1e6),
|
||||
modelId: null
|
||||
}
|
||||
|
||||
return saveUploadFile(data)
|
||||
|
||||
@@ -69,7 +69,8 @@ describe('FileUploads @fileuploads', () => {
|
||||
fileId,
|
||||
fileName: 'testfile.txt',
|
||||
fileSize: 100,
|
||||
fileType: 'text/plain'
|
||||
fileType: 'text/plain',
|
||||
modelId: null
|
||||
})
|
||||
await sleep(2000)
|
||||
await garbageCollector({ logger, timeoutThresholdSeconds: 1 })
|
||||
@@ -98,7 +99,8 @@ describe('FileUploads @fileuploads', () => {
|
||||
fileId,
|
||||
fileName: 'testfile.txt',
|
||||
fileSize: 100,
|
||||
fileType: 'text/plain'
|
||||
fileType: 'text/plain',
|
||||
modelId: null
|
||||
})
|
||||
// timeout far in the future, so it won't be garbage collected
|
||||
await garbageCollector({ logger, timeoutThresholdSeconds: 1 * TIME.hour })
|
||||
@@ -133,7 +135,8 @@ describe('FileUploads @fileuploads', () => {
|
||||
fileId,
|
||||
fileName: 'testfile.txt',
|
||||
fileSize: 100,
|
||||
fileType: 'text/plain'
|
||||
fileType: 'text/plain',
|
||||
modelId: null
|
||||
})
|
||||
|
||||
const results = await getFileInfoFactory({ db })({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { AuthContext } from '@/modules/shared/authz'
|
||||
import { base64Decode, base64Encode } from '@/modules/shared/helpers/cryptoHelper'
|
||||
import DataLoader from 'dataloader'
|
||||
@@ -13,6 +14,8 @@ import {
|
||||
} from '@/modules/shared/errors'
|
||||
import { MaybeNullOrUndefined, Nullable, Optional } from '@speckle/shared'
|
||||
import { Knex } from 'knex'
|
||||
import { SchemaConfig } from '@/modules/core/dbSchema'
|
||||
import { has, isObjectLike, isString, mapValues, pick, times } from 'lodash'
|
||||
|
||||
/**
|
||||
* Encode cursor to turn it into an opaque & obfuscated value
|
||||
@@ -73,12 +76,101 @@ export const decodeCompositeCursor = <C extends object>(
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplifies working with composite cursors in SQL queries. Composite cursors are better because they
|
||||
* allow duplicate values (e.g. updatedAt date) in different rows
|
||||
*/
|
||||
export const compositeCursorTools = <
|
||||
Config extends SchemaConfig<any, any, any>,
|
||||
SelectedCols extends Array<keyof Config['col']>
|
||||
>(args: {
|
||||
schema: Config
|
||||
/**
|
||||
* Order of columns matters - put the primary ordering column first (e.g. updatedAt), then the secondary
|
||||
* ones like the ID.
|
||||
*/
|
||||
cols: SelectedCols
|
||||
}) => {
|
||||
type Cursor = {
|
||||
[Col in SelectedCols[number]]: string
|
||||
}
|
||||
|
||||
type CursorRecord = {
|
||||
[Col in SelectedCols[number]]: string | Date | number | boolean
|
||||
}
|
||||
|
||||
const encode = (val: Cursor) => encodeCompositeCursor(val)
|
||||
const decode = (cursor: MaybeNullOrUndefined<string>): Nullable<Cursor> =>
|
||||
decodeCompositeCursor(
|
||||
cursor,
|
||||
(c) => isObjectLike(c) && args.cols.every((col) => has(c, col))
|
||||
)
|
||||
|
||||
/**
|
||||
* Invoke this on the knex querybuilder to filter the query by the cursor
|
||||
*/
|
||||
const filterByCursor = <Query extends Knex.QueryBuilder>(params: {
|
||||
query: Query
|
||||
/**
|
||||
* If falsy, filter will be skipped
|
||||
*/
|
||||
cursor: MaybeNullOrUndefined<Cursor | string>
|
||||
/**
|
||||
* How the results are sorted. Descending by default.
|
||||
*/
|
||||
sort?: 'desc' | 'asc'
|
||||
}) => {
|
||||
const { query, sort = 'desc' } = params
|
||||
const cursor = isString(params.cursor) ? decode(params.cursor) : params.cursor
|
||||
if (!cursor) return query
|
||||
|
||||
const colCount = args.cols.length
|
||||
|
||||
const sql = `(${times(colCount, () => '??').join(', ')}) ${
|
||||
sort === 'desc' ? '<' : '>'
|
||||
} (${times(colCount, () => '?').join(', ')})` // string like (??, ??) < (?, ?)
|
||||
|
||||
// e.g. WHERE (table.updatedAt, table.id) < ('2023-10-01T00:00:00.000Z', '12345')
|
||||
query.andWhereRaw(sql, [
|
||||
...args.cols.map((col) => args.schema.col[col]),
|
||||
...args.cols.map((col) => cursor[col].toString())
|
||||
])
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
/**
|
||||
* Feed in an entire page of items and this will build the next cursor accordingly
|
||||
*/
|
||||
const resolveNewCursor = (items: Array<CursorRecord>) => {
|
||||
if (!items.length) return null
|
||||
const lastItem = items.at(-1)
|
||||
if (!lastItem) return null
|
||||
|
||||
const cursor: Cursor = mapValues(pick(lastItem, args.cols), (value) => {
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString()
|
||||
}
|
||||
|
||||
return `${value}`
|
||||
})
|
||||
|
||||
return encode(cursor)
|
||||
}
|
||||
|
||||
return {
|
||||
encode,
|
||||
decode,
|
||||
filterByCursor,
|
||||
resolveNewCursor
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* All dataloaders must at the very least follow this type
|
||||
*/
|
||||
export type ModularizedDataLoadersConstraint = {
|
||||
[group: string]: Optional<{
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[loader: string]: DataLoader<any, any> | { clearAll: () => unknown }
|
||||
}>
|
||||
}
|
||||
|
||||
@@ -1033,6 +1033,13 @@ export type FileUpload = {
|
||||
userId: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type FileUploadCollection = {
|
||||
__typename?: 'FileUploadCollection';
|
||||
cursor?: Maybe<Scalars['String']['output']>;
|
||||
items: Array<FileUpload>;
|
||||
totalCount: Scalars['Int']['output'];
|
||||
};
|
||||
|
||||
export type FileUploadMutations = {
|
||||
__typename?: 'FileUploadMutations';
|
||||
/**
|
||||
@@ -1105,6 +1112,13 @@ export type GenerateFileUploadUrlOutput = {
|
||||
url: Scalars['String']['output'];
|
||||
};
|
||||
|
||||
export type GetModelUploadsInput = {
|
||||
/** The cursor for pagination. */
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
/** The maximum number of uploads to return. */
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
};
|
||||
|
||||
export type InvitableCollaboratorsFilter = {
|
||||
search?: InputMaybe<Scalars['String']['input']>;
|
||||
};
|
||||
@@ -1323,6 +1337,8 @@ export type Model = {
|
||||
permissions: ModelPermissionChecks;
|
||||
previewUrl?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
/** Get all file uploads ever done in this model */
|
||||
uploads: FileUploadCollection;
|
||||
version: Version;
|
||||
versions: VersionCollection;
|
||||
};
|
||||
@@ -1339,6 +1355,11 @@ export type ModelPendingImportedVersionsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ModelUploadsArgs = {
|
||||
input?: InputMaybe<GetModelUploadsInput>;
|
||||
};
|
||||
|
||||
|
||||
export type ModelVersionArgs = {
|
||||
id: Scalars['String']['input'];
|
||||
};
|
||||
|
||||
@@ -38,3 +38,22 @@ export const BlobUploadStatus = <const>{
|
||||
}
|
||||
|
||||
export type BlobUploadStatus = (typeof BlobUploadStatus)[keyof typeof BlobUploadStatus]
|
||||
export const FileUploadConvertedStatus = <const>{
|
||||
Queued: 0,
|
||||
Converting: 1,
|
||||
Completed: 2,
|
||||
Error: 3
|
||||
}
|
||||
|
||||
export type FileUploadConvertedStatus =
|
||||
(typeof FileUploadConvertedStatus)[keyof typeof FileUploadConvertedStatus]
|
||||
|
||||
export const fileUploadConvertedStatusLabels: Record<
|
||||
FileUploadConvertedStatus,
|
||||
string
|
||||
> = {
|
||||
[FileUploadConvertedStatus.Queued]: 'Queued',
|
||||
[FileUploadConvertedStatus.Converting]: 'Converting',
|
||||
[FileUploadConvertedStatus.Completed]: 'Completed',
|
||||
[FileUploadConvertedStatus.Error]: 'Error'
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ export default {
|
||||
clickIcon: {
|
||||
action: 'click',
|
||||
type: 'function'
|
||||
},
|
||||
color: {
|
||||
options: ['primary', 'secondary'],
|
||||
control: { type: 'select' }
|
||||
}
|
||||
}
|
||||
} as Meta
|
||||
|
||||
@@ -28,11 +28,7 @@ export const Default: StoryObj = {
|
||||
},
|
||||
template: `
|
||||
<Table
|
||||
:items="args.items"
|
||||
:buttons="args.buttons"
|
||||
:columns="args.columns"
|
||||
:overflow-cells="args.overflowCells"
|
||||
:on-row-click="args.onRowClick"
|
||||
v-bind="args"
|
||||
>
|
||||
<template #name="{ item }">
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -190,7 +186,8 @@ export const Default: StoryObj = {
|
||||
],
|
||||
overflowCells: false,
|
||||
onRowClick: (item: unknown) => console.log('Row clicked', item),
|
||||
roles: ['Admin', 'User', 'Guest']
|
||||
roles: ['Admin', 'User', 'Guest'],
|
||||
maxHeight: undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,3 +225,11 @@ export const NoItems: StoryObj = {
|
||||
items: []
|
||||
}
|
||||
}
|
||||
|
||||
export const WithLimitedHeight: StoryObj = {
|
||||
...Default,
|
||||
args: {
|
||||
...Default.args,
|
||||
maxHeight: 200
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@
|
||||
{{ column.header }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="divide-y divide-outline-3 h-full overflow-visible"
|
||||
:class="{ 'pb-32': overflowCells }"
|
||||
>
|
||||
<div :class="resultContainerClasses" :style="resultContainerStyle">
|
||||
<div
|
||||
v-if="loading || !items"
|
||||
class="flex items-center justify-center py-3"
|
||||
@@ -75,13 +72,14 @@
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="loader" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" generic="T extends {id: string}, C extends string">
|
||||
import { noop, isString } from 'lodash'
|
||||
import { computed } from 'vue'
|
||||
import { computed, type CSSProperties } from 'vue'
|
||||
import type { PropAnyComponent } from '~~/src/helpers/common/components'
|
||||
import { CommonLoadingIcon, FormButton } from '~~/src/lib'
|
||||
import { directive as vTippy } from 'vue-tippy'
|
||||
@@ -111,10 +109,37 @@ const props = withDefaults(
|
||||
rowItemsAlign?: 'center' | 'stretch'
|
||||
emptyMessage?: string
|
||||
loading?: boolean
|
||||
maxHeight?: number
|
||||
}>(),
|
||||
{ rowItemsAlign: 'center', emptyMessage: 'No data found' }
|
||||
)
|
||||
|
||||
const resultContainerClasses = computed(() => {
|
||||
const classParts = ['divide-y divide-outline-3 overflow-visible']
|
||||
|
||||
if (props.overflowCells) {
|
||||
classParts.push('pb-32')
|
||||
}
|
||||
|
||||
if (!props.maxHeight) {
|
||||
classParts.push('h-full overflow-visible')
|
||||
} else {
|
||||
classParts.push('overflow-y-auto simple-scrollbar')
|
||||
}
|
||||
|
||||
return classParts.join(' ')
|
||||
})
|
||||
|
||||
const resultContainerStyle = computed((): CSSProperties => {
|
||||
const style: CSSProperties = {}
|
||||
|
||||
if (props.maxHeight) {
|
||||
style.maxHeight = `${props.maxHeight}px`
|
||||
}
|
||||
|
||||
return style
|
||||
})
|
||||
|
||||
const buttonCount = computed(() => {
|
||||
return (props.buttons || []).length
|
||||
})
|
||||
|
||||
@@ -76,9 +76,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@apollo/client@npm:^3.12.4, @apollo/client@npm:^3.7.0, @apollo/client@npm:^3.8.0":
|
||||
version: 3.12.4
|
||||
resolution: "@apollo/client@npm:3.12.4"
|
||||
"@apollo/client@npm:^3.13.8, @apollo/client@npm:^3.7.0, @apollo/client@npm:^3.8.0":
|
||||
version: 3.13.8
|
||||
resolution: "@apollo/client@npm:3.13.8"
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core": "npm:^3.1.1"
|
||||
"@wry/caches": "npm:^1.0.0"
|
||||
@@ -89,14 +89,13 @@ __metadata:
|
||||
optimism: "npm:^0.18.0"
|
||||
prop-types: "npm:^15.7.2"
|
||||
rehackt: "npm:^0.1.0"
|
||||
response-iterator: "npm:^0.2.6"
|
||||
symbol-observable: "npm:^4.0.0"
|
||||
ts-invariant: "npm:^0.10.3"
|
||||
tslib: "npm:^2.3.0"
|
||||
zen-observable-ts: "npm:^1.2.5"
|
||||
peerDependencies:
|
||||
graphql: ^15.0.0 || ^16.0.0
|
||||
graphql-ws: ^5.5.5
|
||||
graphql-ws: ^5.5.5 || ^6.0.3
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc
|
||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc
|
||||
subscriptions-transport-ws: ^0.9.0 || ^0.11.0
|
||||
@@ -109,7 +108,7 @@ __metadata:
|
||||
optional: true
|
||||
subscriptions-transport-ws:
|
||||
optional: true
|
||||
checksum: 10/9659ccf03ab5d6708cf191a1bd09c33ab5f958a8d66e783f81ccc899448b362a51cecf7d62c0ff5b852e60a340238e6903b3c96dcb5cf68dfb8a438ee6cd24bb
|
||||
checksum: 10/a2fb3990ea25b96df2719ae0e925c07acc84cbf73da8871233169d65f01ae506cc0a52f6a83c40dea06bb3c9a5eb46a34f3e9b6e545931bae8c48f9bf4652e7a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -15748,7 +15747,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@speckle/frontend-2@workspace:packages/frontend-2"
|
||||
dependencies:
|
||||
"@apollo/client": "npm:^3.12.4"
|
||||
"@apollo/client": "npm:^3.13.8"
|
||||
"@artmizu/nuxt-prometheus": "npm:^2.2.1"
|
||||
"@datadog/browser-rum": "npm:^5.11.0"
|
||||
"@datadog/datadog-ci": "npm:^3.5.0"
|
||||
@@ -15810,7 +15809,7 @@ __metadata:
|
||||
"@types/ua-parser-js": "npm:^0.7.39"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^7.12.0"
|
||||
"@typescript-eslint/parser": "npm:^7.12.0"
|
||||
"@vue/apollo-composable": "npm:@speckle/apollo-composable@4.2.1-patch.1"
|
||||
"@vue/apollo-composable": "npm:^4.2.2"
|
||||
"@vue/apollo-ssr": "npm:4.0.0"
|
||||
"@vueuse/core": "npm:^10.9.0"
|
||||
apollo-upload-client: "npm:^18.0.1"
|
||||
@@ -20677,9 +20676,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vue/apollo-composable@npm:@speckle/apollo-composable@4.2.1-patch.1":
|
||||
version: 4.2.1-patch.1
|
||||
resolution: "@speckle/apollo-composable@npm:4.2.1-patch.1"
|
||||
"@vue/apollo-composable@npm:^4.2.2":
|
||||
version: 4.2.2
|
||||
resolution: "@vue/apollo-composable@npm:4.2.2"
|
||||
dependencies:
|
||||
throttle-debounce: "npm:^5.0.0"
|
||||
ts-essentials: "npm:^9.4.0"
|
||||
@@ -20692,7 +20691,7 @@ __metadata:
|
||||
peerDependenciesMeta:
|
||||
"@vue/composition-api":
|
||||
optional: true
|
||||
checksum: 10/f8816269d1e0294b817af863417a49bce353e0324a7c984f9c71a057d4b3415e1708ac6bf41907951a7fe467cf0a1dfacfec9e78db9985ba7f2773cbba0a2e38
|
||||
checksum: 10/1c26ac821016d79158812da0510e6da7148767e4f86f82cb0926391ecfee768c7238865e3bee14bf8a7a186f7ca8ec5c9586b3544bdc61909c52049c75ef3dd0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -43660,13 +43659,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"response-iterator@npm:^0.2.6":
|
||||
version: 0.2.6
|
||||
resolution: "response-iterator@npm:0.2.6"
|
||||
checksum: 10/ef7c74693ef3891461955a666e753585b298fe0de1baaf0d190e7a6818e4311e459d72f4a36f04aa8f49eda9b5f97124e5534be01e40d9e008795125d0bbb374
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"response-time@npm:^2.3.2":
|
||||
version: 2.3.2
|
||||
resolution: "response-time@npm:2.3.2"
|
||||
|
||||
Reference in New Issue
Block a user