Feat: Rename and Delete for imported Models ☁️ (#6969)

## Summary

Add Rename and Delete options for Personal Models.

Also updates and standardizes some styles for Cards and adds a simple
Confirmation dialog.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6969-WIP-Feat-Rename-and-Delete-for-custom-Models-2b86d73d36508140a687e929b0544ae6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Alexander Brown
2025-12-01 17:16:05 -08:00
committed by GitHub
parent 072f1f6ced
commit 04158deb02
21 changed files with 457 additions and 162 deletions

View File

@@ -1,17 +1,15 @@
<template>
<div :class="iconGroupClasses">
<div
:class="
cn(
'flex justify-center items-center shrink-0 outline-hidden border-none p-0 rounded-lg bg-secondary-background shadow-sm transition-all duration-200 cursor-pointer'
)
"
>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { cn } from '@/utils/tailwindUtil'
const iconGroupClasses = cn(
'flex justify-center items-center shrink-0',
'outline-hidden border-none p-0 rounded-lg',
'bg-secondary-background shadow-sm',
'transition-all duration-200',
'cursor-pointer'
)
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="relative inline-flex items-center">
<IconButton :size="size" :type="type" @click="toggle">
<IconButton :size="size" :type="type" @click="popover?.toggle">
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
<i v-else class="icon-[lucide--more-vertical] text-sm" />
</IconButton>
@@ -25,8 +25,18 @@
)
}
}"
@show="$emit('menuOpened')"
@hide="$emit('menuClosed')"
@show="
() => {
isOpen = true
$emit('menuOpened')
}
"
@hide="
() => {
isOpen = false
$emit('menuClosed')
}
"
>
<div class="flex min-w-40 flex-col gap-2 p-2">
<slot :close="hide" />
@@ -48,8 +58,6 @@ interface MoreButtonProps extends BaseButtonProps {
isVertical?: boolean
}
const popover = ref<InstanceType<typeof Popover>>()
const {
size = 'md',
type = 'secondary',
@@ -61,15 +69,15 @@ defineEmits<{
menuClosed: []
}>()
const toggle = (event: Event) => {
popover.value?.toggle(event)
}
const isOpen = ref(false)
const popover = ref<InstanceType<typeof Popover>>()
const hide = () => {
function hide() {
popover.value?.hide()
}
defineExpose({
hide
hide,
isOpen
})
</script>

View File

@@ -18,8 +18,8 @@
...inputAttrs
}
}"
@keyup.enter="blurInputElement"
@keyup.escape="cancelEditing"
@keyup.enter.capture.stop="blurInputElement"
@keyup.escape.stop="cancelEditing"
@click.stop
@pointerdown.stop.capture
@pointermove.stop.capture
@@ -38,7 +38,7 @@ const {
} = defineProps<{
modelValue: string
isEditing?: boolean
inputAttrs?: Record<string, any>
inputAttrs?: Record<string, string>
}>()
const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])

View File

@@ -14,6 +14,7 @@
<component
:is="item.headerComponent"
v-if="item.headerComponent"
v-bind="item.headerProps"
:id="item.key"
/>
<h3 v-else :id="item.key">

View File

@@ -0,0 +1,19 @@
<template>
<div
class="flex flex-col px-4 py-2 text-sm text-muted-foreground border-t border-border-default"
>
<p v-if="promptTextReal">
{{ promptTextReal }}
</p>
</div>
</template>
<script setup lang="ts">
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
const { promptText } = defineProps<{
promptText?: MaybeRefOrGetter<string>
}>()
const promptTextReal = computed(() => toValue(promptText))
</script>

View File

