Files
speckle-server/packages/frontend-2/components/project/page/models/Header.vue
T
Kristaps Fabians Geikins 596312ab0e feat(frontend): personal project limit disclaimers & prompts (#4822)
* ProjectsAdd wrapper

* WorkspaceMoveProject wrapper added

* move wrapper finalized

* passing through location

* more cleanup

* model add wrapper

* permissions cleanup

* add invite wrapper

* vue-tippy bugfix

* ViewerLimitsDialog prep

* upgrade limit alert prep

* limit alerts

* movemanager fix

* new add flow

* slug update fix

* add model flow

* invites?

* some extra fixes

* move unmount fix?

* more fixes

* vue-tsc update

* style: remove h-32 for smaller screens

* vue-tsc parser fix

* prep for new viewer limits dialog

* updated viewer dialogs

* comment variant cleanup

* CR comments

---------

Co-authored-by: michalspeckle <michal@speckle.systems>
2025-05-28 12:12:18 +03:00

241 lines
6.8 KiB
Vue

<template>
<div>
<div
class="flex flex-col space-y-2 xl:space-y-0 xl:flex-row xl:justify-between xl:items-center mb-4 mt-3"
>
<div class="flex justify-between items-center flex-wrap xl:flex-nowrap">
<h1 class="block text-heading-lg">Models</h1>
<div class="flex items-center space-x-2 w-full mt-2 sm:w-auto sm:mt-0">
<FormButton
color="outline"
:disabled="project?.models.totalCount === 0"
class="grow inline-flex sm:grow-0 lg:hidden"
@click="onViewAllClick"
>
View all in 3D
</FormButton>
<div
v-tippy="canCreateModel.cantClickCreateReason.value"
class="grow inline-flex sm:grow-0 lg:hidden"
>
<FormButton
:disabled="!canCreateModel.canClickCreate.value"
@click="handleCreateModelClick"
>
New model
</FormButton>
</div>
</div>
</div>
<div
class="flex flex-col space-y-2 xl:space-y-0 xl:flex-row xl:items-center xl:space-x-2"
>
<FormTextInput
v-model="localSearch"
name="modelsearch"
:show-label="false"
placeholder="Search models..."
color="foundation"
wrapper-classes="grow lg:grow-0 xl:ml-2 xl:w-40 min-w-40 shrink-0"
:show-clear="localSearch !== ''"
@change="($event) => updateSearchImmediately($event.value)"
@update:model-value="updateDebouncedSearch"
/>
<div
class="flex flex-col space-y-2 sm:flex-row sm:items-center sm:space-x-2 sm:space-y-0"
>
<FormSelectUsers
v-model="finalSelectedMembers"
:users="team"
multiple
selector-placeholder="All members"
label="Filter by members"
class="grow shrink sm:w-[120px] md:w-44"
clearable
fixed-height
/>
<div class="flex items-center space-x-2 grow">
<FormSelectSourceApps
v-model="finalSelectedApps"
:items="availableSourceApps"
multiple
selector-placeholder="All sources"
label="Filter by sources"
class="grow shrink sm:w-[120px] md:w-44"
clearable
fixed-height
:label-id="sourceAppsLabelId"
:button-id="sourceAppsBtnId"
/>
<LayoutGridListToggle v-model="finalGridOrList" class="shrink-0" />
</div>
<FormButton
color="outline"
class="hidden lg:inline-flex shrink-0"
:disabled="project?.models.totalCount === 0"
@click="onViewAllClick"
>
View all in 3D
</FormButton>
<div v-tippy="canCreateModel.cantClickCreateReason.value" class="test123">
<FormButton
:disabled="!canCreateModel.canClickCreate.value"
class="hidden lg:inline-flex shrink-0"
@click="handleCreateModelClick"
>
New model
</FormButton>
</div>
</div>
</div>
</div>
<ProjectModelsAdd v-model:open="showNewDialog" :project="project" />
</div>
</template>
<script setup lang="ts">
import { SourceApps, SpeckleViewer } from '@speckle/shared'
import type { SourceAppDefinition } from '@speckle/shared'
import { debounce } from 'lodash-es'
import { graphql } from '~~/lib/common/generated/gql'
import type {
FormUsersSelectItemFragment,
ProjectModelsPageHeader_ProjectFragment
} from '~~/lib/common/generated/gql/graphql'
import { modelRoute } from '~~/lib/common/helpers/route'
import type { GridListToggleValue } from '~~/lib/layout/helpers/components'
import { useMixpanel } from '~~/lib/core/composables/mp'
import { useCanCreateModel } from '~/lib/projects/composables/permissions'
const emit = defineEmits<{
(e: 'update:selected-members', val: FormUsersSelectItemFragment[]): void
(e: 'update:selected-apps', val: SourceAppDefinition[]): void
(e: 'update:grid-or-list', val: GridListToggleValue): void
(e: 'update:search', val: string): void
}>()
/**
* TODO: Bug, tooltip shows old version sometimes
*/
graphql(`
fragment ProjectModelsPageHeader_Project on Project {
id
name
sourceApps
role
models {
totalCount
}
team {
id
user {
...FormUsersSelectItem
}
}
workspace {
id
role
slug
name
readOnly
plan {
name
}
}
permissions {
canCreateModel {
...FullPermissionCheckResult
}
}
...ProjectModelsAdd_Project
}
`)
const props = defineProps<{
projectId: string
project?: ProjectModelsPageHeader_ProjectFragment
selectedMembers: FormUsersSelectItemFragment[]
selectedApps: SourceAppDefinition[]
search: string
gridOrList: GridListToggleValue
disabled?: boolean
}>()
const localSearch = ref('')
const sourceAppsLabelId = useId()
const sourceAppsBtnId = useId()
const router = useRouter()
const mp = useMixpanel()
const onViewAllClick = () => {
router.push(allModelsRoute.value)
mp.track('Viewer Action', {
type: 'action',
name: 'federation',
action: 'view-all',
source: 'project page'
})
}
const showNewDialog = ref(false)
const canCreateModel = useCanCreateModel({
project: computed(() => props.project)
})
const debouncedSearch = computed({
get: () => props.search,
set: (newVal) => emit('update:search', newVal)
})
const finalSelectedMembers = computed({
get: () => props.selectedMembers,
set: (newVal) => emit('update:selected-members', newVal)
})
const finalSelectedApps = computed({
get: () => props.selectedApps,
set: (newVal) => emit('update:selected-apps', newVal)
})
const finalGridOrList = computed({
get: () => props.gridOrList,
set: (newVal) => emit('update:grid-or-list', newVal)
})
const availableSourceApps = computed((): SourceAppDefinition[] =>
props.project
? SourceApps.filter((a) =>
props.project!.sourceApps.find((pa) => pa.toLowerCase().includes(a.searchKey))
)
: []
)
const allModelsRoute = computed(() => {
const resourceIdString = SpeckleViewer.ViewerRoute.resourceBuilder()
.addAllModels()
.toString()
return modelRoute(props.projectId, resourceIdString)
})
const team = computed(() => props.project?.team.map((t) => t.user) || [])
const updateDebouncedSearch = debounce(() => {
debouncedSearch.value = localSearch.value.trim()
}, 500)
const updateSearchImmediately = (val?: string) => {
updateDebouncedSearch.cancel()
debouncedSearch.value = (val ?? localSearch.value).trim()
}
const handleCreateModelClick = () => {
showNewDialog.value = true
}
watch(debouncedSearch, (newVal) => {
if (newVal !== localSearch.value) {
localSearch.value = newVal
}
})
</script>