fix(dui): prevents empty publish selection state
fix(dui): prevents empty publish selection state
This commit is contained in:
@@ -32,13 +32,18 @@
|
|||||||
<FilterListSelect :filter="modelCard.sendFilter" @update:filter="updateFilter" />
|
<FilterListSelect :filter="modelCard.sendFilter" @update:filter="updateFilter" />
|
||||||
|
|
||||||
<div class="mt-4 flex justify-end items-center space-x-2">
|
<div class="mt-4 flex justify-end items-center space-x-2">
|
||||||
<FormButton size="sm" color="outline" @click.stop="saveFilter()">
|
<FormButton
|
||||||
|
size="sm"
|
||||||
|
color="outline"
|
||||||
|
:disabled="isSaveDisabled"
|
||||||
|
@click.stop="saveFilter()"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</FormButton>
|
</FormButton>
|
||||||
<div v-tippy="!canCreateVersionPerm ? canCreateVersionMessage : ''">
|
<div v-tippy="!canCreateVersionPerm ? canCreateVersionMessage : ''">
|
||||||
<FormButton
|
<FormButton
|
||||||
size="sm"
|
size="sm"
|
||||||
:disabled="!canCreateVersionPerm"
|
:disabled="!canCreateVersionPerm || isSaveDisabled"
|
||||||
@click.stop="saveFilterAndSend()"
|
@click.stop="saveFilterAndSend()"
|
||||||
>
|
>
|
||||||
Save & Publish
|
Save & Publish
|
||||||
@@ -114,7 +119,7 @@
|
|||||||
</ModelCardBase>
|
</ModelCardBase>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import ModelCardBase from '~/components/model/CardBase.vue'
|
import ModelCardBase from '~/components/model/CardBase.vue'
|
||||||
import { Square3Stack3DIcon } from '@heroicons/vue/20/solid'
|
import { Square3Stack3DIcon } from '@heroicons/vue/20/solid'
|
||||||
import type { ModelCardNotification } from '~/lib/models/card/notification'
|
import type { ModelCardNotification } from '~/lib/models/card/notification'
|
||||||
@@ -206,22 +211,28 @@ const sendOrCancel = () => {
|
|||||||
hasSetVersionMessage.value = false
|
hasSetVersionMessage.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let newFilter: ISendFilter
|
const newFilter = ref<ISendFilter>()
|
||||||
const updateFilter = (filter: ISendFilter) => {
|
const updateFilter = (filter: ISendFilter) => {
|
||||||
newFilter = filter
|
newFilter.value = filter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isSaveDisabled = computed(() => {
|
||||||
|
const filterToCheck = newFilter.value || props.modelCard.sendFilter
|
||||||
|
return !store.validateSendFilter(filterToCheck).valid
|
||||||
|
})
|
||||||
|
|
||||||
const saveFilter = async () => {
|
const saveFilter = async () => {
|
||||||
|
if (!newFilter.value) return // Safety check
|
||||||
void trackEvent('DUI3 Action', {
|
void trackEvent('DUI3 Action', {
|
||||||
name: 'Publish Card Filter Change',
|
name: 'Publish Card Filter Change',
|
||||||
filter: newFilter.typeDiscriminator
|
filter: newFilter.value.typeDiscriminator
|
||||||
})
|
})
|
||||||
|
|
||||||
// do not reset idmap while creating a new one because it is managed by host app
|
// do not reset idmap while creating a new one because it is managed by host app
|
||||||
newFilter.idMap = props.modelCard.sendFilter?.idMap
|
newFilter.value.idMap = props.modelCard.sendFilter?.idMap
|
||||||
|
|
||||||
await store.patchModel(props.modelCard.modelCardId, {
|
await store.patchModel(props.modelCard.modelCardId, {
|
||||||
sendFilter: newFilter,
|
sendFilter: newFilter.value,
|
||||||
expired: true
|
expired: true
|
||||||
})
|
})
|
||||||
openFilterDialog.value = false
|
openFilterDialog.value = false
|
||||||
|
|||||||
@@ -42,13 +42,10 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<div v-tippy="publishTooltipMessage" class="mt-2">
|
||||||
v-tippy="!canPublish && !isLoadingPermissions ? publishLimitMessage : ''"
|
|
||||||
class="mt-2"
|
|
||||||
>
|
|
||||||
<FormButton
|
<FormButton
|
||||||
full-width
|
full-width
|
||||||
:disabled="!canPublish || isLoadingPermissions"
|
:disabled="isPublishDisabled"
|
||||||
:loading="isLoadingPermissions"
|
:loading="isLoadingPermissions"
|
||||||
@click="addModel"
|
@click="addModel"
|
||||||
>
|
>
|
||||||
@@ -72,6 +69,7 @@ import type { ISendFilter } from '~/lib/models/card/send'
|
|||||||
import { SenderModelCard } from '~/lib/models/card/send'
|
import { SenderModelCard } from '~/lib/models/card/send'
|
||||||
import { useHostAppStore } from '~/store/hostApp'
|
import { useHostAppStore } from '~/store/hostApp'
|
||||||
import { useAccountStore } from '~/store/accounts'
|
import { useAccountStore } from '~/store/accounts'
|
||||||
|
import { useSelectionStore } from '~/store/selection'
|
||||||
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
import { useMixpanel } from '~/lib/core/composables/mixpanel'
|
||||||
import { useSettingsTracking } from '~/lib/core/composables/trackSettings'
|
import { useSettingsTracking } from '~/lib/core/composables/trackSettings'
|
||||||
import type { CardSetting } from '~/lib/models/card/setting'
|
import type { CardSetting } from '~/lib/models/card/setting'
|
||||||
@@ -103,6 +101,23 @@ const { canCreateModelIngestion, canCreateVersion } = useCheckGraphql()
|
|||||||
const canPublish = ref(false)
|
const canPublish = ref(false)
|
||||||
const publishLimitMessage = ref<string | undefined>(undefined)
|
const publishLimitMessage = ref<string | undefined>(undefined)
|
||||||
const isLoadingPermissions = ref(false)
|
const isLoadingPermissions = ref(false)
|
||||||
|
const hostAppStore = useHostAppStore()
|
||||||
|
const selectionStore = useSelectionStore()
|
||||||
|
|
||||||
|
const publishValidation = computed(() => hostAppStore.validateSendFilter(filter.value))
|
||||||
|
|
||||||
|
const isPublishDisabled = computed(() => {
|
||||||
|
return (
|
||||||
|
!canPublish.value || isLoadingPermissions.value || !publishValidation.value.valid
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const publishTooltipMessage = computed(() => {
|
||||||
|
if (!publishValidation.value.valid) return publishValidation.value.reason
|
||||||
|
if (!canPublish.value && !isLoadingPermissions.value)
|
||||||
|
return publishLimitMessage.value || ''
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
const updateSearchText = (text: string | undefined) => {
|
const updateSearchText = (text: string | undefined) => {
|
||||||
urlParseError.value = undefined
|
urlParseError.value = undefined
|
||||||
@@ -119,6 +134,7 @@ watch(urlParsedData, (newVal) => {
|
|||||||
watch(showSendDialog, (newVal) => {
|
watch(showSendDialog, (newVal) => {
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
urlParseError.value = undefined
|
urlParseError.value = undefined
|
||||||
|
void selectionStore.refreshSelectionFromHostApp()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -204,8 +220,6 @@ const selectModel = (model: ModelListModelItemFragment) => {
|
|||||||
void trackEvent('DUI3 Action', { name: 'Publish Wizard', step: 'model selected' })
|
void trackEvent('DUI3 Action', { name: 'Publish Wizard', step: 'model selected' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const hostAppStore = useHostAppStore()
|
|
||||||
|
|
||||||
// accountId, serverUrl, projectId, modelId, sendFilter, settings
|
// accountId, serverUrl, projectId, modelId, sendFilter, settings
|
||||||
const addModel = async () => {
|
const addModel = async () => {
|
||||||
void trackEvent('DUI3 Action', {
|
void trackEvent('DUI3 Action', {
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
|
import { computed } from 'vue'
|
||||||
|
import type {
|
||||||
|
ISendFilter,
|
||||||
|
SendFilterSelect,
|
||||||
|
RevitCategoriesSendFilter,
|
||||||
|
RevitViewsSendFilter
|
||||||
|
} from '~/lib/models/card/send'
|
||||||
import { ValidationHelpers } from '@speckle/ui-components'
|
import { ValidationHelpers } from '@speckle/ui-components'
|
||||||
import type { GenericValidateFunction } from 'vee-validate'
|
import type { GenericValidateFunction } from 'vee-validate'
|
||||||
|
|
||||||
|
export const isSelectFilter = (f: ISendFilter): f is SendFilterSelect =>
|
||||||
|
f.type === 'Select' || 'selectedItems' in f
|
||||||
|
|
||||||
|
export const isRevitCategoriesFilter = (
|
||||||
|
f: ISendFilter
|
||||||
|
): f is RevitCategoriesSendFilter =>
|
||||||
|
f.id === 'revitCategories' || f.id === 'archicadLayers'
|
||||||
|
|
||||||
|
export const isRevitViewsFilter = (f: ISendFilter): f is RevitViewsSendFilter =>
|
||||||
|
f.id === 'revitViews'
|
||||||
|
|
||||||
export const isEmail = ValidationHelpers.isEmail
|
export const isEmail = ValidationHelpers.isEmail
|
||||||
|
|
||||||
export const isOneOrMultipleEmails = ValidationHelpers.isOneOrMultipleEmails
|
export const isOneOrMultipleEmails = ValidationHelpers.isOneOrMultipleEmails
|
||||||
@@ -42,3 +60,42 @@ export function useModelNameValidationRules() {
|
|||||||
isValidModelName
|
isValidModelName
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type FilterValidationResult = { valid: boolean; reason?: string }
|
||||||
|
|
||||||
|
export function validateFilter(
|
||||||
|
filter: ISendFilter | undefined,
|
||||||
|
context: { selectionCount: number }
|
||||||
|
): FilterValidationResult {
|
||||||
|
if (!filter) return { valid: false, reason: 'No filter selected' }
|
||||||
|
|
||||||
|
// Selection Filter check
|
||||||
|
if (filter.name === 'Selection' || filter.id === 'selection') {
|
||||||
|
return context.selectionCount > 0
|
||||||
|
? { valid: true }
|
||||||
|
: { valid: false, reason: 'No objects selected to publish' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// List-based filters (Rhino Layers, etc.)
|
||||||
|
if (isSelectFilter(filter)) {
|
||||||
|
return (filter.selectedItems?.length ?? 0) > 0
|
||||||
|
? { valid: true }
|
||||||
|
: { valid: false, reason: 'No items selected to publish' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category-based filters
|
||||||
|
if (isRevitCategoriesFilter(filter)) {
|
||||||
|
return (filter.selectedCategories?.length ?? 0) > 0
|
||||||
|
? { valid: true }
|
||||||
|
: { valid: false, reason: 'No categories selected to publish' }
|
||||||
|
}
|
||||||
|
|
||||||
|
// View-based filters
|
||||||
|
if (isRevitViewsFilter(filter)) {
|
||||||
|
return filter.selectedView?.trim()
|
||||||
|
? { valid: true }
|
||||||
|
: { valid: false, reason: 'No view selected to publish' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true }
|
||||||
|
}
|
||||||
|
|||||||
+12
-1
@@ -13,6 +13,8 @@ import type {
|
|||||||
RevitViewsSendFilter,
|
RevitViewsSendFilter,
|
||||||
SendFilterSelect
|
SendFilterSelect
|
||||||
} from '~/lib/models/card/send'
|
} from '~/lib/models/card/send'
|
||||||
|
import { useSelectionStore } from '~/store/selection'
|
||||||
|
import { validateFilter } from '~/lib/validation'
|
||||||
import type { ToastNotification } from '@speckle/ui-components'
|
import type { ToastNotification } from '@speckle/ui-components'
|
||||||
import { ToastNotificationType } from '@speckle/ui-components'
|
import { ToastNotificationType } from '@speckle/ui-components'
|
||||||
import type { Nullable } from '@speckle/shared'
|
import type { Nullable } from '@speckle/shared'
|
||||||
@@ -368,6 +370,14 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
|||||||
*/
|
*/
|
||||||
app.$sendBinding?.on('refreshSendFilters', () => void refreshSendFilters())
|
app.$sendBinding?.on('refreshSendFilters', () => void refreshSendFilters())
|
||||||
|
|
||||||
|
const validateSendFilter = (filter?: ISendFilter) => {
|
||||||
|
const selectionStore = useSelectionStore()
|
||||||
|
|
||||||
|
return validateFilter(filter, {
|
||||||
|
selectionCount: selectionStore.selectionInfo.selectedObjectIds?.length ?? 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send functionality
|
* Send functionality
|
||||||
*/
|
*/
|
||||||
@@ -938,6 +948,7 @@ export const useHostAppStore = defineStore('hostAppStore', () => {
|
|||||||
getSendSettings,
|
getSendSettings,
|
||||||
setModelSendResult,
|
setModelSendResult,
|
||||||
setModelReceiveResult,
|
setModelReceiveResult,
|
||||||
handleModelProgressEvents
|
handleModelProgressEvents,
|
||||||
|
validateSendFilter
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user