feat: embedding saved views (#5690)
* init + new API query * embed dialog works * embed actions menu * embed label * WIP positioning fix: * cleanup * embed options finalized * prevent reset in embed mode * more embed UX improverments * tests fix
This commit is contained in:
committed by
GitHub
parent
da7bd39f6f
commit
185806ec64
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<FormSelectBase
|
||||
v-model="selectedValue"
|
||||
:name="name || 'savedView'"
|
||||
:label="label || 'View'"
|
||||
:label-id="labelId"
|
||||
:button-id="buttonId"
|
||||
mount-menu-on-body
|
||||
:show-label="showLabel"
|
||||
:fully-control-value="fullyControlValue"
|
||||
:disabled="disabled"
|
||||
:clearable="clearable"
|
||||
:get-search-results="getSearchResults"
|
||||
:allow-unset="allowUnset"
|
||||
search
|
||||
>
|
||||
<template #nothing-selected>Select view</template>
|
||||
<template #something-selected="{ value }">
|
||||
<div class="truncate text-foreground capitalize">
|
||||
{{ isArrayValue(value) ? value.map((v) => v.name).join(', ') : value.name }}
|
||||
</div>
|
||||
</template>
|
||||
<template #option="{ item }">
|
||||
<div class="flex gap-2 items-center">
|
||||
<img
|
||||
:src="item.thumbnailUrl"
|
||||
alt="thumbnail"
|
||||
class="w-20 h-[60px] object-cover rounded border border-outline-3 bg-foundation-page"
|
||||
/>
|
||||
<span class="truncate capitalize">{{ item.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</FormSelectBase>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useFormSelectChildInternals } from '@speckle/ui-components'
|
||||
import { useApolloClient } from '@vue/apollo-composable'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
FormSelectSavedView_SavedViewFragment,
|
||||
SavedViewVisibility
|
||||
} from '~/lib/common/generated/gql/graphql'
|
||||
|
||||
graphql(`
|
||||
fragment FormSelectSavedView_SavedView on SavedView {
|
||||
id
|
||||
name
|
||||
thumbnailUrl
|
||||
}
|
||||
`)
|
||||
|
||||
const searchItemsQuery = graphql(`
|
||||
query FormSelectSavedView_SavedViews(
|
||||
$projectId: String!
|
||||
$input: ProjectSavedViewsInput!
|
||||
) {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
savedViews(input: $input) {
|
||||
items {
|
||||
id
|
||||
...FormSelectSavedView_SavedView
|
||||
}
|
||||
totalCount
|
||||
cursor
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
|
||||
type ItemType = FormSelectSavedView_SavedViewFragment
|
||||
type ValueType = ItemType | ItemType[] | undefined
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', v: ValueType): void
|
||||
}>()
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
projectId: string
|
||||
resourceIdString?: string
|
||||
onlyVisibility?: SavedViewVisibility
|
||||
modelValue?: ValueType
|
||||
fullyControlValue?: boolean
|
||||
label?: string
|
||||
disabled?: boolean
|
||||
showLabel?: boolean
|
||||
clearable?: boolean
|
||||
allowUnset?: boolean
|
||||
name?: string
|
||||
}>(),
|
||||
{
|
||||
clearable: false,
|
||||
allowUnset: false
|
||||
}
|
||||
)
|
||||
|
||||
const apollo = useApolloClient().client
|
||||
const labelId = useId()
|
||||
const buttonId = useId()
|
||||
|
||||
const { selectedValue, isArrayValue } = useFormSelectChildInternals<ItemType>({
|
||||
props: toRefs(props),
|
||||
emit
|
||||
})
|
||||
|
||||
const getSearchResults = async (search: string): Promise<ItemType[]> => {
|
||||
const res = await apollo
|
||||
.query({
|
||||
query: searchItemsQuery,
|
||||
variables: {
|
||||
projectId: props.projectId,
|
||||
input: {
|
||||
resourceIdString: props.resourceIdString,
|
||||
onlyVisibility: props.onlyVisibility,
|
||||
search,
|
||||
limit: 10
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(convertThrowIntoFetchResult)
|
||||
|
||||
const items = res.data?.project.savedViews.items || []
|
||||
return items
|
||||
}
|
||||
</script>
|
||||
@@ -49,13 +49,37 @@
|
||||
</p>
|
||||
<h4 class="text-heading-sm text-foreground-2 mb-1 ml-0.5">Embed URL</h4>
|
||||
<FormClipboardInput class="mb-4" :value="updatedUrl" />
|
||||
<LayoutDialogSection border-b border-t title="Options">
|
||||
<LayoutDialogSection
|
||||
v-model:open="areOptionsExpanded"
|
||||
border-b
|
||||
border-t
|
||||
title="Options"
|
||||
>
|
||||
<div class="flex flex-col gap-1.5 sm:gap-2 text-body-xs cursor-default">
|
||||
<div v-if="areSavedViewsEnabled" class="flex flex-col gap-1">
|
||||
<label for="option-saved-view" :class="optionLabelClasses">
|
||||
<FormCheckbox
|
||||
id="option-saved-view"
|
||||
v-model="shouldEmbedSavedView"
|
||||
name="Embed a saved view"
|
||||
hide-label
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
<span>Embed a saved view</span>
|
||||
</label>
|
||||
<FormSelectSavedView
|
||||
v-if="shouldEmbedSavedView"
|
||||
v-model="embeddedSavedView"
|
||||
:project-id="props.project.id"
|
||||
:resource-id-string="routeModelId"
|
||||
:only-visibility="SavedViewVisibility.Public"
|
||||
:show-label="false"
|
||||
mount-menu-on-body
|
||||
class="max-w-sm"
|
||||
/>
|
||||
</div>
|
||||
<div v-for="option in embedDialogOptions" :key="option.id">
|
||||
<label
|
||||
:for="`option-${option.id}`"
|
||||
class="flex items-center gap-1 cursor-pointer max-w-max"
|
||||
>
|
||||
<label :for="`option-${option.id}`" :class="optionLabelClasses">
|
||||
<FormCheckbox
|
||||
:id="`option-${option.id}`"
|
||||
:model-value="option.value.value"
|
||||
@@ -70,10 +94,7 @@
|
||||
</label>
|
||||
</div>
|
||||
<div v-if="isWorkspacesEnabled">
|
||||
<label
|
||||
:for="`option-hide-logo`"
|
||||
class="flex items-center gap-1 cursor-pointer max-w-max"
|
||||
>
|
||||
<label :for="`option-hide-logo`" :class="optionLabelClasses">
|
||||
<FormCheckbox
|
||||
id="option-hide-logo"
|
||||
v-model="hideSpeckleBranding"
|
||||
@@ -163,7 +184,11 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ProjectsModelPageEmbed_ProjectFragment } from '~~/lib/common/generated/gql/graphql'
|
||||
import {
|
||||
SavedViewVisibility,
|
||||
type FormSelectSavedView_SavedViewFragment,
|
||||
type ProjectsModelPageEmbed_ProjectFragment
|
||||
} from '~~/lib/common/generated/gql/graphql'
|
||||
import { useClipboard } from '~~/composables/browser'
|
||||
import { SpeckleViewer, Roles } from '@speckle/shared'
|
||||
import { graphql } from '~~/lib/common/generated/gql'
|
||||
@@ -175,6 +200,7 @@ import {
|
||||
castToSupportedVisibility
|
||||
} from '~/lib/projects/helpers/visibility'
|
||||
import { useMixpanel } from '~/lib/core/composables/mp'
|
||||
import { useAreSavedViewsEnabled } from '~/lib/viewer/composables/savedViews/general'
|
||||
|
||||
graphql(`
|
||||
fragment ProjectsModelPageEmbed_Project on Project {
|
||||
@@ -208,6 +234,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const isOpen = defineModel<boolean>('open', { required: true })
|
||||
const areOptionsExpanded = ref(false)
|
||||
|
||||
const mixpanel = useMixpanel()
|
||||
const route = useRoute()
|
||||
@@ -217,6 +244,7 @@ const {
|
||||
} = useRuntimeConfig()
|
||||
const createEmbedToken = useCreateEmbedToken()
|
||||
|
||||
const areSavedViewsEnabled = useAreSavedViewsEnabled()
|
||||
const isWorkspacesEnabled = useIsWorkspacesEnabled()
|
||||
const { isSmallerOrEqualSm } = useIsSmallerOrEqualThanBreakpoint()
|
||||
|
||||
@@ -228,7 +256,15 @@ const preventScrolling = ref(false)
|
||||
const manuallyLoadModel = ref(false)
|
||||
const hideSpeckleBranding = ref(false)
|
||||
const embedToken = ref<string | null>(null)
|
||||
const shouldEmbedSavedView = ref(false)
|
||||
|
||||
const embeddedSavedView = defineModel<FormSelectSavedView_SavedViewFragment>('view', {
|
||||
required: false
|
||||
})
|
||||
|
||||
const optionLabelClasses = computed(
|
||||
() => 'flex items-center gap-1 cursor-pointer max-w-max'
|
||||
)
|
||||
const isAdmin = computed(() => props.project.workspace?.role === Roles.Workspace.Admin)
|
||||
|
||||
const routeModelId = computed(() => route.params.modelId as string)
|
||||
@@ -270,7 +306,7 @@ const updatedUrl = computed(() => {
|
||||
}
|
||||
|
||||
// Construct the embed options as a hash fragment
|
||||
const embedOptions: Record<string, boolean> = { isEnabled: true }
|
||||
const embedOptions: Record<string, unknown> = { isEnabled: true }
|
||||
embedDialogOptions.forEach((option) => {
|
||||
if (option.value.value) {
|
||||
embedOptions[option.id] = true
|
||||
@@ -289,6 +325,17 @@ const updatedUrl = computed(() => {
|
||||
const hashFragment = encodeURIComponent(JSON.stringify(embedOptions))
|
||||
url.hash = `embed=${hashFragment}`
|
||||
|
||||
// Embed view?
|
||||
const savedViewSettings: Record<string, unknown> = {}
|
||||
if (shouldEmbedSavedView.value && embeddedSavedView.value) {
|
||||
savedViewSettings['id'] = embeddedSavedView.value.id
|
||||
}
|
||||
|
||||
if (Object.keys(savedViewSettings).length > 0) {
|
||||
const savedViewFragment = encodeURIComponent(JSON.stringify(savedViewSettings))
|
||||
url.hash += `&savedView=${savedViewFragment}`
|
||||
}
|
||||
|
||||
return url.toString()
|
||||
})
|
||||
|
||||
@@ -438,4 +485,14 @@ watch(
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
watch(embeddedSavedView, (newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
shouldEmbedSavedView.value = true
|
||||
|
||||
if (!oldVal || newVal.id !== oldVal.id) {
|
||||
areOptionsExpanded.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</ClientOnly>
|
||||
</div>
|
||||
<ViewerEmbedFooter
|
||||
:name="modelName || 'Loading...'"
|
||||
:name="embedName"
|
||||
:date="lastUpdate"
|
||||
:url="route.path"
|
||||
:hide-speckle-branding="hideSpeckleLogo"
|
||||
@@ -121,6 +121,13 @@ import { ViewerLimitsDialogType } from '~/lib/projects/helpers/limits'
|
||||
import { TailwindBreakpoints } from '~~/lib/common/helpers/tailwind'
|
||||
import { useBreakpoints } from '@vueuse/core'
|
||||
|
||||
graphql(`
|
||||
fragment ViewerPageSetup_SavedView on SavedView {
|
||||
id
|
||||
name
|
||||
}
|
||||
`)
|
||||
|
||||
graphql(`
|
||||
fragment ModelPageProject on Project {
|
||||
id
|
||||
@@ -165,7 +172,7 @@ const mp = useMixpanel()
|
||||
|
||||
const {
|
||||
resources: {
|
||||
response: { project, modelsAndVersionIds }
|
||||
response: { project, modelsAndVersionIds, savedView }
|
||||
}
|
||||
} = state
|
||||
|
||||
@@ -225,6 +232,18 @@ const modelName = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const embedName = computed(() => {
|
||||
if (!modelName.value) return 'Loading...'
|
||||
|
||||
let ret = ''
|
||||
if (savedView.value) {
|
||||
ret += `${savedView.value.name} | `
|
||||
}
|
||||
|
||||
ret += modelName.value
|
||||
return ret
|
||||
})
|
||||
|
||||
const lastUpdate = computed(() => {
|
||||
if (project.value?.models?.items[0] && project.value.models.items[0].updatedAt) {
|
||||
return 'Updated ' + dayjs(project.value.models.items[0].updatedAt).fromNow()
|
||||
|
||||
@@ -46,6 +46,12 @@
|
||||
v-model:open="showGroupDeleteDialog"
|
||||
:group="groupBeingDeleted"
|
||||
/>
|
||||
<ProjectModelPageDialogEmbed
|
||||
v-if="mainProject"
|
||||
v-model:open="showViewEmbedDialog"
|
||||
:view="viewBeingEmbedded"
|
||||
:project="mainProject"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -53,6 +59,7 @@ import { omit } from 'lodash-es'
|
||||
import { usePaginatedQuery } from '~/lib/common/composables/graphql'
|
||||
import { graphql } from '~/lib/common/generated/gql'
|
||||
import type {
|
||||
FormSelectSavedView_SavedViewFragment,
|
||||
UseUpdateSavedViewGroup_SavedViewGroupFragment,
|
||||
ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewEditDialog_SavedViewFragment,
|
||||
@@ -99,7 +106,8 @@ const props = defineProps<{
|
||||
const {
|
||||
projectId,
|
||||
resources: {
|
||||
request: { resourceIdString }
|
||||
request: { resourceIdString },
|
||||
response: { project: mainProject }
|
||||
},
|
||||
ui: {
|
||||
savedViews: { openedGroupState }
|
||||
@@ -113,6 +121,7 @@ const viewBeingDeleted = ref<ViewerSavedViewsPanelViewDeleteDialog_SavedViewFrag
|
||||
const groupBeingDeleted =
|
||||
ref<ViewerSavedViewsPanelViewsGroupDeleteDialog_SavedViewGroupFragment>()
|
||||
const groupBeingRenamed = ref<UseUpdateSavedViewGroup_SavedViewGroupFragment>()
|
||||
const viewBeingEmbedded = ref<FormSelectSavedView_SavedViewFragment>()
|
||||
|
||||
const {
|
||||
identifier,
|
||||
@@ -190,6 +199,15 @@ const showGroupDeleteDialog = computed({
|
||||
}
|
||||
})
|
||||
|
||||
const showViewEmbedDialog = computed({
|
||||
get: () => !!viewBeingEmbedded.value,
|
||||
set: (value) => {
|
||||
if (!value) {
|
||||
viewBeingEmbedded.value = undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const isGroupInRenameMode = (
|
||||
group: ViewerSavedViewsPanelViewsGroup_SavedViewGroupFragment
|
||||
) => {
|
||||
@@ -237,6 +255,10 @@ eventBus.on(ViewerEventBusKeys.MarkSavedViewForEdit, ({ type, view }) => {
|
||||
}
|
||||
})
|
||||
|
||||
eventBus.on(ViewerEventBusKeys.MarkSavedViewForEmbed, ({ view }) => {
|
||||
viewBeingEmbedded.value = view
|
||||
})
|
||||
|
||||
const onMoveSuccess = (groupId: string) => {
|
||||
openedGroupState.value.set(groupId, true)
|
||||
}
|
||||
|
||||
@@ -138,7 +138,8 @@ const MenuItems = StringEnum([
|
||||
'ChangeVisibility',
|
||||
'ReplaceView',
|
||||
'MoveToGroup',
|
||||
'SetAsHomeView'
|
||||
'SetAsHomeView',
|
||||
'Embed'
|
||||
])
|
||||
type MenuItems = StringEnumValues<typeof MenuItems>
|
||||
|
||||
@@ -193,7 +194,8 @@ const {
|
||||
isHomeView,
|
||||
canToggleVisibility,
|
||||
canMove,
|
||||
canOpenEditDialog
|
||||
canOpenEditDialog,
|
||||
canEmbed
|
||||
} = useSavedViewValidationHelpers({
|
||||
view: computed(() => props.view)
|
||||
})
|
||||
@@ -265,6 +267,12 @@ const menuItems = computed((): LayoutMenuItem<MenuItems>[][] => [
|
||||
title: isOnlyVisibleToMe.value ? 'Make view shared' : 'Make view private',
|
||||
disabled: !canToggleVisibility.value.authorized,
|
||||
disabledTooltip: canToggleVisibility.value.message
|
||||
},
|
||||
{
|
||||
id: MenuItems.Embed,
|
||||
title: 'Embed view',
|
||||
disabled: !canEmbed.value?.authorized,
|
||||
disabledTooltip: canEmbed.value?.errorMessage
|
||||
}
|
||||
],
|
||||
[
|
||||
@@ -360,6 +368,11 @@ const onActionChosen = async (item: LayoutMenuItem<MenuItems>) => {
|
||||
}
|
||||
})
|
||||
break
|
||||
case MenuItems.Embed:
|
||||
eventBus.emit(ViewerEventBusKeys.MarkSavedViewForEmbed, {
|
||||
view: props.view
|
||||
})
|
||||
break
|
||||
default:
|
||||
throwUncoveredError(item.id)
|
||||
}
|
||||
|
||||
@@ -52,6 +52,8 @@ type Documents = {
|
||||
"\n mutation DashboardsShareDisableToken($input: DashboardShareInput!) {\n dashboardMutations {\n disableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n": typeof types.DashboardsShareDisableTokenDocument,
|
||||
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": typeof types.FormSelectModels_ModelFragmentDoc,
|
||||
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": typeof types.FormSelectProjects_ProjectFragmentDoc,
|
||||
"\n fragment FormSelectSavedView_SavedView on SavedView {\n id\n name\n thumbnailUrl\n }\n": typeof types.FormSelectSavedView_SavedViewFragmentDoc,
|
||||
"\n query FormSelectSavedView_SavedViews(\n $projectId: String!\n $input: ProjectSavedViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViews(input: $input) {\n items {\n id\n ...FormSelectSavedView_SavedView\n }\n totalCount\n cursor\n }\n }\n }\n": typeof types.FormSelectSavedView_SavedViewsDocument,
|
||||
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": typeof types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
|
||||
"\n query FormSelectSavedViewGroup_SavedViewGroups(\n $projectId: String!\n $input: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroups(input: $input) {\n items {\n id\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n totalCount\n cursor\n }\n }\n }\n": typeof types.FormSelectSavedViewGroup_SavedViewGroupsDocument,
|
||||
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": typeof types.FormUsersSelectItemFragmentDoc,
|
||||
@@ -183,6 +185,7 @@ type Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": typeof types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityWorkspaceCreation_Workspace on Workspace {\n id\n slug\n role\n isExclusive\n hasAccessToExclusiveMembership: hasAccessToFeature(featureName: exclusiveMembership)\n permissions {\n canMakeWorkspaceExclusive {\n authorized\n message\n }\n }\n }\n": typeof types.SettingsWorkspacesSecurityWorkspaceCreation_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": typeof types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ViewerPageSetup_SavedView on SavedView {\n id\n name\n }\n": typeof types.ViewerPageSetup_SavedViewFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n limitedWorkspace {\n id\n slug\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n hasAccessToFeature(featureName: hideSpeckleBranding)\n ...ViewerLimitsDialog_Project\n }\n": typeof types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": typeof types.ThreadCommentAttachmentFragmentDoc,
|
||||
@@ -468,7 +471,7 @@ type Documents = {
|
||||
"\n fragment UseDraggableViewTargetView_SavedView on SavedView {\n id\n name\n position\n group {\n id\n }\n }\n": typeof types.UseDraggableViewTargetView_SavedViewFragmentDoc,
|
||||
"\n fragment UseDraggableViewTargetGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": typeof types.UseDraggableViewTargetGroup_SavedViewGroupFragmentDoc,
|
||||
"\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMove {\n ...FullPermissionCheckResult\n }\n canEditTitle {\n ...FullPermissionCheckResult\n }\n canEditDescription {\n ...FullPermissionCheckResult\n }\n canSetAsHomeView {\n ...FullPermissionCheckResult\n }\n }\n }\n": typeof types.UseSavedViewValidationHelpers_SavedViewFragmentDoc,
|
||||
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": typeof types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
|
||||
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n ...ViewerPageSetup_SavedView\n }\n": typeof types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": typeof types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": typeof types.ViewerCommentsReplyItemFragmentDoc,
|
||||
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": typeof types.BroadcastViewerUserActivityDocument,
|
||||
@@ -605,6 +608,8 @@ const documents: Documents = {
|
||||
"\n mutation DashboardsShareDisableToken($input: DashboardShareInput!) {\n dashboardMutations {\n disableShare(input: $input) {\n id\n revoked\n content\n }\n }\n }\n": types.DashboardsShareDisableTokenDocument,
|
||||
"\n fragment FormSelectModels_Model on Model {\n id\n name\n }\n": types.FormSelectModels_ModelFragmentDoc,
|
||||
"\n fragment FormSelectProjects_Project on Project {\n id\n name\n }\n": types.FormSelectProjects_ProjectFragmentDoc,
|
||||
"\n fragment FormSelectSavedView_SavedView on SavedView {\n id\n name\n thumbnailUrl\n }\n": types.FormSelectSavedView_SavedViewFragmentDoc,
|
||||
"\n query FormSelectSavedView_SavedViews(\n $projectId: String!\n $input: ProjectSavedViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViews(input: $input) {\n items {\n id\n ...FormSelectSavedView_SavedView\n }\n totalCount\n cursor\n }\n }\n }\n": types.FormSelectSavedView_SavedViewsDocument,
|
||||
"\n fragment FormSelectSavedViewGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n isUngroupedViewsGroup\n }\n": types.FormSelectSavedViewGroup_SavedViewGroupFragmentDoc,
|
||||
"\n query FormSelectSavedViewGroup_SavedViewGroups(\n $projectId: String!\n $input: SavedViewGroupsInput!\n ) {\n project(id: $projectId) {\n id\n savedViewGroups(input: $input) {\n items {\n id\n ...FormSelectSavedViewGroup_SavedViewGroup\n }\n totalCount\n cursor\n }\n }\n }\n": types.FormSelectSavedViewGroup_SavedViewGroupsDocument,
|
||||
"\n fragment FormUsersSelectItem on LimitedUser {\n id\n name\n avatar\n }\n": types.FormUsersSelectItemFragmentDoc,
|
||||
@@ -736,6 +741,7 @@ const documents: Documents = {
|
||||
"\n fragment SettingsWorkspacesSecurityDomainRemoveDialog_Workspace on Workspace {\n id\n domains {\n ...SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceDomain\n }\n }\n": types.SettingsWorkspacesSecurityDomainRemoveDialog_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecurityWorkspaceCreation_Workspace on Workspace {\n id\n slug\n role\n isExclusive\n hasAccessToExclusiveMembership: hasAccessToFeature(featureName: exclusiveMembership)\n permissions {\n canMakeWorkspaceExclusive {\n authorized\n message\n }\n }\n }\n": types.SettingsWorkspacesSecurityWorkspaceCreation_WorkspaceFragmentDoc,
|
||||
"\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n": types.SettingsWorkspacesSecuritySsoWrapper_WorkspaceFragmentDoc,
|
||||
"\n fragment ViewerPageSetup_SavedView on SavedView {\n id\n name\n }\n": types.ViewerPageSetup_SavedViewFragmentDoc,
|
||||
"\n fragment ModelPageProject on Project {\n id\n createdAt\n name\n visibility\n limitedWorkspace {\n id\n slug\n name\n }\n embedOptions {\n hideSpeckleBranding\n }\n hasAccessToFeature(featureName: hideSpeckleBranding)\n ...ViewerLimitsDialog_Project\n }\n": types.ModelPageProjectFragmentDoc,
|
||||
"\n fragment ViewerCommentThreadData on Comment {\n id\n permissions {\n canArchive {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.ViewerCommentThreadDataFragmentDoc,
|
||||
"\n fragment ThreadCommentAttachment on Comment {\n text {\n attachments {\n id\n fileName\n fileType\n fileSize\n }\n }\n }\n": types.ThreadCommentAttachmentFragmentDoc,
|
||||
@@ -1021,7 +1027,7 @@ const documents: Documents = {
|
||||
"\n fragment UseDraggableViewTargetView_SavedView on SavedView {\n id\n name\n position\n group {\n id\n }\n }\n": types.UseDraggableViewTargetView_SavedViewFragmentDoc,
|
||||
"\n fragment UseDraggableViewTargetGroup_SavedViewGroup on SavedViewGroup {\n id\n title\n }\n": types.UseDraggableViewTargetGroup_SavedViewGroupFragmentDoc,
|
||||
"\n fragment UseSavedViewValidationHelpers_SavedView on SavedView {\n id\n isHomeView\n visibility\n permissions {\n canUpdate {\n ...FullPermissionCheckResult\n }\n canMove {\n ...FullPermissionCheckResult\n }\n canEditTitle {\n ...FullPermissionCheckResult\n }\n canEditDescription {\n ...FullPermissionCheckResult\n }\n canSetAsHomeView {\n ...FullPermissionCheckResult\n }\n }\n }\n": types.UseSavedViewValidationHelpers_SavedViewFragmentDoc,
|
||||
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n": types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
|
||||
"\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n ...ViewerPageSetup_SavedView\n }\n": types.UseViewerSavedViewSetup_SavedViewFragmentDoc,
|
||||
"\n fragment ViewerCommentThread on Comment {\n ...ViewerCommentsListItem\n ...ViewerCommentBubblesData\n ...ViewerCommentsReplyItem\n ...ViewerCommentThreadData\n }\n": types.ViewerCommentThreadFragmentDoc,
|
||||
"\n fragment ViewerCommentsReplyItem on Comment {\n id\n archived\n rawText\n text {\n doc\n }\n author {\n ...LimitedUserAvatar\n }\n createdAt\n ...ThreadCommentAttachment\n }\n": types.ViewerCommentsReplyItemFragmentDoc,
|
||||
"\n mutation BroadcastViewerUserActivity(\n $projectId: String!\n $resourceIdString: String!\n $message: ViewerUserActivityMessageInput!\n ) {\n broadcastViewerUserActivity(\n projectId: $projectId\n resourceIdString: $resourceIdString\n message: $message\n )\n }\n": types.BroadcastViewerUserActivityDocument,
|
||||
@@ -1286,6 +1292,14 @@ export function graphql(source: "\n fragment FormSelectModels_Model on Model {\
|
||||
* 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 FormSelectProjects_Project on Project {\n id\n name\n }\n"): (typeof documents)["\n fragment FormSelectProjects_Project on Project {\n id\n name\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 FormSelectSavedView_SavedView on SavedView {\n id\n name\n thumbnailUrl\n }\n"): (typeof documents)["\n fragment FormSelectSavedView_SavedView on SavedView {\n id\n name\n thumbnailUrl\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 FormSelectSavedView_SavedViews(\n $projectId: String!\n $input: ProjectSavedViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViews(input: $input) {\n items {\n id\n ...FormSelectSavedView_SavedView\n }\n totalCount\n cursor\n }\n }\n }\n"): (typeof documents)["\n query FormSelectSavedView_SavedViews(\n $projectId: String!\n $input: ProjectSavedViewsInput!\n ) {\n project(id: $projectId) {\n id\n savedViews(input: $input) {\n items {\n id\n ...FormSelectSavedView_SavedView\n }\n totalCount\n cursor\n }\n }\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -1810,6 +1824,10 @@ export function graphql(source: "\n fragment SettingsWorkspacesSecurityWorkspac
|
||||
* 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 SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\n }\n"): (typeof documents)["\n fragment SettingsWorkspacesSecuritySsoWrapper_Workspace on Workspace {\n id\n role\n slug\n sso {\n provider {\n id\n name\n clientId\n issuerUrl\n }\n }\n hasAccessToSSO: hasAccessToFeature(featureName: oidcSso)\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 ViewerPageSetup_SavedView on SavedView {\n id\n name\n }\n"): (typeof documents)["\n fragment ViewerPageSetup_SavedView on SavedView {\n id\n name\n }\n"];
|
||||
/**
|
||||
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
|
||||
*/
|
||||
@@ -2953,7 +2971,7 @@ export function graphql(source: "\n fragment UseSavedViewValidationHelpers_Save
|
||||
/**
|
||||
* 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 UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n"): (typeof documents)["\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n }\n"];
|
||||
export function graphql(source: "\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n ...ViewerPageSetup_SavedView\n }\n"): (typeof documents)["\n fragment UseViewerSavedViewSetup_SavedView on SavedView {\n id\n viewerState\n ...ViewerPageSetup_SavedView\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
@@ -1,29 +1,60 @@
|
||||
import { CleanStackTrace, collectLongTrace } from '@speckle/shared'
|
||||
import { isString } from 'lodash-es'
|
||||
|
||||
type LoggingRefOptions<R extends Ref<unknown>> = Partial<{
|
||||
writes: boolean
|
||||
reads: boolean
|
||||
name: string
|
||||
transform: (val: R['value']) => unknown
|
||||
enabled: boolean
|
||||
}>
|
||||
type LoggingRefNameOrOptions<R extends Ref<unknown>> = string | LoggingRefOptions<R>
|
||||
|
||||
const parseNameOrOptions = <R extends Ref<unknown>>(
|
||||
nameOrOptions: LoggingRefNameOrOptions<R> | undefined
|
||||
): Required<LoggingRefOptions<R>> => {
|
||||
const options: LoggingRefOptions<R> = nameOrOptions
|
||||
? isString(nameOrOptions)
|
||||
? { name: nameOrOptions }
|
||||
: nameOrOptions
|
||||
: {}
|
||||
const name = options.name || 'unknown ref'
|
||||
const transform = options.transform || ((val) => val)
|
||||
const enabled = options.enabled ?? true
|
||||
const writes = options.writes ?? true
|
||||
const reads = options.reads ?? false // usually interested in just writes
|
||||
|
||||
return { name, transform, enabled, writes, reads }
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap refs with writable computeds that output where reads/writes are coming from
|
||||
*/
|
||||
export function wrapRefWithTracking<R extends Ref<unknown>>(
|
||||
export function makeRefLogged<R extends Ref<unknown>>(
|
||||
ref: R,
|
||||
name: string,
|
||||
options?: Partial<{
|
||||
writesOnly: boolean
|
||||
readsOnly: boolean
|
||||
}>
|
||||
nameOrOptions?: LoggingRefNameOrOptions<R> | undefined
|
||||
): R {
|
||||
const { writesOnly, readsOnly } = options || {}
|
||||
const getTrace = () => (new Error('Trace:').stack || '').substring(7)
|
||||
const { writes, reads, name, transform, enabled } = parseNameOrOptions(nameOrOptions)
|
||||
const getTrace = collectLongTrace
|
||||
const { logger } = useSafeLogger()
|
||||
|
||||
return computed({
|
||||
get: () => {
|
||||
if (!writesOnly) {
|
||||
logger().debug(`debugging: '${name}' read`, ref.value, getTrace())
|
||||
if (reads && enabled) {
|
||||
logger().debug(`debugging: '${name}' read`, {
|
||||
val: transform(ref.value),
|
||||
trace: getTrace()
|
||||
})
|
||||
}
|
||||
|
||||
return ref.value
|
||||
},
|
||||
set: (newVal) => {
|
||||
if (!readsOnly) {
|
||||
logger().debug(`debugging: '${name}' written to`, { newVal }, getTrace())
|
||||
if (writes && enabled) {
|
||||
logger().debug(`debugging: '${name}' written to`, {
|
||||
val: transform(newVal),
|
||||
trace: getTrace()
|
||||
})
|
||||
}
|
||||
|
||||
ref.value = newVal
|
||||
@@ -33,15 +64,20 @@ export function wrapRefWithTracking<R extends Ref<unknown>>(
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to render a stack trace with clickable links to the source code in the browser console
|
||||
* Define a trackable ref that logs reads and writes to the console w/ stack traces. Useful for figuring out
|
||||
* what's causing a ref to change.
|
||||
*/
|
||||
export class StackTrace extends Error {
|
||||
constructor() {
|
||||
super('')
|
||||
this.name = 'Stack trace:'
|
||||
}
|
||||
export const refLogged = <T>(
|
||||
nameOrOptions: LoggingRefNameOrOptions<Ref<T>>,
|
||||
value: T
|
||||
) => {
|
||||
return makeRefLogged<Ref<T>>(ref<T>(value) as Ref<T>, nameOrOptions)
|
||||
}
|
||||
|
||||
export const refWithLogging = refLogged
|
||||
|
||||
export function getCurrentTrace() {
|
||||
return (new Error('Trace:').stack || '').substring(7)
|
||||
}
|
||||
|
||||
export { CleanStackTrace as StackTrace, CleanStackTrace }
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
|
||||
export const useAreSavedViewsEnabled = () => {
|
||||
const {
|
||||
public: { FF_SAVED_VIEWS_ENABLED }
|
||||
public: { FF_SAVED_VIEWS_ENABLED, FF_WORKSPACES_MODULE_ENABLED }
|
||||
} = useRuntimeConfig()
|
||||
|
||||
return FF_SAVED_VIEWS_ENABLED
|
||||
return !!(FF_SAVED_VIEWS_ENABLED && FF_WORKSPACES_MODULE_ENABLED)
|
||||
}
|
||||
|
||||
export const useViewerSavedViewsUtils = () => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
type InitialSetupState,
|
||||
type UseSetupViewerParams
|
||||
} from '~/lib/viewer/composables/setup'
|
||||
import { useEmbedState } from '~/lib/viewer/composables/setup/embed'
|
||||
import type { SavedViewUrlSettings } from '~/lib/viewer/helpers/savedViews'
|
||||
import { ViewerRenderPageType } from '~/lib/viewer/helpers/state'
|
||||
|
||||
@@ -32,6 +33,7 @@ export const useViewerSavedViewIntegration = () => {
|
||||
const applyState = useApplySerializedState()
|
||||
const { serializedStateId } = useViewerRealtimeActivityTracker()
|
||||
const { on, emit } = useEventBus()
|
||||
const { embedOptions } = useEmbedState()
|
||||
|
||||
const validState = (state: unknown) => (isSerializedViewerState(state) ? state : null)
|
||||
|
||||
@@ -45,6 +47,10 @@ export const useViewerSavedViewIntegration = () => {
|
||||
savedViewStateId.value = serializedStateId.value
|
||||
}
|
||||
|
||||
const resetUrlHashState = async () => {
|
||||
await urlHashStateSavedViewSettings.update(null)
|
||||
}
|
||||
|
||||
const update = async (params: { settings: SavedViewUrlSettings }) => {
|
||||
const { settings } = params
|
||||
|
||||
@@ -52,12 +58,17 @@ export const useViewerSavedViewIntegration = () => {
|
||||
|
||||
// If passing in viewId and it differs, apply and wait for that to finish
|
||||
if (settings.id && settings.id !== savedViewId.value) {
|
||||
// wipe hash state, if any exists, otherwise the state will be stale
|
||||
await resetUrlHashState()
|
||||
if (embedOptions.value?.isEnabled) {
|
||||
// in embed mode we want clicking views to actually show selected view in the URL
|
||||
await urlHashStateSavedViewSettings.update({ id: settings.id })
|
||||
} else {
|
||||
// wipe hash state, if any exists, otherwise the state will be stale
|
||||
await resetUrlHashState()
|
||||
savedViewId.value = settings.id
|
||||
}
|
||||
|
||||
// this acts as a reset of the state id too, cause it only applies to active view
|
||||
savedViewStateId.value = undefined
|
||||
savedViewId.value = settings.id
|
||||
reapplyState = false
|
||||
}
|
||||
|
||||
@@ -74,10 +85,6 @@ export const useViewerSavedViewIntegration = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const resetUrlHashState = async () => {
|
||||
await urlHashStateSavedViewSettings.update(null)
|
||||
}
|
||||
|
||||
const reset = async () => {
|
||||
// No such thing as a reset in presentation mode - we always have a view active
|
||||
if (pageType.value === ViewerRenderPageType.Presentation) return
|
||||
@@ -112,11 +119,14 @@ export const useViewerSavedViewIntegration = () => {
|
||||
() => serializedStateId.value,
|
||||
async (newVal, oldVal) => {
|
||||
if (newVal === oldVal) return
|
||||
if (embedOptions.value?.isEnabled) return // we never reset in embed mode
|
||||
|
||||
// If the saved view state ID is different from the current serialized state ID (user interaction) -
|
||||
// user has changed the state from the view's state
|
||||
if (savedViewStateId.value && newVal !== savedViewStateId.value) {
|
||||
// emit event that this happened
|
||||
emit(ViewerEventBusKeys.UserChangedOpenedView, { viewId: savedViewId.value })
|
||||
if (savedViewId.value)
|
||||
emit(ViewerEventBusKeys.UserChangedOpenedView, { viewId: savedViewId.value })
|
||||
|
||||
// reset the saved view - its no longer active
|
||||
await reset()
|
||||
|
||||
@@ -56,6 +56,28 @@ export const useSavedViewValidationHelpers = (params: {
|
||||
const canEditTitle = computed(() => permissions.value?.canEditTitle)
|
||||
const canEditDescription = computed(() => permissions.value?.canEditDescription)
|
||||
|
||||
const canEmbed = computed((): FullPermissionCheckResultFragment | undefined => {
|
||||
if (isLoading.value) {
|
||||
return {
|
||||
authorized: false,
|
||||
errorMessage: undefined,
|
||||
code: 'LOADING',
|
||||
message: ''
|
||||
}
|
||||
}
|
||||
|
||||
if (params.view.value?.visibility !== SavedViewVisibility.Public) {
|
||||
return {
|
||||
authorized: false,
|
||||
errorMessage: 'Only shared views can be embedded',
|
||||
code: 'FORBIDDEN',
|
||||
message: 'Only shared views can be embedded'
|
||||
}
|
||||
}
|
||||
|
||||
return { authorized: true, code: 'OK', message: '' }
|
||||
})
|
||||
|
||||
const canOpenEditDialog = computed(
|
||||
(): FullPermissionCheckResultFragment | undefined => {
|
||||
if (isLoading.value) {
|
||||
@@ -163,6 +185,7 @@ export const useSavedViewValidationHelpers = (params: {
|
||||
canMove,
|
||||
canEditTitle,
|
||||
canEditDescription,
|
||||
canOpenEditDialog
|
||||
canOpenEditDialog,
|
||||
canEmbed
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ function useViewerObjectAutoLoading() {
|
||||
const getUniqueObjectIds = (resourceItems: ViewerResourceItem[]) =>
|
||||
uniq(resourceItems.map((i) => i.objectId))
|
||||
|
||||
const activeLoads = new Set<Promise<void>>()
|
||||
watch(
|
||||
() => <const>[resourceItems.value, isInitialized.value, hasDoneInitialLoad.value],
|
||||
async ([newResources, newIsInitialized, newHasDoneInitialLoad], oldData) => {
|
||||
@@ -177,8 +178,6 @@ function useViewerObjectAutoLoading() {
|
||||
|
||||
const [oldResources] = oldData || [[], false]
|
||||
|
||||
hasLoadedQueuedUpModels.value = false
|
||||
|
||||
// we dont want to zoom to object, if we're loading specific coords because of a thread,
|
||||
// or spotlight mode or a saved view etc.
|
||||
const preventZooming =
|
||||
@@ -191,33 +190,60 @@ function useViewerObjectAutoLoading() {
|
||||
// Viewer initialized - load in all resources
|
||||
if (!newHasDoneInitialLoad) {
|
||||
const allObjectIds = getUniqueObjectIds(newResources)
|
||||
if (allObjectIds.length) {
|
||||
// only mark, if anything to load
|
||||
hasLoadedQueuedUpModels.value = false
|
||||
}
|
||||
|
||||
/** Load sequentially */
|
||||
const res = []
|
||||
for (const i of allObjectIds) {
|
||||
res.push(await loadObject(i, false, { zoomToObject }))
|
||||
const loadAll = async () => {
|
||||
for (const i of allObjectIds) {
|
||||
res.push(await loadObject(i, false, { zoomToObject }))
|
||||
}
|
||||
}
|
||||
|
||||
// Register for accurate 'is anything loading' reporting
|
||||
const promise = loadAll().then(() => {
|
||||
activeLoads.delete(promise)
|
||||
})
|
||||
activeLoads.add(promise)
|
||||
await promise
|
||||
|
||||
if (res.length) {
|
||||
hasDoneInitialLoad.value = true
|
||||
hasLoadedQueuedUpModels.value = true
|
||||
if (!activeLoads.size) hasLoadedQueuedUpModels.value = true
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Resources changed?
|
||||
const newObjectIds = getUniqueObjectIds(newResources)
|
||||
const oldObjectIds = getUniqueObjectIds(oldResources)
|
||||
const removableObjectIds = difference(oldObjectIds, newObjectIds)
|
||||
const addableObjectIds = difference(newObjectIds, oldObjectIds)
|
||||
const loadAndUnloadChanged = async () => {
|
||||
const newObjectIds = getUniqueObjectIds(newResources)
|
||||
const oldObjectIds = getUniqueObjectIds(oldResources)
|
||||
const removableObjectIds = difference(oldObjectIds, newObjectIds)
|
||||
const addableObjectIds = difference(newObjectIds, oldObjectIds)
|
||||
|
||||
await Promise.all(removableObjectIds.map((i) => loadObject(i, true)))
|
||||
await Promise.all(
|
||||
addableObjectIds.map((i) => loadObject(i, false, { zoomToObject: false }))
|
||||
)
|
||||
if (addableObjectIds.length) {
|
||||
// only mark, if anything to load
|
||||
hasLoadedQueuedUpModels.value = false
|
||||
}
|
||||
|
||||
hasLoadedQueuedUpModels.value = true
|
||||
await Promise.all(removableObjectIds.map((i) => loadObject(i, true)))
|
||||
await Promise.all(
|
||||
addableObjectIds.map((i) => loadObject(i, false, { zoomToObject: false }))
|
||||
)
|
||||
}
|
||||
|
||||
// Register for accurate 'is anything loading' reporting
|
||||
const promise = loadAndUnloadChanged().then(() => {
|
||||
activeLoads.delete(promise)
|
||||
})
|
||||
activeLoads.add(promise)
|
||||
await promise
|
||||
|
||||
if (!activeLoads.size) hasLoadedQueuedUpModels.value = true
|
||||
},
|
||||
{ deep: true, immediate: true }
|
||||
)
|
||||
@@ -444,21 +470,18 @@ function useViewerCameraIntegration() {
|
||||
|
||||
const loadCameraDataFromViewer = () => {
|
||||
const extension: CameraController = instance.getExtension(CameraController)
|
||||
let cameraManuallyChanged = false
|
||||
|
||||
const viewerPos = new Vector3().copy(extension.getPosition())
|
||||
const viewerTarget = new Vector3().copy(extension.getTarget())
|
||||
|
||||
if (!areVectorsLooselyEqual(position.value, viewerPos)) {
|
||||
if (hasInitialLoadFired.value) position.value = viewerPos.clone()
|
||||
cameraManuallyChanged = true
|
||||
if (hasInitialLoadFired.value) {
|
||||
if (!areVectorsLooselyEqual(position.value, viewerPos)) {
|
||||
position.value = viewerPos.clone()
|
||||
}
|
||||
if (!areVectorsLooselyEqual(target.value, viewerTarget)) {
|
||||
target.value = viewerTarget.clone()
|
||||
}
|
||||
}
|
||||
if (!areVectorsLooselyEqual(target.value, viewerTarget)) {
|
||||
if (hasInitialLoadFired.value) target.value = viewerTarget.clone()
|
||||
cameraManuallyChanged = true
|
||||
}
|
||||
|
||||
return cameraManuallyChanged
|
||||
}
|
||||
|
||||
// viewer -> state
|
||||
@@ -810,6 +833,7 @@ graphql(`
|
||||
fragment UseViewerSavedViewSetup_SavedView on SavedView {
|
||||
id
|
||||
viewerState
|
||||
...ViewerPageSetup_SavedView
|
||||
}
|
||||
`)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
FormSelectSavedView_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewEditDialog_SavedViewFragment,
|
||||
ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment
|
||||
@@ -8,6 +9,7 @@ import type { SavedViewUrlSettings } from '~/lib/viewer/helpers/savedViews'
|
||||
export enum ViewerEventBusKeys {
|
||||
ApplySavedView = 'UpdateSavedView',
|
||||
MarkSavedViewForEdit = 'MarkSavedViewForEdit',
|
||||
MarkSavedViewForEmbed = 'MarkSavedViewForEmbed',
|
||||
UserChangedOpenedView = 'UserChangedOpenedView'
|
||||
}
|
||||
|
||||
@@ -23,10 +25,15 @@ export type ViewerSavedViewEventBusPayloads = {
|
||||
[ViewerEventBusKeys.UserChangedOpenedView]: {
|
||||
viewId: string
|
||||
}
|
||||
[ViewerEventBusKeys.MarkSavedViewForEmbed]: {
|
||||
view: FormSelectSavedView_SavedViewFragment
|
||||
}
|
||||
}
|
||||
|
||||
// Add mappings between event keys and expected payloads here
|
||||
export type ViewerEventBusKeyPayloadMap = {
|
||||
[ViewerEventBusKeys.ApplySavedView]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.ApplySavedView]
|
||||
[ViewerEventBusKeys.MarkSavedViewForEdit]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.MarkSavedViewForEdit]
|
||||
[ViewerEventBusKeys.UserChangedOpenedView]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.UserChangedOpenedView]
|
||||
[ViewerEventBusKeys.MarkSavedViewForEmbed]: ViewerSavedViewEventBusPayloads[ViewerEventBusKeys.MarkSavedViewForEmbed]
|
||||
} & { [k in ViewerEventBusKeys]: unknown } & Record<string, unknown>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { RouteLocationNormalized } from 'vue-router'
|
||||
import { noop } from 'lodash-es'
|
||||
import { wrapRefWithTracking } from '~/lib/common/helpers/debugging'
|
||||
import {
|
||||
refLogged,
|
||||
makeRefLogged,
|
||||
refWithLogging
|
||||
} from '~/lib/common/helpers/debugging'
|
||||
import { ToastNotificationType } from '~~/lib/common/composables/toast'
|
||||
import {
|
||||
convertThrowIntoFetchResult,
|
||||
@@ -30,7 +34,9 @@ export const getRouteDefinition = (route?: RouteLocationNormalized) => {
|
||||
}
|
||||
export {
|
||||
ToastNotificationType,
|
||||
wrapRefWithTracking,
|
||||
makeRefLogged,
|
||||
refLogged,
|
||||
refWithLogging,
|
||||
noop,
|
||||
convertThrowIntoFetchResult,
|
||||
getFirstGqlErrorMessage,
|
||||
|
||||
@@ -83,6 +83,33 @@ input SavedViewGroupViewsInput {
|
||||
cursor: String
|
||||
}
|
||||
|
||||
input ProjectSavedViewsInput {
|
||||
"""
|
||||
Viewer resource ID string that identifies which resources should be loaded. If not specified,
|
||||
views from all resources in the project will be returned.
|
||||
"""
|
||||
resourceIdString: String
|
||||
"""
|
||||
Optionally filter by visibility.
|
||||
"""
|
||||
onlyVisibility: SavedViewVisibility
|
||||
"""
|
||||
Whether to only include views matching this search term
|
||||
"""
|
||||
search: String
|
||||
"""
|
||||
Optionally specify sort direction. Default: descending
|
||||
"""
|
||||
sortDirection: SortDirection
|
||||
"""
|
||||
Optionally specify sort by field. Default: position
|
||||
Options: updatedAt, createdAt, name, position
|
||||
"""
|
||||
sortBy: String
|
||||
limit: Int
|
||||
cursor: String
|
||||
}
|
||||
|
||||
type SavedViewGroup {
|
||||
"""
|
||||
This is always set even for fake/not persisted groups for Apollo caching
|
||||
@@ -266,6 +293,7 @@ extend type ProjectMutations {
|
||||
}
|
||||
|
||||
extend type Project {
|
||||
savedViews(input: ProjectSavedViewsInput!): SavedViewCollection!
|
||||
savedViewGroups(input: SavedViewGroupsInput!): SavedViewGroupCollection!
|
||||
savedViewGroup(id: ID!): SavedViewGroup!
|
||||
ungroupedViewGroup(input: GetUngroupedViewGroupInput!): SavedViewGroup!
|
||||
|
||||
@@ -2696,6 +2696,7 @@ export type Project = {
|
||||
savedViewGroups: SavedViewGroupCollection;
|
||||
/** Same as savedView(), but won't throw if view isn't found */
|
||||
savedViewIfExists?: Maybe<SavedView>;
|
||||
savedViews: SavedViewCollection;
|
||||
/** Source apps used in any models of this project */
|
||||
sourceApps: Array<Scalars['String']['output']>;
|
||||
team: Array<ProjectCollaborator>;
|
||||
@@ -2855,6 +2856,11 @@ export type ProjectSavedViewIfExistsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type ProjectSavedViewsArgs = {
|
||||
input: ProjectSavedViewsInput;
|
||||
};
|
||||
|
||||
|
||||
export type ProjectUngroupedViewGroupArgs = {
|
||||
input: GetUngroupedViewGroupInput;
|
||||
};
|
||||
@@ -3374,6 +3380,27 @@ export type ProjectSavedViewGroupsUpdatedMessage = {
|
||||
type: ProjectSavedViewsUpdatedMessageType;
|
||||
};
|
||||
|
||||
export type ProjectSavedViewsInput = {
|
||||
cursor?: InputMaybe<Scalars['String']['input']>;
|
||||
limit?: InputMaybe<Scalars['Int']['input']>;
|
||||
/** Optionally filter by visibility. */
|
||||
onlyVisibility?: InputMaybe<SavedViewVisibility>;
|
||||
/**
|
||||
* Viewer resource ID string that identifies which resources should be loaded. If not specified,
|
||||
* views from all resources in the project will be returned.
|
||||
*/
|
||||
resourceIdString?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Whether to only include views matching this search term */
|
||||
search?: InputMaybe<Scalars['String']['input']>;
|
||||
/**
|
||||
* Optionally specify sort by field. Default: position
|
||||
* Options: updatedAt, createdAt, name, position
|
||||
*/
|
||||
sortBy?: InputMaybe<Scalars['String']['input']>;
|
||||
/** Optionally specify sort direction. Default: descending */
|
||||
sortDirection?: InputMaybe<SortDirection>;
|
||||
};
|
||||
|
||||
export type ProjectSavedViewsUpdatedMessage = {
|
||||
__typename?: 'ProjectSavedViewsUpdatedMessage';
|
||||
/** Set if view was deleted/updated, allows some limited access into the view before the change (update/delete) */
|
||||
@@ -6615,6 +6642,7 @@ export type ResolversTypes = {
|
||||
ProjectPermissionChecks: ResolverTypeWrapper<ProjectPermissionChecksGraphQLReturn>;
|
||||
ProjectRole: ResolverTypeWrapper<ProjectRoleGraphQLReturn>;
|
||||
ProjectSavedViewGroupsUpdatedMessage: ResolverTypeWrapper<ProjectSavedViewGroupsUpdatedMessageGraphQLReturn>;
|
||||
ProjectSavedViewsInput: ProjectSavedViewsInput;
|
||||
ProjectSavedViewsUpdatedMessage: ResolverTypeWrapper<ProjectSavedViewsUpdatedMessageGraphQLReturn>;
|
||||
ProjectSavedViewsUpdatedMessageType: ProjectSavedViewsUpdatedMessageType;
|
||||
ProjectTestAutomationCreateInput: ProjectTestAutomationCreateInput;
|
||||
@@ -7020,6 +7048,7 @@ export type ResolversParentTypes = {
|
||||
ProjectPermissionChecks: ProjectPermissionChecksGraphQLReturn;
|
||||
ProjectRole: ProjectRoleGraphQLReturn;
|
||||
ProjectSavedViewGroupsUpdatedMessage: ProjectSavedViewGroupsUpdatedMessageGraphQLReturn;
|
||||
ProjectSavedViewsInput: ProjectSavedViewsInput;
|
||||
ProjectSavedViewsUpdatedMessage: ProjectSavedViewsUpdatedMessageGraphQLReturn;
|
||||
ProjectTestAutomationCreateInput: ProjectTestAutomationCreateInput;
|
||||
ProjectTriggeredAutomationsStatusUpdatedMessage: ProjectTriggeredAutomationsStatusUpdatedMessageGraphQLReturn;
|
||||
@@ -8272,6 +8301,7 @@ export type ProjectResolvers<ContextType = GraphQLContext, ParentType extends Re
|
||||
savedViewGroup?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType, RequireFields<ProjectSavedViewGroupArgs, 'id'>>;
|
||||
savedViewGroups?: Resolver<ResolversTypes['SavedViewGroupCollection'], ParentType, ContextType, RequireFields<ProjectSavedViewGroupsArgs, 'input'>>;
|
||||
savedViewIfExists?: Resolver<Maybe<ResolversTypes['SavedView']>, ParentType, ContextType, Partial<ProjectSavedViewIfExistsArgs>>;
|
||||
savedViews?: Resolver<ResolversTypes['SavedViewCollection'], ParentType, ContextType, RequireFields<ProjectSavedViewsArgs, 'input'>>;
|
||||
sourceApps?: Resolver<Array<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
team?: Resolver<Array<ResolversTypes['ProjectCollaborator']>, ParentType, ContextType>;
|
||||
ungroupedViewGroup?: Resolver<ResolversTypes['SavedViewGroup'], ParentType, ContextType, RequireFields<ProjectUngroupedViewGroupArgs, 'input'>>;
|
||||
@@ -10121,6 +10151,14 @@ export type OnProjectSavedViewGroupsUpdatedSubscriptionVariables = Exact<{
|
||||
|
||||
export type OnProjectSavedViewGroupsUpdatedSubscription = { __typename?: 'Subscription', projectSavedViewGroupsUpdated: { __typename?: 'ProjectSavedViewGroupsUpdatedMessage', type: ProjectSavedViewsUpdatedMessageType, id: string, project: { __typename?: 'Project', id: string }, savedViewGroup?: { __typename?: 'SavedViewGroup', id: string, projectId: string, resourceIds: Array<string>, title: string, isUngroupedViewsGroup: boolean, views: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } }> } } | null } };
|
||||
|
||||
export type GetProjectSavedViewsQueryVariables = Exact<{
|
||||
projectId: Scalars['String']['input'];
|
||||
input: ProjectSavedViewsInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type GetProjectSavedViewsQuery = { __typename?: 'Query', project: { __typename?: 'Project', id: string, savedViews: { __typename?: 'SavedViewCollection', totalCount: number, cursor?: string | null, items: Array<{ __typename?: 'SavedView', id: string, name: string, description?: string | null, groupId?: string | null, createdAt: Date, updatedAt: Date, resourceIdString: string, resourceIds: Array<string>, isHomeView: boolean, visibility: SavedViewVisibility, viewerState: Record<string, unknown>, screenshot: string, position: number, projectId: string, author?: { __typename?: 'LimitedUser', id: string } | null, group: { __typename?: 'SavedViewGroup', id: string, title: string, isUngroupedViewsGroup: boolean } }> } } };
|
||||
|
||||
export type BasicWorkspaceFragment = { __typename?: 'Workspace', id: string, name: string, slug: string, updatedAt: Date, createdAt: Date, role?: string | null, readOnly: boolean };
|
||||
|
||||
export type BasicPendingWorkspaceCollaboratorFragment = { __typename?: 'PendingWorkspaceCollaborator', id: string, inviteId: string, title: string, role: string, token?: string | null, workspace: { __typename?: 'LimitedWorkspace', id: string, name: string }, invitedBy: { __typename?: 'LimitedUser', id: string, name: string }, user?: { __typename?: 'LimitedUser', id: string, name: string } | null };
|
||||
@@ -11260,6 +11298,7 @@ export const UpdateSavedViewGroupDocument = {"kind":"Document","definitions":[{"
|
||||
export const GetModelHomeViewDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetModelHomeView"},"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":"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","name":{"kind":"Name","value":"homeView"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<GetModelHomeViewQuery, GetModelHomeViewQueryVariables>;
|
||||
export const OnProjectSavedViewsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"onProjectSavedViewsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectSavedViewsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"savedView"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedView"}}]}},{"kind":"Field","name":{"kind":"Name","value":"beforeChangeSavedView"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<OnProjectSavedViewsUpdatedSubscription, OnProjectSavedViewsUpdatedSubscriptionVariables>;
|
||||
export const OnProjectSavedViewGroupsUpdatedDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"onProjectSavedViewGroupsUpdated"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroupViewsInput"}}},"defaultValue":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"10"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"projectSavedViewGroupsUpdated"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"projectId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"project"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"savedViewGroup"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicSavedViewGroup"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedViewGroup"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedViewGroup"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}},{"kind":"Field","name":{"kind":"Name","value":"views"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"viewsInput"}}}],"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":"BasicSavedView"}}]}}]}}]}}]} as unknown as DocumentNode<OnProjectSavedViewGroupsUpdatedSubscription, OnProjectSavedViewGroupsUpdatedSubscriptionVariables>;
|
||||
export const GetProjectSavedViewsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectSavedViews"},"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":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProjectSavedViewsInput"}}}}],"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":"savedViews"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"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":"BasicSavedView"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicSavedView"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SavedView"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"groupId"}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"isUngroupedViewsGroup"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIdString"}},{"kind":"Field","name":{"kind":"Name","value":"resourceIds"}},{"kind":"Field","name":{"kind":"Name","value":"isHomeView"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"viewerState"}},{"kind":"Field","name":{"kind":"Name","value":"screenshot"}},{"kind":"Field","name":{"kind":"Name","value":"position"}},{"kind":"Field","name":{"kind":"Name","value":"projectId"}}]}}]} as unknown as DocumentNode<GetProjectSavedViewsQuery, GetProjectSavedViewsQueryVariables>;
|
||||
export const CreateWorkspaceInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateWorkspaceInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"create"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<CreateWorkspaceInviteMutation, CreateWorkspaceInviteMutationVariables>;
|
||||
export const BatchCreateWorkspaceInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"BatchCreateWorkspaceInvites"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"WorkspaceInviteCreateInput"}}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspaceMutations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"invites"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"batchCreate"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"workspaceId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}},{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<BatchCreateWorkspaceInvitesMutation, BatchCreateWorkspaceInvitesMutationVariables>;
|
||||
export const GetWorkspaceWithTeamDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetWorkspaceWithTeam"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workspace"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"workspaceId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicWorkspace"}},{"kind":"Field","name":{"kind":"Name","value":"invitedTeam"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicWorkspace"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"Workspace"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"slug"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"readOnly"}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"BasicPendingWorkspaceCollaborator"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"PendingWorkspaceCollaborator"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"inviteId"}},{"kind":"Field","name":{"kind":"Name","value":"workspace"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"role"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"token"}}]}}]} as unknown as DocumentNode<GetWorkspaceWithTeamQuery, GetWorkspaceWithTeamQueryVariables>;
|
||||
|
||||
@@ -43,6 +43,38 @@ export type GetStoredViewCount = (params: { projectId: string }) => Promise<numb
|
||||
|
||||
export type GetStoredViewGroupCount = (params: { projectId: string }) => Promise<number>
|
||||
|
||||
export type GetProjectSavedViewsBaseParams = {
|
||||
/**
|
||||
* Falsy means - anonymous user (so no onlyAuthored filtering)
|
||||
*/
|
||||
userId?: MaybeNullOrUndefined<string>
|
||||
projectId: string
|
||||
/**
|
||||
* If not provided, views from all resources in the project will be returned.
|
||||
*/
|
||||
resourceIdString?: MaybeNullOrUndefined<string>
|
||||
onlyVisibility?: MaybeNullOrUndefined<SavedViewVisibility>
|
||||
search?: MaybeNullOrUndefined<string>
|
||||
}
|
||||
|
||||
export type GetProjectSavedViewsPageParams = GetProjectSavedViewsBaseParams & {
|
||||
limit?: MaybeNullOrUndefined<number>
|
||||
cursor?: MaybeNullOrUndefined<string>
|
||||
sortDirection?: MaybeNullOrUndefined<'asc' | 'desc'>
|
||||
/**
|
||||
* Null means - manual positioning
|
||||
*/
|
||||
sortBy?: MaybeNullOrUndefined<'createdAt' | 'name' | 'updatedAt' | 'position'>
|
||||
}
|
||||
|
||||
export type GetProjectSavedViewsTotalCount = (
|
||||
params: GetProjectSavedViewsBaseParams
|
||||
) => Promise<number>
|
||||
|
||||
export type GetProjectSavedViewsPageItems = (
|
||||
params: GetProjectSavedViewsPageParams
|
||||
) => Promise<Omit<Collection<SavedView>, 'totalCount'>>
|
||||
|
||||
export type GetProjectSavedViewGroupsBaseParams = {
|
||||
/**
|
||||
* Falsy means - anonymous user (so no onlyAuthored filtering)
|
||||
@@ -262,6 +294,10 @@ export type GetProjectSavedViewGroups = (
|
||||
params: GetProjectSavedViewGroupsPageParams
|
||||
) => Promise<Collection<SavedViewGroup>>
|
||||
|
||||
export type GetProjectSavedViews = (
|
||||
params: GetProjectSavedViewsPageParams
|
||||
) => Promise<Collection<SavedView>>
|
||||
|
||||
export type GetGroupSavedViews = (
|
||||
params: GetGroupSavedViewsPageParams
|
||||
) => Promise<Collection<SavedView>>
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
getNewViewSpecificPositionFactory,
|
||||
getProjectSavedViewGroupsPageItemsFactory,
|
||||
getProjectSavedViewGroupsTotalCountFactory,
|
||||
getProjectSavedViewsPageItemsFactory,
|
||||
getProjectSavedViewsTotalCountFactory,
|
||||
getStoredViewCountFactory,
|
||||
getStoredViewGroupCountFactory,
|
||||
getUngroupedSavedViewsGroupFactory,
|
||||
@@ -46,6 +48,7 @@ import {
|
||||
deleteSavedViewGroupFactory,
|
||||
getGroupSavedViewsFactory,
|
||||
getProjectSavedViewGroupsFactory,
|
||||
getProjectSavedViewsFactory,
|
||||
updateSavedViewFactory,
|
||||
updateSavedViewGroupFactory
|
||||
} from '@/modules/viewer/services/savedViewsManagement'
|
||||
@@ -89,6 +92,42 @@ const buildGetViewerResourceGroups = (params: {
|
||||
|
||||
const resolvers: Resolvers = {
|
||||
Project: {
|
||||
async savedViews(parent, args, ctx) {
|
||||
const { input } = args
|
||||
|
||||
const projectDb = await getProjectDbClient({ projectId: parent.id })
|
||||
const getProjectSavedViews = getProjectSavedViewsFactory({
|
||||
getProjectSavedViewsPageItems: getProjectSavedViewsPageItemsFactory({
|
||||
db: projectDb
|
||||
}),
|
||||
getProjectSavedViewsTotalCount: getProjectSavedViewsTotalCountFactory({
|
||||
db: projectDb
|
||||
}),
|
||||
getViewerResourceGroups: buildGetViewerResourceGroups({
|
||||
projectDb,
|
||||
loaders: ctx.loaders
|
||||
})
|
||||
})
|
||||
|
||||
const allowedSortBy = <const>['createdAt', 'name', 'updatedAt']
|
||||
const sortBy = input.sortBy
|
||||
? allowedSortBy.find((s) => s === input.sortBy)
|
||||
: undefined
|
||||
|
||||
return await getProjectSavedViews({
|
||||
projectId: parent.id,
|
||||
userId: ctx.userId,
|
||||
resourceIdString: input.resourceIdString,
|
||||
onlyVisibility: input.onlyVisibility,
|
||||
search: input.search,
|
||||
limit: input.limit,
|
||||
cursor: input.cursor,
|
||||
sortDirection: input.sortDirection
|
||||
? mapGqlToDbSortDirection(input.sortDirection)
|
||||
: undefined,
|
||||
sortBy
|
||||
})
|
||||
},
|
||||
async savedViewGroups(parent, args, ctx) {
|
||||
const { input } = args
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
GetGroupSavedViewsBaseParams,
|
||||
GetGroupSavedViewsPageItems,
|
||||
GetGroupSavedViewsTotalCount,
|
||||
GetProjectSavedViewsBaseParams,
|
||||
GetProjectSavedViewGroupsBaseParams,
|
||||
GetProjectSavedViewGroupsPageItems,
|
||||
GetProjectSavedViewGroupsTotalCount,
|
||||
@@ -31,7 +32,9 @@ import type {
|
||||
SetNewHomeView,
|
||||
GetNewViewBoundaryPosition,
|
||||
GetNewViewSpecificPosition,
|
||||
RebalanceViewPositions
|
||||
RebalanceViewPositions,
|
||||
GetProjectSavedViewsTotalCount,
|
||||
GetProjectSavedViewsPageItems
|
||||
} from '@/modules/viewer/domain/operations/savedViews'
|
||||
import {
|
||||
SavedViewVisibility,
|
||||
@@ -1020,3 +1023,81 @@ export const rebalancingViewPositionsFactory =
|
||||
const ret = (await q) as { rowCount: number }
|
||||
return ret.rowCount
|
||||
}
|
||||
|
||||
const getProjectSavedViewsBaseQueryFactory =
|
||||
(deps: { db: Knex }) => (params: GetProjectSavedViewsBaseParams) => {
|
||||
const { projectId, resourceIdString, search, userId } = params
|
||||
|
||||
const onlyVisibility =
|
||||
params.onlyVisibility === SavedViewVisibility.authorOnly && !userId
|
||||
? undefined
|
||||
: params.onlyVisibility
|
||||
const resourceIds = formatResourceIdsForGroup(resourceIdString || '')
|
||||
|
||||
const q = tables
|
||||
.savedViews(deps.db)
|
||||
.where({ [SavedViews.col.projectId]: projectId })
|
||||
|
||||
// If resourceIdString provided, filter by resource overlap
|
||||
if (resourceIds.length) {
|
||||
q.andWhereRaw('?? && ?', [SavedViews.col.groupResourceIds, resourceIds])
|
||||
}
|
||||
|
||||
// checking visibility/authorship
|
||||
if (onlyVisibility) {
|
||||
if (onlyVisibility === SavedViewVisibility.authorOnly) {
|
||||
q.andWhere({ [SavedViews.col.authorId]: userId })
|
||||
}
|
||||
q.andWhere({ [SavedViews.col.visibility]: onlyVisibility })
|
||||
} else {
|
||||
q.andWhere((w1) => {
|
||||
w1.andWhere(SavedViews.col.visibility, SavedViewVisibility.public)
|
||||
if (userId) {
|
||||
w1.orWhere(SavedViews.col.authorId, userId)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// search filter
|
||||
if (search) {
|
||||
q.andWhere(SavedViews.col.name, 'ilike', `%${search}%`)
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
export const getProjectSavedViewsTotalCountFactory =
|
||||
(deps: { db: Knex }): GetProjectSavedViewsTotalCount =>
|
||||
async (params) => {
|
||||
const q = getProjectSavedViewsBaseQueryFactory(deps)(params)
|
||||
const countQ = deps.db.count<{ count: string }[]>().from(q.as('sq1'))
|
||||
const [count] = await countQ
|
||||
return parseInt(count.count + '')
|
||||
}
|
||||
|
||||
export const getProjectSavedViewsPageItemsFactory =
|
||||
(deps: { db: Knex }): GetProjectSavedViewsPageItems =>
|
||||
async (params) => {
|
||||
const sortByCol = params.sortBy || 'position'
|
||||
const sortDir = params.sortDirection || 'desc'
|
||||
|
||||
const q = getProjectSavedViewsBaseQueryFactory(deps)(params)
|
||||
const { applyCursorSortAndFilter, resolveNewCursor } = compositeCursorTools({
|
||||
schema: SavedViews,
|
||||
cols: [sortByCol, 'id']
|
||||
})
|
||||
|
||||
const limit = clamp(params.limit ?? 10, 0, 100)
|
||||
q.limit(limit)
|
||||
|
||||
// Apply cursor filter and sort
|
||||
applyCursorSortAndFilter({ query: q, cursor: params.cursor, sort: sortDir })
|
||||
|
||||
const items = await q
|
||||
const newCursor = resolveNewCursor(items)
|
||||
|
||||
return {
|
||||
items,
|
||||
cursor: newCursor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ import type {
|
||||
GetProjectSavedViewGroups,
|
||||
GetProjectSavedViewGroupsPageItems,
|
||||
GetProjectSavedViewGroupsTotalCount,
|
||||
GetProjectSavedViews,
|
||||
GetProjectSavedViewsPageItems,
|
||||
GetProjectSavedViewsTotalCount,
|
||||
GetSavedView,
|
||||
GetSavedViewGroup,
|
||||
GetStoredViewCount,
|
||||
@@ -83,8 +86,12 @@ const formatIncomingScreenshotFactory =
|
||||
|
||||
const unwrapResourceIdStringFactory =
|
||||
(deps: { getViewerResourceGroups: GetViewerResourceGroups }) =>
|
||||
async (params: { resourceIdString: ViewerResourcesTarget; projectId: string }) => {
|
||||
const { resourceIdString, projectId } = params
|
||||
async (params: {
|
||||
resourceIdString: ViewerResourcesTarget
|
||||
projectId: string
|
||||
allowInvalid?: boolean
|
||||
}) => {
|
||||
const { resourceIdString, projectId, allowInvalid } = params
|
||||
|
||||
// It could be a `$prefix` string, in which case we need to resolve the final resources there
|
||||
const { groups: resourceGroups } = await deps.getViewerResourceGroups({
|
||||
@@ -94,6 +101,10 @@ const unwrapResourceIdStringFactory =
|
||||
allowEmptyModels: true
|
||||
})
|
||||
|
||||
// If completely empty - invalid string, return as is
|
||||
if (resourceGroups.length === 0 && allowInvalid)
|
||||
return resourceBuilder().addResources(resourceIdString)
|
||||
|
||||
const finalResourceIds = resourceBuilder()
|
||||
for (const resourceGroup of resourceGroups) {
|
||||
// If there's any items in there, add them
|
||||
@@ -1041,3 +1052,35 @@ export const updateSavedViewGroupFactory =
|
||||
|
||||
return updatedGroup
|
||||
}
|
||||
|
||||
export const getProjectSavedViewsFactory =
|
||||
(
|
||||
deps: {
|
||||
getProjectSavedViewsPageItems: GetProjectSavedViewsPageItems
|
||||
getProjectSavedViewsTotalCount: GetProjectSavedViewsTotalCount
|
||||
} & DependenciesOf<typeof unwrapResourceIdStringFactory>
|
||||
): GetProjectSavedViews =>
|
||||
async (params) => {
|
||||
// Resolve final resourceIdString from input one (may contain $prefix ids that need unwrapping)
|
||||
if (params.resourceIdString) {
|
||||
const finalResourceIds = await unwrapResourceIdStringFactory(deps)({
|
||||
resourceIdString: params.resourceIdString,
|
||||
projectId: params.projectId,
|
||||
allowInvalid: true // so that user gets empty results instead of results for empty resourceIdString
|
||||
})
|
||||
params.resourceIdString = finalResourceIds.toString()
|
||||
}
|
||||
|
||||
const noItemsNeeded = params.limit === 0
|
||||
const [totalCount, pageItems] = await Promise.all([
|
||||
deps.getProjectSavedViewsTotalCount(params),
|
||||
noItemsNeeded
|
||||
? Promise.resolve({ items: [], cursor: null })
|
||||
: deps.getProjectSavedViewsPageItems(params)
|
||||
])
|
||||
|
||||
return {
|
||||
totalCount,
|
||||
...pageItems
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,3 +321,20 @@ export const onProjectSavedViewGroupsUpdated = gql`
|
||||
|
||||
${basicSavedViewGroupFragment}
|
||||
`
|
||||
|
||||
export const getProjectSavedViewsQuery = gql`
|
||||
query GetProjectSavedViews($projectId: String!, $input: ProjectSavedViewsInput!) {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
savedViews(input: $input) {
|
||||
totalCount
|
||||
cursor
|
||||
items {
|
||||
...BasicSavedView
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
${basicSavedViewFragment}
|
||||
`
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
GetProjectSavedViewGroupsQueryVariables,
|
||||
GetProjectSavedViewIfExistsQueryVariables,
|
||||
GetProjectSavedViewQueryVariables,
|
||||
GetProjectSavedViewsQueryVariables,
|
||||
GetProjectUngroupedViewGroupQueryVariables,
|
||||
UpdateSavedViewGroupMutationVariables,
|
||||
UpdateSavedViewInput,
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
GetProjectSavedViewGroupDocument,
|
||||
GetProjectSavedViewGroupsDocument,
|
||||
GetProjectSavedViewIfExistsDocument,
|
||||
GetProjectSavedViewsDocument,
|
||||
GetProjectUngroupedViewGroupDocument,
|
||||
OnProjectSavedViewGroupsUpdatedDocument,
|
||||
OnProjectSavedViewsUpdatedDocument,
|
||||
@@ -3355,6 +3357,333 @@ const fakeViewerState = (overrides?: PartialDeep<ViewerState.SerializedViewerSta
|
||||
const sortedCreatedAt = [...createdAt].sort((a, b) => (b.isAfter(a) ? 1 : -1))
|
||||
expect(createdAt).to.deep.equal(sortedCreatedAt)
|
||||
})
|
||||
|
||||
describe('directly through Project.savedViews', () => {
|
||||
const getProjectViews = (
|
||||
input: GetProjectSavedViewsQueryVariables,
|
||||
options?: ExecuteOperationOptions
|
||||
) => apollo.execute(GetProjectSavedViewsDocument, input, options)
|
||||
|
||||
it('should successfully read all project views with pagination', async () => {
|
||||
let cursor: string | null = null
|
||||
let pagesLoaded = 0
|
||||
let viewsFound = 0
|
||||
|
||||
// First, get the actual total count from the endpoint
|
||||
const initialRes = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 1,
|
||||
resourceIdString: getAllReadModelResourceIds().toString()
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const TOTAL_EXPECTED_VIEWS =
|
||||
initialRes.data?.project.savedViews.totalCount || 0
|
||||
const PAGE_SIZE = Math.ceil(TOTAL_EXPECTED_VIEWS / 3) // Use 3 pages
|
||||
|
||||
const loadPage = async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: PAGE_SIZE,
|
||||
cursor,
|
||||
resourceIdString: getAllReadModelResourceIds().toString()
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
expect(data!.totalCount).to.equal(TOTAL_EXPECTED_VIEWS)
|
||||
|
||||
if (data?.cursor) {
|
||||
expect(data!.items.length).to.be.lessThanOrEqual(PAGE_SIZE)
|
||||
} else {
|
||||
expect(data!.items.length).to.eq(0)
|
||||
}
|
||||
|
||||
for (const view of data!.items) {
|
||||
expect(view.projectId).to.equal(readTestProject.id)
|
||||
expect(view.resourceIds.length).to.be.greaterThan(0)
|
||||
viewsFound++
|
||||
}
|
||||
|
||||
cursor = data?.cursor || null
|
||||
pagesLoaded++
|
||||
}
|
||||
|
||||
do {
|
||||
if (pagesLoaded > 5) {
|
||||
// Increase max pages
|
||||
throw new Error(
|
||||
'Too many pages loaded, something is wrong with pagination logic'
|
||||
)
|
||||
}
|
||||
|
||||
await loadPage()
|
||||
} while (cursor)
|
||||
|
||||
expect(pagesLoaded).to.equal(4) // 3 pages + 1 empty page
|
||||
expect(viewsFound).to.equal(TOTAL_EXPECTED_VIEWS)
|
||||
})
|
||||
|
||||
it('should respect search filter across all views', async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100, // all in 1 page
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
search: SEARCH_STRING
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
expect(data!.totalCount).to.be.greaterThan(0) // Should find some matching views
|
||||
expect(data!.items.length).to.equal(data!.totalCount)
|
||||
|
||||
for (const view of data!.items) {
|
||||
expect(view.name.includes(SEARCH_STRING)).to.be.true
|
||||
}
|
||||
})
|
||||
|
||||
it('should respect onlyAuthored flag across all views', async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100, // all in 1 page
|
||||
resourceIdString: getAllReadModelResourceIds().toString()
|
||||
}
|
||||
},
|
||||
{
|
||||
assertNoErrors: true,
|
||||
authUserId: otherReader.id
|
||||
}
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
|
||||
// Should see all public views visible to otherReader
|
||||
expect(data!.totalCount).to.be.greaterThan(0)
|
||||
expect(data!.items.length).to.equal(data!.totalCount)
|
||||
|
||||
// Should not see any private views from other authors
|
||||
const privateViewsFromOthers = data!.items.filter(
|
||||
(v) =>
|
||||
v.author?.id !== otherReader.id &&
|
||||
v.visibility === SavedViewVisibility.authorOnly
|
||||
)
|
||||
expect(privateViewsFromOthers.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('should allow sorting by name asc across all views', async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100, // all in 1 page
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
sortBy: 'name',
|
||||
sortDirection: 'ASC'
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
expect(data!.items.length).to.be.greaterThan(0)
|
||||
expect(data!.items.length).to.equal(data!.totalCount)
|
||||
|
||||
const names = data!.items.map((v) => v.name as string)
|
||||
const sortedNames = [...names].sort()
|
||||
expect(names).to.deep.equal(sortedNames)
|
||||
})
|
||||
|
||||
it('should allow sorting by createdAt desc across all views', async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100, // all in 1 page
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
sortBy: 'createdAt',
|
||||
sortDirection: 'DESC'
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
expect(data!.items.length).to.be.greaterThan(0)
|
||||
expect(data!.items.length).to.equal(data!.totalCount)
|
||||
|
||||
const createdAt = data!.items.map((v) => dayjs(v.createdAt))
|
||||
const sortedCreatedAt = [...createdAt].sort((a, b) =>
|
||||
b.isAfter(a) ? 1 : -1
|
||||
)
|
||||
expect(createdAt).to.deep.equal(sortedCreatedAt)
|
||||
})
|
||||
|
||||
it('should handle empty results for non-existent resource', async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100,
|
||||
resourceIdString: 'non-existent-resource-id'
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
expect(data!.totalCount).to.equal(0)
|
||||
expect(data!.items.length).to.equal(0)
|
||||
expect(data!.cursor).to.be.null
|
||||
})
|
||||
|
||||
it('should handle mixed visibility filtering correctly', async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100,
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
onlyVisibility: SavedViewVisibility.public
|
||||
}
|
||||
},
|
||||
{
|
||||
assertNoErrors: true,
|
||||
authUserId: otherReader.id
|
||||
}
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
|
||||
// Should only see public views
|
||||
expect(
|
||||
data!.items.every((v) => v.visibility === SavedViewVisibility.public)
|
||||
).to.be.true
|
||||
|
||||
// Should not see any private views
|
||||
const privateViews = data!.items.filter(
|
||||
(v) => v.visibility === SavedViewVisibility.authorOnly
|
||||
)
|
||||
expect(privateViews.length).to.equal(0)
|
||||
})
|
||||
|
||||
it('should respect both search and visibility filters combined', async () => {
|
||||
const res = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100,
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
search: SEARCH_STRING,
|
||||
onlyVisibility: SavedViewVisibility.public
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data = res.data?.project.savedViews
|
||||
expect(data).to.be.ok
|
||||
|
||||
// Should find views that match both search string AND are public
|
||||
for (const view of data!.items) {
|
||||
expect(view.name.includes(SEARCH_STRING)).to.be.true
|
||||
expect(view.visibility).to.equal(SavedViewVisibility.public)
|
||||
}
|
||||
|
||||
// Get search-only results to compare
|
||||
const searchOnlyRes = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: 100,
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
search: SEARCH_STRING
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
// Should be fewer or equal results than search alone
|
||||
expect(data!.totalCount).to.be.lessThanOrEqual(
|
||||
searchOnlyRes.data?.project.savedViews.totalCount || 0
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle cursor pagination with search filters', async () => {
|
||||
const PAGE_SIZE = 2
|
||||
let cursor: string | null = null
|
||||
let totalFound = 0
|
||||
|
||||
// First page
|
||||
const res1 = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: PAGE_SIZE,
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
search: SEARCH_STRING
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data1 = res1.data?.project.savedViews
|
||||
expect(data1).to.be.ok
|
||||
expect(data1!.items.length).to.be.lessThanOrEqual(PAGE_SIZE)
|
||||
totalFound += data1!.items.length
|
||||
cursor = data1!.cursor || null
|
||||
|
||||
if (cursor) {
|
||||
// Second page
|
||||
const res2 = await getProjectViews(
|
||||
{
|
||||
projectId: readTestProject.id,
|
||||
input: {
|
||||
limit: PAGE_SIZE,
|
||||
cursor,
|
||||
resourceIdString: getAllReadModelResourceIds().toString(),
|
||||
search: SEARCH_STRING
|
||||
}
|
||||
},
|
||||
{ assertNoErrors: true }
|
||||
)
|
||||
|
||||
const data2 = res2.data?.project.savedViews
|
||||
expect(data2).to.be.ok
|
||||
totalFound += data2!.items.length
|
||||
|
||||
// Verify no duplicate views across pages
|
||||
const ids1 = data1!.items.map((v) => v.id as string)
|
||||
const ids2 = data2!.items.map((v) => v.id as string)
|
||||
const ids1Set = new Set(ids1)
|
||||
const duplicateCount = ids2.filter((id) => ids1Set.has(id)).length
|
||||
expect(duplicateCount).to.equal(0)
|
||||
}
|
||||
|
||||
// Total should match expected searchable count
|
||||
expect(totalFound).to.be.lessThanOrEqual(SEARCHABLE_VIEW_COUNT)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
} else {
|
||||
|
||||
@@ -38,13 +38,27 @@ export function createUncoveredError(e: unknown) {
|
||||
return new UncoveredError(`Uncovered error case ${errorRepr}.`)
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom error class that produces a cleaner stack trace when instantiated.
|
||||
*/
|
||||
export class CleanStackTrace extends Error {
|
||||
constructor() {
|
||||
super('')
|
||||
this.name = 'Stack trace:'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Note: Only V8 and Node.js support controlling the stack trace limit
|
||||
*/
|
||||
export const collectLongTrace = (limit?: number) => {
|
||||
const originalLimit = Error.stackTraceLimit
|
||||
Error.stackTraceLimit = limit || 30
|
||||
const trace = (new Error().stack || '').split('\n').slice(1).join('\n').trim()
|
||||
const trace = (new CleanStackTrace().stack || '')
|
||||
.split('\n')
|
||||
.slice(2) // remove "Error" and this function's own frame
|
||||
.join('\n')
|
||||
.trim()
|
||||
Error.stackTraceLimit = originalLimit
|
||||
return trace
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { FaceSmileIcon } from '@heroicons/vue/24/outline'
|
||||
import type { Meta, StoryObj } from '@storybook/vue3'
|
||||
import { ref } from 'vue'
|
||||
import DialogSection from '~~/src/components/layout/DialogSection.vue'
|
||||
import { FormButton } from '~~/src/lib'
|
||||
|
||||
export default {
|
||||
component: DialogSection,
|
||||
@@ -76,3 +78,27 @@ export const ButtonExpandsContent: StoryObj = {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const WithModel: StoryObj = {
|
||||
...Default,
|
||||
render: (args) => ({
|
||||
components: { DialogSection, FormButton },
|
||||
setup() {
|
||||
const open = ref(false)
|
||||
return { args, open }
|
||||
},
|
||||
template: `
|
||||
<div class="bg-foundation flex flex-col gap-2">
|
||||
<DialogSection v-bind="args" v-model:open="open">
|
||||
<div class="flex flex-col text-foreground space-y-4">
|
||||
<div class="h4 font-semibold">Hello world!</div>
|
||||
<div>Lorem ipsum blah blah blah</div>
|
||||
</div>
|
||||
</DialogSection>
|
||||
<FormButton @click="open = !open">{{ open ? 'Close' : 'Open' }} Section</FormButton>
|
||||
</div>`
|
||||
}),
|
||||
args: {
|
||||
...Default.args
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,13 +84,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, unref, computed, nextTick } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { PropType, Ref } from 'vue'
|
||||
import { ChevronDownIcon } from '@heroicons/vue/24/outline'
|
||||
import { FormButton } from '~~/src/lib'
|
||||
import { keyboardClick } from '~~/src/helpers/global/accessibility'
|
||||
import type { PropAnyComponent } from '~~/src/helpers/common/components'
|
||||
import type { FormButtonStyle } from '~~/src/helpers/form/button'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
type TitleColor = 'default' | 'danger' | 'warning' | 'success' | 'secondary' | 'info'
|
||||
|
||||
@@ -125,8 +126,10 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const content: Ref<HTMLElement | null> = ref(null)
|
||||
const contentHeight = ref(0)
|
||||
const isExpanded = ref(false)
|
||||
const { height: scrollHeight } = useElementSize(content)
|
||||
|
||||
const contentHeight = computed(() => (scrollHeight.value || 0) + 64)
|
||||
const isExpanded = defineModel<boolean>('open', { required: false })
|
||||
|
||||
const backgroundClass = computed(() => {
|
||||
const classes = []
|
||||
@@ -155,12 +158,7 @@ const titleClasses = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const toggleExpansion = async () => {
|
||||
const toggleExpansion = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
|
||||
if (isExpanded.value) {
|
||||
await nextTick()
|
||||
contentHeight.value = (unref(content)?.scrollHeight || 0) + 64
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user