mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 19:09:52 +00:00
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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'])
|
||||
|
||||
@@ -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">
|
||||
|
||||
19
src/components/dialog/confirm/ConfirmBody.vue
Normal file
19
src/components/dialog/confirm/ConfirmBody.vue
Normal 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>
|
||||
43
src/components/dialog/confirm/ConfirmFooter.vue
Normal file
43
src/components/dialog/confirm/ConfirmFooter.vue
Normal 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>
|
||||
12
src/components/dialog/confirm/ConfirmHeader.vue
Normal file
12
src/components/dialog/confirm/ConfirmHeader.vue
Normal 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>
|
||||
31
src/components/dialog/confirm/confirmDialog.ts
Normal file
31
src/components/dialog/confirm/confirmDialog.ts
Normal 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!'
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
v-model="searchQuery"
|
||||
:autofocus="true"
|
||||
size="lg"
|
||||
:placeholder="$t('assetBrowser.searchAssetsPlaceholder')"
|
||||
:placeholder="$t('g.searchPlaceholder')"
|
||||
class="max-w-96"
|
||||
/>
|
||||
<IconTextButton
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user