@@ -0,0 +1,43 @@
<template>
<section class="w-full flex gap-2 justify-end px-2 pb-2">
<TextButton
:label="cancelTextX"
:disabled
type="transparent"
autofocus
@click="$emit('cancel')"
/>
<TextButton
:label="confirmTextX"
:disabled
type="transparent"
:class="confirmClass"
@click="$emit('confirm')"
/>
</section>
</template>
<script setup lang="ts">
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
import TextButton from '@/components/button/TextButton.vue'
const { t } = useI18n()
const { cancelText, confirmText, confirmClass, optionsDisabled } = defineProps<{
cancelText?: string
confirmText?: string
confirmClass?: string
optionsDisabled?: MaybeRefOrGetter<boolean>
}>()
defineEmits<{
cancel: []
confirm: []
}>()
const confirmTextX = computed(() => confirmText || t('g.confirm'))
const cancelTextX = computed(() => cancelText || t('g.cancel'))
const disabled = computed(() => toValue(optionsDisabled))
</script>

View File

@@ -0,0 +1,12 @@
<template>
<div
class="flex items-center gap-2 p-4 font-bold text-sm text-base-foreground font-inter"
>
<span v-if="title" class="flex-auto">{{ title }}</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
title?: string
}>()
</script>

View File

@@ -0,0 +1,31 @@
import ConfirmBody from '@/components/dialog/confirm/ConfirmBody.vue'
import ConfirmFooter from '@/components/dialog/confirm/ConfirmFooter.vue'
import ConfirmHeader from '@/components/dialog/confirm/ConfirmHeader.vue'
import { useDialogStore } from '@/stores/dialogStore'
import type { ComponentAttrs } from 'vue-component-type-helpers'
interface ConfirmDialogOptions {
headerProps?: ComponentAttrs<typeof ConfirmHeader>
props?: ComponentAttrs<typeof ConfirmBody>
footerProps?: ComponentAttrs<typeof ConfirmFooter>
}
export function showConfirmDialog(options: ConfirmDialogOptions = {}) {
const dialogStore = useDialogStore()
const { headerProps, props, footerProps } = options
return dialogStore.showDialog({
headerComponent: ConfirmHeader,
component: ConfirmBody,
footerComponent: ConfirmFooter,
headerProps,
props,
footerProps,
dialogComponentProps: {
pt: {
header: 'py-0! px-0!',
content: 'p-0!',
footer: 'p-0!'
}
}
})
}

View File

