feat: saved views search (#5266)

* feat: saved view search

* caching fixes

* clean up chromatic
This commit is contained in:
Kristaps Fabians Geikins
2025-08-20 10:58:35 +03:00
committed by GitHub
parent 028e4b713e
commit 79ccd28828
12 changed files with 104 additions and 759 deletions
-22
View File
@@ -117,28 +117,6 @@ jobs:
run: yarn build
working-directory: 'packages/viewer-sandbox'
ui-components-chromatic:
env:
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
name: UI components chromatic
runs-on: blacksmith
continue-on-error: ${{ inputs.CONTINUE_ON_ERROR }}
steps:
- uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- uses: useblacksmith/setup-node@v5
with:
node-version: 22
cache: yarn
- name: Install dependencies
run: YARN_ENABLE_HARDENED_MODE=0 PUPPETEER_SKIP_DOWNLOAD=true PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 yarn --immutable
- name: Build public packages
run: yarn build:public
- name: Run chromatic
run: yarn chromatic
working-directory: 'packages/ui-components'
test-shared:
name: Shared
runs-on: blacksmith
@@ -3,11 +3,13 @@
<div
class="flex shrink-0 justify-between items-center border-b border-outline-3 h-10 pl-4 pr-2.5"
>
<div class="text-body-xs text-foreground font-medium leading-none">
<span v-if="title" class="truncate">{{ title }}</span>
<slot name="title"></slot>
</div>
<slot name="actions"></slot>
<slot name="fullTitle">
<div class="text-body-xs text-foreground font-medium leading-none">
<span v-if="title" class="truncate">{{ title }}</span>
<slot name="title"></slot>
</div>
<slot name="actions"></slot>
</slot>
</div>
<div
:class="[
@@ -8,11 +8,11 @@
<template #actions>
<div v-if="!isLowerPlan" class="flex items-center gap-0.5">
<FormButton
v-if="false"
size="sm"
color="subtle"
:icon-left="Search"
hide-text
@click="setSearchMode(true)"
/>
<div v-tippy="canCreateViewOrGroup?.errorMessage" class="flex items-center">
<FormButton
@@ -38,6 +38,27 @@
</div>
</div>
</template>
<template v-if="searchMode" #fullTitle>
<div class="self-center w-full pr-2 flex gap-2 items-center">
<FormTextInput
v-bind="bind"
name="search"
placeholder="Search"
color="foundation"
auto-focus
v-on="on"
/>
<FormButton
v-tippy="'Exit search'"
size="sm"
color="subtle"
:icon-left="X"
hide-text
name="disableSearch"
@click="setSearchMode(false)"
/>
</div>
</template>
<template v-if="!isLowerPlan">
<div class="px-4 pt-2">
<ViewerButtonGroup>
@@ -58,6 +79,7 @@
<ViewerSavedViewsPanelGroups
v-model:selected-group-id="selectedGroupId"
:views-type="selectedViewsType"
:search="searchMode ? search || undefined : undefined"
/>
</div>
<div
@@ -78,7 +100,7 @@
</template>
<script setup lang="ts">
import { useMutationLoading } from '@vue/apollo-composable'
import { Search, FolderPlus, Plus } from 'lucide-vue-next'
import { Search, FolderPlus, Plus, X } from 'lucide-vue-next'
import { useSynchronizedCookie } from '~/lib/common/composables/reactiveCookie'
import { graphql } from '~/lib/common/generated/gql'
import {
@@ -91,6 +113,7 @@ import {
} from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { ViewsType, viewsTypeLabels } from '~/lib/viewer/helpers/savedViews'
import { useDebouncedTextInput } from '@speckle/ui-components'
graphql(`
fragment ViewerSavedViewsPanel_Project on Project {
@@ -122,6 +145,7 @@ const {
const createGroup = useCreateSavedViewGroup()
const createSavedView = useCreateSavedView()
const isLoading = useMutationLoading()
const { on, bind, value: search } = useDebouncedTextInput()
const selectedViewsType = ref<ViewsType>(ViewsType.Personal)
const selectedGroupId = ref<string | null>(null)
@@ -131,6 +155,7 @@ const hideViewerSeatDisclaimer = useSynchronizedCookie<boolean>(
default: () => false
}
)
const searchMode = ref(false)
const canCreateViewOrGroup = computed(
() => project.value?.permissions.canCreateSavedView
@@ -165,4 +190,14 @@ const onAddGroup = async () => {
selectedGroupId.value = group.id
}
}
const setSearchMode = (val: boolean) => {
if (val) {
searchMode.value = true
} else {
searchMode.value = false
}
search.value = ''
}
</script>
@@ -14,6 +14,7 @@
:views-type="viewsType"
:group="group"
:project="project"
:search="search"
:is-selected="isGroupSelected(group)"
:rename-mode="isGroupInRenameMode(group)"
@update:is-selected="(value) => (selectedGroupId = value ? group.id : null)"
@@ -92,6 +93,7 @@ const paginableGroupsQuery = graphql(`
const props = defineProps<{
viewsType: ViewsType
search?: string
}>()
const selectedGroupId = defineModel<string | null>('selectedGroupId', {
@@ -106,7 +108,6 @@ const {
} = useInjectedViewerState()
const eventBus = useEventBus()
const search = ref('')
const viewBeingEdited = ref<ViewerSavedViewsPanelViewEditDialog_SavedViewFragment>()
const viewBeingMoved = ref<ViewerSavedViewsPanelViewMoveDialog_SavedViewFragment>()
const viewBeingDeleted = ref<ViewerSavedViewsPanelViewDeleteDialog_SavedViewFragment>()
@@ -126,7 +127,7 @@ const {
savedViewGroupsInput: {
resourceIdString: resourceIdString.value,
cursor: null as null | string,
search: search.value?.trim() || null,
search: props.search?.trim() || null,
...viewsTypeToFilters(props.viewsType)
}
})),
@@ -148,7 +149,7 @@ const {
const hasGroups = computed(
() => (result.value?.project.savedViewGroups.items.length || 0) > 0
)
const isSearch = computed(() => search.value?.trim().length > 0)
const isSearch = computed(() => (props.search || '').trim().length > 0)
const emptyStateType = computed(() => (isSearch.value ? 'search' : 'base'))
const project = computed(() => result.value?.project)
@@ -206,8 +207,10 @@ watch(
groups,
(newGroups) => {
if (newGroups.length && !selectedGroupId.value) {
// open default group, if any
selectedGroupId.value = newGroups.find((g) => g.isUngroupedViewsGroup)?.id || null
selectedGroupId.value =
(props.search
? newGroups[0].id
: newGroups.find((g) => !g.isUngroupedViewsGroup)?.id) || null
}
},
{ immediate: true }
@@ -52,6 +52,7 @@ import {
import { isRequired, isStringOfLength } from '~/lib/common/helpers/validation'
import { useUpdateSavedView } from '~/lib/viewer/composables/savedViews/management'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import { isUndefined } from 'lodash-es'
graphql(`
fragment ViewerSavedViewsPanelViewEditDialog_SavedView on SavedView {
@@ -128,22 +129,30 @@ const onSubmit = handleSubmit(async (values) => {
const name =
values.name.trim() && values.name.trim() !== props.view.name
? values.name.trim()
: null
: undefined
const description =
values.description?.trim() !== (props.view.description || undefined)
? values.description?.trim() || null
: null
: undefined
const visibility =
values.visibility !== props.view.visibility ? values.visibility : null
const groupId = values.group.id !== props.view.group.id ? values.group.id : null
values.visibility !== props.view.visibility ? values.visibility : undefined
const groupId = values.group.id !== props.view.group.id ? values.group.id : undefined
const coreInput = {
...(isUndefined(name) ? {} : { name }),
...(isUndefined(description) ? {} : { description }),
...(isUndefined(visibility) ? {} : { visibility }),
...(isUndefined(groupId) ? {} : { groupId })
}
if (!Object.keys(coreInput).length) {
open.value = false
return
}
const res = await updateView({
view: props.view,
input: {
name,
description,
visibility,
groupId,
...coreInput,
id: props.view.id,
projectId: props.view.projectId
}
@@ -1,7 +1,7 @@
<template>
<div class="flex flex-col gap-8 items-center my-16">
<IllustrationEmptystateViewsTab />
<div class="text-foreground-2">{{ message }}</div>
<div class="text-foreground-2 px-6">{{ message }}</div>
</div>
</template>
<script setup lang="ts">
@@ -699,8 +699,7 @@ export const modifyObjectField = <
'filter',
'search',
'filter.search',
'filter.onlyAuthored',
'filter.onlyVisibility',
'input.search',
...(isArray(autoEvictFiltered) ? autoEvictFiltered : [])
]
const hasFilter = commonFilters.some(checkFilter)
@@ -14,6 +14,7 @@ import type {
import { useStateSerialization } from '~/lib/viewer/composables/serialization'
import { useInjectedViewerState } from '~/lib/viewer/composables/setup'
import {
filterKeys,
onGroupViewRemovalCacheUpdates,
onNewGroupViewCacheUpdates
} from '~/lib/viewer/helpers/savedViews/cache'
@@ -349,7 +350,7 @@ export const useCreateSavedViewGroup = () => {
return newItems
})
}),
{ autoEvictFiltered: true }
{ autoEvictFiltered: filterKeys }
)
}
}
@@ -425,7 +426,7 @@ export const useDeleteSavedViewGroup = () => {
return newItems
})
}),
{ autoEvictFiltered: true }
{ autoEvictFiltered: filterKeys }
)
// Possibly a bunch of views got moved back to Ungrouped as well
@@ -1,6 +1,15 @@
import type { ApolloCache } from '@apollo/client/cache'
import { isUngroupedGroup } from '@speckle/shared/saved-views'
export const filterKeys = [
'input.search',
'input.onlyAuthored',
'input.onlyVisibility',
'filter.search',
'filter.onlyAuthored',
'filter.onlyVisibility'
]
/**
* Cache mutations for when a group gets a new view:
* - If new group, Project.savedViewGroups + 1
@@ -36,7 +45,7 @@ export const onNewGroupViewCacheUpdates = (
update('items', (items) => [...items, ref('SavedViewGroup', groupId)])
})
},
{ autoEvictFiltered: true }
{ autoEvictFiltered: filterKeys }
)
// SavedViewGroup.views + 1
@@ -50,7 +59,7 @@ export const onNewGroupViewCacheUpdates = (
update('items', (items) => [ref('SavedView', viewId), ...items])
})
},
{ autoEvictFiltered: true }
{ autoEvictFiltered: filterKeys }
)
}
@@ -115,7 +124,7 @@ export const onGroupViewRemovalCacheUpdates = (
)
})
},
{ autoEvictFiltered: true }
{ autoEvictFiltered: filterKeys }
)
// Evict entirely
@@ -133,7 +142,7 @@ export const onGroupViewRemovalCacheUpdates = (
update('items', (items) => items.filter((item) => fromRef(item).id !== id))
})
},
{ autoEvictFiltered: true }
{ autoEvictFiltered: filterKeys }
)
}
}
-1
View File
@@ -120,7 +120,6 @@
"@typescript-eslint/parser": "^7.12.0",
"autoprefixer": "^10.4.14",
"browserify-zlib": "^0.2.0",
"chromatic": "^6.11.4",
"concurrently": "^9.1.2",
"eslint": "^9.4.0",
"eslint-config-prettier": "^9.1.0",
-2
View File
@@ -12,7 +12,6 @@
"storybook:test": "test-storybook",
"storybook:test:ci": "concurrently -k -s first -n \"SB,TEST\" -c \"magenta,blue\" \"yarn build-storybook --quiet && npx http-server storybook-static --port 6006 --silent\" \"wait-on tcp:6006 && yarn test-storybook --ci\"",
"storybook:test:watch": "test-storybook --watch",
"chromatic": "chromatic --exit-zero-on-changes --exit-once-uploaded",
"lint:js": "eslint .",
"lint:tsc": "vue-tsc --noEmit",
"lint:prettier": "prettier --config ../../.prettierrc --ignore-path ../../.prettierignore --check .",
@@ -80,7 +79,6 @@
"@vitejs/plugin-vue": "^5.2.1",
"autoprefixer": "^10.4.14",
"browserify-zlib": "^0.2.0",
"chromatic": "^6.17.4",
"concurrently": "^8.0.1",
"eslint": "^9.4.0",
"eslint-config-prettier": "^9.1.0",
+17 -705
View File
File diff suppressed because it is too large Load Diff