@@ -9,7 +9,8 @@ export enum ServerFeatureFlag {
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
MAX_UPLOAD_SIZE = 'max_upload_size',
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled'
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled'
}
/**
@@ -31,6 +32,12 @@ export function useFeatureFlags() {
ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED,
false
)
},
get assetUpdateOptionsEnabled() {
return api.getServerFeature(
ServerFeatureFlag.ASSET_UPDATE_OPTIONS_ENABLED,
false
)
}
})

View File

@@ -163,6 +163,7 @@ export const i18n = createI18n({
legacy: false,
locale: navigator.language.split('-')[0] || 'en',
fallbackLocale: 'en',
escapeParameter: true,
messages,
// Ignore warnings for locale options as each option is in its own language.
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français", "Español"

View File

@@ -1,6 +1,5 @@
{
"g": {
"beta": "Beta",
"user": "User",
"currentUser": "Current user",
"empty": "Empty",
@@ -125,6 +124,7 @@
"searchKeybindings": "Search Keybindings",
"searchExtensions": "Search Extensions",
"search": "Search",
"searchPlaceholder": "Search...",
"noResultsFound": "No Results Found",
"searchFailedMessage": "We couldn't find any settings matching your search. Try adjusting your search terms.",
"noTasksFound": "No Tasks Found",
@@ -2093,7 +2093,6 @@
"connectionError": "Please check your connection and try again",
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Type to search...",
"uploadModel": "Import model",
"uploadModelFromCivitai": "Import a model from Civitai",
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
@@ -2152,6 +2151,16 @@
"media": {
"threeDModelPlaceholder": "3D Model",
"audioPlaceholder": "Audio"
},
"deletion": {
"header": "Delete this model?",
"body": "This model will be permanently removed from your library.",
"inProgress": "Deleting {assetName}...",
"complete": "{assetName} has been deleted.",
"failed": "{assetName} could not be deleted."
},
"rename": {
"failed": "Could not rename asset."
}
},
"mediaAsset": {

View File

@@ -1,5 +1,5 @@
<template>
<div class="absolute right-2 bottom-2 flex flex-wrap justify-end gap-1">
<div class="absolute left-2 bottom-2 flex flex-wrap justify-start gap-1">
<span
v-for="badge in badges"
:key="badge.label"

View File

@@ -26,7 +26,7 @@
v-model="searchQuery"
:autofocus="true"
size="lg"
:placeholder="$t('assetBrowser.searchAssetsPlaceholder')"
:placeholder="$t('g.searchPlaceholder')"
class="max-w-96"
/>
<IconTextButton

View File

@@ -1,53 +1,106 @@
<template>
<component
:is="interactive ? 'button' : 'div'"
<div
v-if="!deletedLocal"
data-component-id="AssetCard"
:data-asset-id="asset.id"
v-bind="elementProps"
:class="cardClasses"
@click="interactive && $emit('select', asset)"
@keydown.enter="interactive && $emit('select', asset)"
:aria-labelledby="titleId"
:aria-describedby="descId"
:tabindex="interactive ? 0 : -1"
:class="
cn(
'rounded-2xl overflow-hidden transition-all duration-200 bg-modal-card-background p-2 gap-2 flex flex-col h-full',
interactive &&
'group appearance-none bg-transparent m-0 outline-none text-left hover:bg-secondary-background focus:bg-secondary-background border-none focus:outline-solid outline-base-foreground outline-4'
)
"
@keydown.enter.self="interactive && $emit('select', asset)"
>
<div class="relative aspect-square w-full overflow-hidden rounded-xl">
<img
v-if="shouldShowImage"
:src="asset.preview_url"
class="h-full w-full object-contain"
/>
<div
v-if="isLoading || error"
class="flex size-full cursor-pointer items-center justify-center bg-gradient-to-br from-smoke-400 via-smoke-800 to-charcoal-400"
role="button"
@click.self="interactive && $emit('select', asset)"
/>
<img
v-else
class="flex h-full w-full items-center justify-center bg-gradient-to-br from-smoke-400 via-smoke-800 to-charcoal-400"
></div>
:src="asset.preview_url"
:alt="displayName"
class="size-full object-contain cursor-pointer"
role="button"
@click.self="interactive && $emit('select', asset)"
/>
<AssetBadgeGroup :badges="asset.badges" />
<IconGroup
v-if="flags.assetUpdateOptionsEnabled"
:class="
cn(
'absolute top-2 right-2 invisible group-hover:visible',
dropdownMenuButton?.isOpen && 'visible'
)
"
>
<IconButton v-if="false" size="sm">
<i class="icon-[lucide--file-text]" />
</IconButton>
<MoreButton ref="dropdown-menu-button" size="sm">
<template #default>
<IconTextButton
:label="$t('g.rename')"
type="secondary"
size="md"
@click="startAssetRename"
>
<template #icon>
<i class="icon-[lucide--pencil]" />
</template>
</IconTextButton>
<IconTextButton
:label="$t('g.delete')"
type="secondary"
size="md"
@click="confirmDeletion"
>
<template #icon>
<i class="icon-[lucide--trash-2]" />
</template>
</IconTextButton>
</template>
</MoreButton>
</IconGroup>
</div>
<div :class="cn('p-4 h-32 flex flex-col justify-between')">
<div>
<h3
:id="titleId"
v-tooltip.top="{ value: asset.name, showDelay: tooltipDelay }"
:class="
cn(
'mb-2 m-0 text-base font-semibold line-clamp-2 wrap-anywhere',
'text-base-foreground'
)
"
>
{{ asset.name }}
</h3>
<p
:id="descId"
v-tooltip.top="{ value: asset.description, showDelay: tooltipDelay }"
:class="
cn(
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]',
'text-muted-foreground'
)
"
>
{{ asset.description }}
</p>
</div>
<div :class="cn('flex gap-4 text-xs text-muted-foreground')">
<div class="max-h-32 flex flex-col gap-2 justify-between flex-auto">
<h3
:id="titleId"
v-tooltip.top="{ value: displayName, showDelay: tooltipDelay }"
:class="
cn(
'mb-2 m-0 text-base font-semibold line-clamp-2 wrap-anywhere',
'text-base-foreground'
)
"
>
<EditableText
:model-value="displayName"
:is-editing="isEditing"
:input-attrs="{ 'data-testid': 'asset-name-input' }"
@edit="assetRename"
@cancel="assetRename()"
/>
</h3>
<p
:id="descId"
v-tooltip.top="{ value: asset.description, showDelay: tooltipDelay }"
:class="
cn(
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box] text-muted-foreground'
)
"
>
{{ asset.description }}
</p>
<div class="flex gap-4 text-xs text-muted-foreground mt-auto">
<span v-if="asset.stats.stars" class="flex items-center gap-1">
<i class="icon-[lucide--star] size-3" />
{{ asset.stats.stars }}
@@ -62,73 +115,141 @@
</span>
</div>
</div>
</component>
</div>
</template>
<script setup lang="ts">
import { useImage } from '@vueuse/core'
import { computed, useId } from 'vue'
import { computed, ref, toValue, useId, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconGroup from '@/components/button/IconGroup.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import MoreButton from '@/components/button/MoreButton.vue'
import EditableText from '@/components/common/EditableText.vue'
import { showConfirmDialog } from '@/components/dialog/confirm/confirmDialog'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import { assetService } from '@/platform/assets/services/assetService'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useDialogStore } from '@/stores/dialogStore'
import { cn } from '@/utils/tailwindUtil'
const props = defineProps<{
const { asset, interactive } = defineProps<{
asset: AssetDisplayItem
interactive?: boolean
}>()
defineEmits<{
select: [asset: AssetDisplayItem]
}>()
const { t } = useI18n()
const settingStore = useSettingStore()
const { closeDialog } = useDialogStore()
const { flags } = useFeatureFlags()
const toastStore = useToastStore()
const dropdownMenuButton = useTemplateRef<InstanceType<typeof MoreButton>>(
'dropdown-menu-button'
)
const titleId = useId()
const descId = useId()
const isEditing = ref(false)
const newNameRef = ref<string>()
const deletedLocal = ref(false)
const displayName = computed(() => newNameRef.value ?? asset.name)
const tooltipDelay = computed<number>(() =>
settingStore.get('LiteGraph.Node.TooltipDelay')
)
const { error } = useImage({
src: props.asset.preview_url ?? '',
alt: props.asset.name
const { isLoading, error } = useImage({
src: asset.preview_url ?? '',
alt: asset.name
})
const shouldShowImage = computed(() => props.asset.preview_url && !error.value)
function confirmDeletion() {
dropdownMenuButton.value?.hide()
const assetName = toValue(displayName)
const promptText = ref<string>(t('assetBrowser.deletion.body'))
const optionsDisabled = ref(false)
const confirmDialog = showConfirmDialog({
headerProps: {
title: t('assetBrowser.deletion.header')
},
props: {
promptText
},
footerProps: {
confirmText: t('g.delete'),
// TODO: These need to be put into the new Button Variants once we have them.
confirmClass: cn(
'bg-danger-200 text-base-foreground hover:bg-danger-200/80 focus:bg-danger-200/80 focus:ring ring-base-foreground'
),
optionsDisabled,
onCancel: () => {
closeDialog(confirmDialog)
},
onConfirm: async () => {
optionsDisabled.value = true
try {
promptText.value = t('assetBrowser.deletion.inProgress', {
assetName
})
await assetService.deleteAsset(asset.id)
promptText.value = t('assetBrowser.deletion.complete', {
assetName
})
// Give a second for the completion message
await new Promise((resolve) => setTimeout(resolve, 1_000))
deletedLocal.value = true
} catch (err: unknown) {
console.error(err)
promptText.value = t('assetBrowser.deletion.failed', {
assetName
})
// Give a second for the completion message
await new Promise((resolve) => setTimeout(resolve, 3_000))
} finally {
closeDialog(confirmDialog)
}
}
}
})
}
const cardClasses = computed(() => {
const base = cn(
'rounded-xl overflow-hidden transition-all duration-200 bg-modal-card-background'
)
function startAssetRename() {
dropdownMenuButton.value?.hide()
isEditing.value = true
}
if (!props.interactive) {
return base
async function assetRename(newName?: string) {
isEditing.value = false
if (newName) {
// Optimistic update
newNameRef.value = newName
try {
const result = await assetService.updateAsset(asset.id, {
name: newName
})
// Update with the actual name once the server responds
newNameRef.value = result.name
} catch (err: unknown) {
console.error(err)
toastStore.add({
severity: 'error',
summary: t('assetBrowser.rename.failed'),
life: 10_000
})
newNameRef.value = undefined
}
}
return cn(
base,
'group',
'appearance-none bg-transparent p-0 m-0',
'font-inherit text-inherit outline-none cursor-pointer text-left',
'hover:bg-secondary-background',
'border-none',
'focus:outline-solid outline-azure-600 outline-4'
)
})
const elementProps = computed(() =>
props.interactive
? {
type: 'button',
'aria-labelledby': titleId,
'aria-describedby': descId
}
: {
'aria-labelledby': titleId,
'aria-describedby': descId
}
)
defineEmits<{
select: [asset: AssetDisplayItem]
}>()
}
</script>

View File

@@ -1,7 +1,9 @@
<template>
<div
data-component-id="AssetGrid"
:style="gridStyle"
:class="
cn('grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-4 p-2')
"
role="grid"
:aria-label="$t('assetBrowser.assetCollection')"
:aria-rowcount="-1"
@@ -34,7 +36,6 @@
:key="asset.id"
:asset="asset"
:interactive="true"
role="gridcell"
@select="$emit('assetSelect', $event)"
/>
</template>
@@ -42,11 +43,9 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
import AssetCard from '@/platform/assets/components/AssetCard.vue'
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
import { createGridStyle } from '@/utils/gridUtil'
import { cn } from '@/utils/tailwindUtil'
defineProps<{
assets: AssetDisplayItem[]
@@ -56,7 +55,4 @@ defineProps<{
defineEmits<{
assetSelect: [asset: AssetDisplayItem]
}>()
// Use same grid style as BaseModalLayout
const gridStyle = computed(() => createGridStyle())
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div
class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6 border-t-[1px] border-border-default"
class="upload-model-dialog flex flex-col justify-between gap-6 p-4 pt-6 border-t border-border-default"
>
<!-- Step 1: Enter URL -->
<UploadModelUrlInput

View File

@@ -1,7 +1,10 @@
import { fromZodError } from 'zod-validation-error'
import { st } from '@/i18n'
import { assetResponseSchema } from '@/platform/assets/schemas/assetSchema'
import {
assetItemSchema,
assetResponseSchema
} from '@/platform/assets/schemas/assetSchema'
import type {
AssetItem,
AssetMetadata,
@@ -281,6 +284,43 @@ function createAssetService() {
}
}
/**
* Update metadata of an asset by ID
* Only available in cloud environment
*
* @param id - The asset ID (UUID)
* @param newData - The data to update
* @returns Promise<AssetItem>
* @throws Error if update fails
*/
async function updateAsset(
id: string,
newData: Partial<AssetMetadata>
): Promise<AssetItem> {
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(newData)
})
if (!res.ok) {
throw new Error(
`Unable to update asset ${id}: Server returned ${res.status}`
)
}
const newAsset = assetItemSchema.safeParse(await res.json())
if (newAsset.success) {
return newAsset.data
}
throw new Error(
`Unable to update asset ${id}: Invalid response - ${newAsset.error}`
)
}
/**
* Retrieves metadata from a download URL without downloading the file
*
@@ -360,6 +400,7 @@ function createAssetService() {
getAssetDetails,
getAssetsByTag,
deleteAsset,
updateAsset,
getAssetMetadata,
uploadAssetFromUrl
}

View File

@@ -34,7 +34,7 @@ import NodeConflictDialogContent from '@/workbench/extensions/manager/components
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
import type { ComponentProps } from 'vue-component-type-helpers'
import type { ComponentAttrs } from 'vue-component-type-helpers'
export type ConfirmationDialogType =
| 'default'
@@ -48,7 +48,7 @@ export const useDialogService = () => {
const dialogStore = useDialogStore()
function showLoadWorkflowWarning(
props: ComponentProps<typeof MissingNodesContent>
props: ComponentAttrs<typeof MissingNodesContent>
) {
dialogStore.showDialog({
key: 'global-missing-nodes',
@@ -74,7 +74,7 @@ export const useDialogService = () => {
}
function showMissingModelsWarning(
props: InstanceType<typeof MissingModelsWarning>['$props']
props: ComponentAttrs<typeof MissingModelsWarning>
) {
dialogStore.showDialog({
key: 'global-missing-models-warning',
@@ -115,7 +115,7 @@ export const useDialogService = () => {
}
function showExecutionErrorDialog(executionError: ExecutionErrorWsMessage) {
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
const props: ComponentAttrs<typeof ErrorDialogContent> = {
error: {
exceptionType: executionError.exception_type,
exceptionMessage: executionError.exception_message,
@@ -141,7 +141,7 @@ export const useDialogService = () => {
}
function showManagerDialog(
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
props: ComponentAttrs<typeof ManagerDialogContent> = {}
) {
dialogStore.showDialog({
key: 'global-manager',
@@ -206,7 +206,7 @@ export const useDialogService = () => {
errorMessage: String(error)
}
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
const props: ComponentAttrs<typeof ErrorDialogContent> = {
error: {
exceptionType: options.title ?? 'Unknown Error',
exceptionMessage: errorProps.errorMessage,
@@ -430,7 +430,7 @@ export const useDialogService = () => {
}
function toggleManagerDialog(
props?: InstanceType<typeof ManagerDialogContent>['$props']
props?: ComponentAttrs<typeof ManagerDialogContent>
) {
if (dialogStore.isDialogOpen('global-manager')) {
dialogStore.closeDialog({ key: 'global-manager' })
@@ -440,7 +440,7 @@ export const useDialogService = () => {
}
function toggleManagerProgressDialog(
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
props?: ComponentAttrs<typeof ManagerProgressDialogContent>
) {
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })

View File

@@ -7,6 +7,7 @@ import { markRaw, ref } from 'vue'
import type { Component } from 'vue'
import type GlobalDialog from '@/components/dialog/GlobalDialog.vue'
import type { ComponentAttrs } from 'vue-component-type-helpers'
type DialogPosition =
| 'center'
@@ -33,30 +34,40 @@ interface CustomDialogComponentProps {
headless?: boolean
}
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
export type DialogComponentProps = ComponentAttrs<typeof GlobalDialog> &
CustomDialogComponentProps
interface DialogInstance {
interface DialogInstance<
H extends Component = Component,
B extends Component = Component,
F extends Component = Component
> {
key: string
visible: boolean
title?: string
headerComponent?: Component
component: Component
contentProps: Record<string, any>
footerComponent?: Component
footerProps?: Record<string, any>
headerComponent?: H
headerProps?: ComponentAttrs<H>
component: B
contentProps: ComponentAttrs<B>
footerComponent?: F
footerProps?: ComponentAttrs<F>
dialogComponentProps: DialogComponentProps
priority: number
}
export interface ShowDialogOptions {
export interface ShowDialogOptions<
H extends Component = Component,
B extends Component = Component,
F extends Component = Component
> {
key?: string
title?: string
headerComponent?: Component
footerComponent?: Component
component: Component
props?: Record<string, any>
footerProps?: Record<string, any>
headerComponent?: H
footerComponent?: F
component: B
props?: ComponentAttrs<B>
headerProps?: ComponentAttrs<H>
footerProps?: ComponentAttrs<F>
dialogComponentProps?: DialogComponentProps
/**
* Optional priority for dialog stacking.
@@ -123,17 +134,11 @@ export const useDialogStore = defineStore('dialog', () => {
updateCloseOnEscapeStates()
}
function createDialog(options: {
key: string
title?: string
headerComponent?: Component
footerComponent?: Component
component: Component
props?: Record<string, any>
footerProps?: Record<string, any>
dialogComponentProps?: DialogComponentProps
priority?: number
}) {
function createDialog<
H extends Component = Component,
B extends Component = Component,
F extends Component = Component
>(options: ShowDialogOptions<H, B, F> & { key: string }) {
if (dialogStack.value.length >= 10) {
dialogStack.value.shift()
}
@@ -149,6 +154,7 @@ export const useDialogStore = defineStore('dialog', () => {
? markRaw(options.footerComponent)
: undefined,
component: markRaw(options.component),
headerProps: { ...options.headerProps },
contentProps: { ...options.props },
footerProps: { ...options.footerProps },
priority: options.priority ?? 1,
@@ -203,7 +209,11 @@ export const useDialogStore = defineStore('dialog', () => {
})
}
function showDialog(options: ShowDialogOptions) {
function showDialog<
H extends Component = Component,
B extends Component = Component,
F extends Component = Component
>(options: ShowDialogOptions<H, B, F>) {
const dialogKey = options.key || genDialogKey()
let dialog = dialogStack.value.find((d) => d.key === dialogKey)

View File

@@ -25,8 +25,7 @@ export const getButtonSizeClasses = (size: ButtonSize = 'md') => {
export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
const baseByType = {
primary:
'bg-neutral-900 border-none text-white dark-theme:bg-white dark-theme:text-neutral-900',
primary: 'bg-base-foreground border-none text-base-background',
secondary: cn(
'bg-secondary-background border-none text-base-foreground hover:bg-secondary-background-hover'
),
@@ -42,10 +41,8 @@ export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
const baseByType = {
primary:
'bg-neutral-900 text-white dark-theme:bg-white dark-theme:text-neutral-900',
secondary:
'bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
primary: 'bg-base-background text-base-foreground',
secondary: 'bg-secondary-background text-base-foreground',
transparent: cn(
'bg-transparent text-base-foreground hover:bg-secondary-background-hover'
),
@@ -54,10 +51,9 @@ export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
} as const
const borderByType = {
primary: 'border border-solid border-white dark-theme:border-neutral-900',
secondary: 'border border-solid border-neutral-950 dark-theme:border-white',
transparent:
'border border-solid border-neutral-950 dark-theme:border-white',
primary: 'border border-solid border-base-background',
secondary: 'border border-solid border-base-foreground',
transparent: 'border border-solid border-base-foreground',
accent: 'border border-solid border-primary-background'
} as const

View File

@@ -14,6 +14,8 @@ interface GridOptions {
}
/**
* @deprecated Just use tailwind utilities directly.
* TODO: Create a common grid layout component if needed.
* Creates CSS grid styles for responsive grid layouts
* @param options Grid configuration options
* @returns CSS properties object for grid styling