mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +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>
|
<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>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="relative inline-flex items-center">
|
<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-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
|
||||||
<i v-else class="icon-[lucide--more-vertical] text-sm" />
|
<i v-else class="icon-[lucide--more-vertical] text-sm" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -25,8 +25,18 @@
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@show="$emit('menuOpened')"
|
@show="
|
||||||
@hide="$emit('menuClosed')"
|
() => {
|
||||||
|
isOpen = true
|
||||||
|
$emit('menuOpened')
|
||||||
|
}
|
||||||
|
"
|
||||||
|
@hide="
|
||||||
|
() => {
|
||||||
|
isOpen = false
|
||||||
|
$emit('menuClosed')
|
||||||
|
}
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="flex min-w-40 flex-col gap-2 p-2">
|
<div class="flex min-w-40 flex-col gap-2 p-2">
|
||||||
<slot :close="hide" />
|
<slot :close="hide" />
|
||||||
@@ -48,8 +58,6 @@ interface MoreButtonProps extends BaseButtonProps {
|
|||||||
isVertical?: boolean
|
isVertical?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const popover = ref<InstanceType<typeof Popover>>()
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
size = 'md',
|
size = 'md',
|
||||||
type = 'secondary',
|
type = 'secondary',
|
||||||
@@ -61,15 +69,15 @@ defineEmits<{
|
|||||||
menuClosed: []
|
menuClosed: []
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const toggle = (event: Event) => {
|
const isOpen = ref(false)
|
||||||
popover.value?.toggle(event)
|
const popover = ref<InstanceType<typeof Popover>>()
|
||||||
}
|
|
||||||
|
|
||||||
const hide = () => {
|
function hide() {
|
||||||
popover.value?.hide()
|
popover.value?.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
hide
|
hide,
|
||||||
|
isOpen
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,8 +18,8 @@
|
|||||||
...inputAttrs
|
...inputAttrs
|
||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
@keyup.enter="blurInputElement"
|
@keyup.enter.capture.stop="blurInputElement"
|
||||||
@keyup.escape="cancelEditing"
|
@keyup.escape.stop="cancelEditing"
|
||||||
@click.stop
|
@click.stop
|
||||||
@pointerdown.stop.capture
|
@pointerdown.stop.capture
|
||||||
@pointermove.stop.capture
|
@pointermove.stop.capture
|
||||||
@@ -38,7 +38,7 @@ const {
|
|||||||
} = defineProps<{
|
} = defineProps<{
|
||||||
modelValue: string
|
modelValue: string
|
||||||
isEditing?: boolean
|
isEditing?: boolean
|
||||||
inputAttrs?: Record<string, any>
|
inputAttrs?: Record<string, string>
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])
|
const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
<component
|
<component
|
||||||
:is="item.headerComponent"
|
:is="item.headerComponent"
|
||||||
v-if="item.headerComponent"
|
v-if="item.headerComponent"
|
||||||
|
v-bind="item.headerProps"
|
||||||
:id="item.key"
|
:id="item.key"
|
||||||
/>
|
/>
|
||||||
<h3 v-else :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',
|
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
|
||||||
MAX_UPLOAD_SIZE = 'max_upload_size',
|
MAX_UPLOAD_SIZE = 'max_upload_size',
|
||||||
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
|
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,
|
ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
get assetUpdateOptionsEnabled() {
|
||||||
|
return api.getServerFeature(
|
||||||
|
ServerFeatureFlag.ASSET_UPDATE_OPTIONS_ENABLED,
|
||||||
|
false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export const i18n = createI18n({
|
|||||||
legacy: false,
|
legacy: false,
|
||||||
locale: navigator.language.split('-')[0] || 'en',
|
locale: navigator.language.split('-')[0] || 'en',
|
||||||
fallbackLocale: 'en',
|
fallbackLocale: 'en',
|
||||||
|
escapeParameter: true,
|
||||||
messages,
|
messages,
|
||||||
// Ignore warnings for locale options as each option is in its own language.
|
// Ignore warnings for locale options as each option is in its own language.
|
||||||
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français", "Español"
|
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français", "Español"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{
|
{
|
||||||
"g": {
|
"g": {
|
||||||
"beta": "Beta",
|
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"currentUser": "Current user",
|
"currentUser": "Current user",
|
||||||
"empty": "Empty",
|
"empty": "Empty",
|
||||||
@@ -125,6 +124,7 @@
|
|||||||
"searchKeybindings": "Search Keybindings",
|
"searchKeybindings": "Search Keybindings",
|
||||||
"searchExtensions": "Search Extensions",
|
"searchExtensions": "Search Extensions",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
|
"searchPlaceholder": "Search...",
|
||||||
"noResultsFound": "No Results Found",
|
"noResultsFound": "No Results Found",
|
||||||
"searchFailedMessage": "We couldn't find any settings matching your search. Try adjusting your search terms.",
|
"searchFailedMessage": "We couldn't find any settings matching your search. Try adjusting your search terms.",
|
||||||
"noTasksFound": "No Tasks Found",
|
"noTasksFound": "No Tasks Found",
|
||||||
@@ -2093,7 +2093,6 @@
|
|||||||
"connectionError": "Please check your connection and try again",
|
"connectionError": "Please check your connection and try again",
|
||||||
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
|
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
|
||||||
"noModelsInFolder": "No {type} available in this folder",
|
"noModelsInFolder": "No {type} available in this folder",
|
||||||
"searchAssetsPlaceholder": "Type to search...",
|
|
||||||
"uploadModel": "Import model",
|
"uploadModel": "Import model",
|
||||||
"uploadModelFromCivitai": "Import a model from Civitai",
|
"uploadModelFromCivitai": "Import a model from Civitai",
|
||||||
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
|
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
|
||||||
@@ -2152,6 +2151,16 @@
|
|||||||
"media": {
|
"media": {
|
||||||
"threeDModelPlaceholder": "3D Model",
|
"threeDModelPlaceholder": "3D Model",
|
||||||
"audioPlaceholder": "Audio"
|
"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": {
|
"mediaAsset": {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<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
|
<span
|
||||||
v-for="badge in badges"
|
v-for="badge in badges"
|
||||||
:key="badge.label"
|
:key="badge.label"
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
:autofocus="true"
|
:autofocus="true"
|
||||||
size="lg"
|
size="lg"
|
||||||
:placeholder="$t('assetBrowser.searchAssetsPlaceholder')"
|
:placeholder="$t('g.searchPlaceholder')"
|
||||||
class="max-w-96"
|
class="max-w-96"
|
||||||
/>
|
/>
|
||||||
<IconTextButton
|
<IconTextButton
|
||||||
|
|||||||
@@ -1,53 +1,106 @@
|
|||||||
<template>
|
<template>
|
||||||
<component
|
<div
|
||||||
:is="interactive ? 'button' : 'div'"
|
v-if="!deletedLocal"
|
||||||
data-component-id="AssetCard"
|
data-component-id="AssetCard"
|
||||||
:data-asset-id="asset.id"
|
:data-asset-id="asset.id"
|
||||||
v-bind="elementProps"
|
:aria-labelledby="titleId"
|
||||||
:class="cardClasses"
|
:aria-describedby="descId"
|
||||||
@click="interactive && $emit('select', asset)"
|
:tabindex="interactive ? 0 : -1"
|
||||||
@keydown.enter="interactive && $emit('select', asset)"
|
: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">
|
<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
|
<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
|
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"
|
:src="asset.preview_url"
|
||||||
></div>
|
:alt="displayName"
|
||||||
|
class="size-full object-contain cursor-pointer"
|
||||||
|
role="button"
|
||||||
|
@click.self="interactive && $emit('select', asset)"
|
||||||
|
/>
|
||||||
|
|
||||||
<AssetBadgeGroup :badges="asset.badges" />
|
<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>
|
||||||
<div :class="cn('p-4 h-32 flex flex-col justify-between')">
|
<div class="max-h-32 flex flex-col gap-2 justify-between flex-auto">
|
||||||
<div>
|
<h3
|
||||||
<h3
|
:id="titleId"
|
||||||
:id="titleId"
|
v-tooltip.top="{ value: displayName, showDelay: tooltipDelay }"
|
||||||
v-tooltip.top="{ value: asset.name, showDelay: tooltipDelay }"
|
:class="
|
||||||
:class="
|
cn(
|
||||||
cn(
|
'mb-2 m-0 text-base font-semibold line-clamp-2 wrap-anywhere',
|
||||||
'mb-2 m-0 text-base font-semibold line-clamp-2 wrap-anywhere',
|
'text-base-foreground'
|
||||||
'text-base-foreground'
|
)
|
||||||
)
|
"
|
||||||
"
|
>
|
||||||
>
|
<EditableText
|
||||||
{{ asset.name }}
|
:model-value="displayName"
|
||||||
</h3>
|
:is-editing="isEditing"
|
||||||
<p
|
:input-attrs="{ 'data-testid': 'asset-name-input' }"
|
||||||
:id="descId"
|
@edit="assetRename"
|
||||||
v-tooltip.top="{ value: asset.description, showDelay: tooltipDelay }"
|
@cancel="assetRename()"
|
||||||
:class="
|
/>
|
||||||
cn(
|
</h3>
|
||||||
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box]',
|
<p
|
||||||
'text-muted-foreground'
|
:id="descId"
|
||||||
)
|
v-tooltip.top="{ value: asset.description, showDelay: tooltipDelay }"
|
||||||
"
|
:class="
|
||||||
>
|
cn(
|
||||||
{{ asset.description }}
|
'm-0 text-sm leading-6 overflow-hidden [-webkit-box-orient:vertical] [-webkit-line-clamp:2] [display:-webkit-box] text-muted-foreground'
|
||||||
</p>
|
)
|
||||||
</div>
|
"
|
||||||
<div :class="cn('flex gap-4 text-xs 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">
|
<span v-if="asset.stats.stars" class="flex items-center gap-1">
|
||||||
<i class="icon-[lucide--star] size-3" />
|
<i class="icon-[lucide--star] size-3" />
|
||||||
{{ asset.stats.stars }}
|
{{ asset.stats.stars }}
|
||||||
@@ -62,73 +115,141 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</component>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useImage } from '@vueuse/core'
|
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 AssetBadgeGroup from '@/platform/assets/components/AssetBadgeGroup.vue'
|
||||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||||
|
import { assetService } from '@/platform/assets/services/assetService'
|
||||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||||
|
import { useToastStore } from '@/platform/updates/common/toastStore'
|
||||||
|
import { useDialogStore } from '@/stores/dialogStore'
|
||||||
import { cn } from '@/utils/tailwindUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
const props = defineProps<{
|
const { asset, interactive } = defineProps<{
|
||||||
asset: AssetDisplayItem
|
asset: AssetDisplayItem
|
||||||
interactive?: boolean
|
interactive?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{
|
||||||
|
select: [asset: AssetDisplayItem]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const settingStore = useSettingStore()
|
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 titleId = useId()
|
||||||
const descId = 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>(() =>
|
const tooltipDelay = computed<number>(() =>
|
||||||
settingStore.get('LiteGraph.Node.TooltipDelay')
|
settingStore.get('LiteGraph.Node.TooltipDelay')
|
||||||
)
|
)
|
||||||
|
|
||||||
const { error } = useImage({
|
const { isLoading, error } = useImage({
|
||||||
src: props.asset.preview_url ?? '',
|
src: asset.preview_url ?? '',
|
||||||
alt: props.asset.name
|
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(() => {
|
function startAssetRename() {
|
||||||
const base = cn(
|
dropdownMenuButton.value?.hide()
|
||||||
'rounded-xl overflow-hidden transition-all duration-200 bg-modal-card-background'
|
isEditing.value = true
|
||||||
)
|
}
|
||||||
|
|
||||||
if (!props.interactive) {
|
async function assetRename(newName?: string) {
|
||||||
return base
|
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>
|
</script>
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
data-component-id="AssetGrid"
|
data-component-id="AssetGrid"
|
||||||
:style="gridStyle"
|
:class="
|
||||||
|
cn('grid grid-cols-[repeat(auto-fill,minmax(15rem,1fr))] gap-4 p-2')
|
||||||
|
"
|
||||||
role="grid"
|
role="grid"
|
||||||
:aria-label="$t('assetBrowser.assetCollection')"
|
:aria-label="$t('assetBrowser.assetCollection')"
|
||||||
:aria-rowcount="-1"
|
:aria-rowcount="-1"
|
||||||
@@ -34,7 +36,6 @@
|
|||||||
:key="asset.id"
|
:key="asset.id"
|
||||||
:asset="asset"
|
:asset="asset"
|
||||||
:interactive="true"
|
:interactive="true"
|
||||||
role="gridcell"
|
|
||||||
@select="$emit('assetSelect', $event)"
|
@select="$emit('assetSelect', $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -42,11 +43,9 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
import AssetCard from '@/platform/assets/components/AssetCard.vue'
|
import AssetCard from '@/platform/assets/components/AssetCard.vue'
|
||||||
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
import type { AssetDisplayItem } from '@/platform/assets/composables/useAssetBrowser'
|
||||||
import { createGridStyle } from '@/utils/gridUtil'
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
assets: AssetDisplayItem[]
|
assets: AssetDisplayItem[]
|
||||||
@@ -56,7 +55,4 @@ defineProps<{
|
|||||||
defineEmits<{
|
defineEmits<{
|
||||||
assetSelect: [asset: AssetDisplayItem]
|
assetSelect: [asset: AssetDisplayItem]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// Use same grid style as BaseModalLayout
|
|
||||||
const gridStyle = computed(() => createGridStyle())
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<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 -->
|
<!-- Step 1: Enter URL -->
|
||||||
<UploadModelUrlInput
|
<UploadModelUrlInput
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import { fromZodError } from 'zod-validation-error'
|
import { fromZodError } from 'zod-validation-error'
|
||||||
|
|
||||||
import { st } from '@/i18n'
|
import { st } from '@/i18n'
|
||||||
import { assetResponseSchema } from '@/platform/assets/schemas/assetSchema'
|
import {
|
||||||
|
assetItemSchema,
|
||||||
|
assetResponseSchema
|
||||||
|
} from '@/platform/assets/schemas/assetSchema'
|
||||||
import type {
|
import type {
|
||||||
AssetItem,
|
AssetItem,
|
||||||
AssetMetadata,
|
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
|
* Retrieves metadata from a download URL without downloading the file
|
||||||
*
|
*
|
||||||
@@ -360,6 +400,7 @@ function createAssetService() {
|
|||||||
getAssetDetails,
|
getAssetDetails,
|
||||||
getAssetsByTag,
|
getAssetsByTag,
|
||||||
deleteAsset,
|
deleteAsset,
|
||||||
|
updateAsset,
|
||||||
getAssetMetadata,
|
getAssetMetadata,
|
||||||
uploadAssetFromUrl
|
uploadAssetFromUrl
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import NodeConflictDialogContent from '@/workbench/extensions/manager/components
|
|||||||
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
|
import NodeConflictFooter from '@/workbench/extensions/manager/components/manager/NodeConflictFooter.vue'
|
||||||
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
|
import NodeConflictHeader from '@/workbench/extensions/manager/components/manager/NodeConflictHeader.vue'
|
||||||
import type { ConflictDetectionResult } from '@/workbench/extensions/manager/types/conflictDetectionTypes'
|
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 =
|
export type ConfirmationDialogType =
|
||||||
| 'default'
|
| 'default'
|
||||||
@@ -48,7 +48,7 @@ export const useDialogService = () => {
|
|||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
function showLoadWorkflowWarning(
|
function showLoadWorkflowWarning(
|
||||||
props: ComponentProps<typeof MissingNodesContent>
|
props: ComponentAttrs<typeof MissingNodesContent>
|
||||||
) {
|
) {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-missing-nodes',
|
key: 'global-missing-nodes',
|
||||||
@@ -74,7 +74,7 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showMissingModelsWarning(
|
function showMissingModelsWarning(
|
||||||
props: InstanceType<typeof MissingModelsWarning>['$props']
|
props: ComponentAttrs<typeof MissingModelsWarning>
|
||||||
) {
|
) {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-missing-models-warning',
|
key: 'global-missing-models-warning',
|
||||||
@@ -115,7 +115,7 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showExecutionErrorDialog(executionError: ExecutionErrorWsMessage) {
|
function showExecutionErrorDialog(executionError: ExecutionErrorWsMessage) {
|
||||||
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
|
const props: ComponentAttrs<typeof ErrorDialogContent> = {
|
||||||
error: {
|
error: {
|
||||||
exceptionType: executionError.exception_type,
|
exceptionType: executionError.exception_type,
|
||||||
exceptionMessage: executionError.exception_message,
|
exceptionMessage: executionError.exception_message,
|
||||||
@@ -141,7 +141,7 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showManagerDialog(
|
function showManagerDialog(
|
||||||
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
|
props: ComponentAttrs<typeof ManagerDialogContent> = {}
|
||||||
) {
|
) {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-manager',
|
key: 'global-manager',
|
||||||
@@ -206,7 +206,7 @@ export const useDialogService = () => {
|
|||||||
errorMessage: String(error)
|
errorMessage: String(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
const props: InstanceType<typeof ErrorDialogContent>['$props'] = {
|
const props: ComponentAttrs<typeof ErrorDialogContent> = {
|
||||||
error: {
|
error: {
|
||||||
exceptionType: options.title ?? 'Unknown Error',
|
exceptionType: options.title ?? 'Unknown Error',
|
||||||
exceptionMessage: errorProps.errorMessage,
|
exceptionMessage: errorProps.errorMessage,
|
||||||
@@ -430,7 +430,7 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleManagerDialog(
|
function toggleManagerDialog(
|
||||||
props?: InstanceType<typeof ManagerDialogContent>['$props']
|
props?: ComponentAttrs<typeof ManagerDialogContent>
|
||||||
) {
|
) {
|
||||||
if (dialogStore.isDialogOpen('global-manager')) {
|
if (dialogStore.isDialogOpen('global-manager')) {
|
||||||
dialogStore.closeDialog({ key: 'global-manager' })
|
dialogStore.closeDialog({ key: 'global-manager' })
|
||||||
@@ -440,7 +440,7 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleManagerProgressDialog(
|
function toggleManagerProgressDialog(
|
||||||
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
|
props?: ComponentAttrs<typeof ManagerProgressDialogContent>
|
||||||
) {
|
) {
|
||||||
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
|
if (dialogStore.isDialogOpen('global-manager-progress-dialog')) {
|
||||||
dialogStore.closeDialog({ key: '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 { Component } from 'vue'
|
||||||
|
|
||||||
import type GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
import type GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||||
|
import type { ComponentAttrs } from 'vue-component-type-helpers'
|
||||||
|
|
||||||
type DialogPosition =
|
type DialogPosition =
|
||||||
| 'center'
|
| 'center'
|
||||||
@@ -33,30 +34,40 @@ interface CustomDialogComponentProps {
|
|||||||
headless?: boolean
|
headless?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DialogComponentProps = InstanceType<typeof GlobalDialog>['$props'] &
|
export type DialogComponentProps = ComponentAttrs<typeof GlobalDialog> &
|
||||||
CustomDialogComponentProps
|
CustomDialogComponentProps
|
||||||
|
|
||||||
interface DialogInstance {
|
interface DialogInstance<
|
||||||
|
H extends Component = Component,
|
||||||
|
B extends Component = Component,
|
||||||
|
F extends Component = Component
|
||||||
|
> {
|
||||||
key: string
|
key: string
|
||||||
visible: boolean
|
visible: boolean
|
||||||
title?: string
|
title?: string
|
||||||
headerComponent?: Component
|
headerComponent?: H
|
||||||
component: Component
|
headerProps?: ComponentAttrs<H>
|
||||||
contentProps: Record<string, any>
|
component: B
|
||||||
footerComponent?: Component
|
contentProps: ComponentAttrs<B>
|
||||||
footerProps?: Record<string, any>
|
footerComponent?: F
|
||||||
|
footerProps?: ComponentAttrs<F>
|
||||||
dialogComponentProps: DialogComponentProps
|
dialogComponentProps: DialogComponentProps
|
||||||
priority: number
|
priority: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShowDialogOptions {
|
export interface ShowDialogOptions<
|
||||||
|
H extends Component = Component,
|
||||||
|
B extends Component = Component,
|
||||||
|
F extends Component = Component
|
||||||
|
> {
|
||||||
key?: string
|
key?: string
|
||||||
title?: string
|
title?: string
|
||||||
headerComponent?: Component
|
headerComponent?: H
|
||||||
footerComponent?: Component
|
footerComponent?: F
|
||||||
component: Component
|
component: B
|
||||||
props?: Record<string, any>
|
props?: ComponentAttrs<B>
|
||||||
footerProps?: Record<string, any>
|
headerProps?: ComponentAttrs<H>
|
||||||
|
footerProps?: ComponentAttrs<F>
|
||||||
dialogComponentProps?: DialogComponentProps
|
dialogComponentProps?: DialogComponentProps
|
||||||
/**
|
/**
|
||||||
* Optional priority for dialog stacking.
|
* Optional priority for dialog stacking.
|
||||||
@@ -123,17 +134,11 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
updateCloseOnEscapeStates()
|
updateCloseOnEscapeStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDialog(options: {
|
function createDialog<
|
||||||
key: string
|
H extends Component = Component,
|
||||||
title?: string
|
B extends Component = Component,
|
||||||
headerComponent?: Component
|
F extends Component = Component
|
||||||
footerComponent?: Component
|
>(options: ShowDialogOptions<H, B, F> & { key: string }) {
|
||||||
component: Component
|
|
||||||
props?: Record<string, any>
|
|
||||||
footerProps?: Record<string, any>
|
|
||||||
dialogComponentProps?: DialogComponentProps
|
|
||||||
priority?: number
|
|
||||||
}) {
|
|
||||||
if (dialogStack.value.length >= 10) {
|
if (dialogStack.value.length >= 10) {
|
||||||
dialogStack.value.shift()
|
dialogStack.value.shift()
|
||||||
}
|
}
|
||||||
@@ -149,6 +154,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
? markRaw(options.footerComponent)
|
? markRaw(options.footerComponent)
|
||||||
: undefined,
|
: undefined,
|
||||||
component: markRaw(options.component),
|
component: markRaw(options.component),
|
||||||
|
headerProps: { ...options.headerProps },
|
||||||
contentProps: { ...options.props },
|
contentProps: { ...options.props },
|
||||||
footerProps: { ...options.footerProps },
|
footerProps: { ...options.footerProps },
|
||||||
priority: options.priority ?? 1,
|
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()
|
const dialogKey = options.key || genDialogKey()
|
||||||
|
|
||||||
let dialog = dialogStack.value.find((d) => d.key === dialogKey)
|
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') => {
|
export const getButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||||
const baseByType = {
|
const baseByType = {
|
||||||
primary:
|
primary: 'bg-base-foreground border-none text-base-background',
|
||||||
'bg-neutral-900 border-none text-white dark-theme:bg-white dark-theme:text-neutral-900',
|
|
||||||
secondary: cn(
|
secondary: cn(
|
||||||
'bg-secondary-background border-none text-base-foreground hover:bg-secondary-background-hover'
|
'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') => {
|
export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
|
||||||
const baseByType = {
|
const baseByType = {
|
||||||
primary:
|
primary: 'bg-base-background text-base-foreground',
|
||||||
'bg-neutral-900 text-white dark-theme:bg-white dark-theme:text-neutral-900',
|
secondary: 'bg-secondary-background text-base-foreground',
|
||||||
secondary:
|
|
||||||
'bg-white text-neutral-950 dark-theme:bg-zinc-700 dark-theme:text-white',
|
|
||||||
transparent: cn(
|
transparent: cn(
|
||||||
'bg-transparent text-base-foreground hover:bg-secondary-background-hover'
|
'bg-transparent text-base-foreground hover:bg-secondary-background-hover'
|
||||||
),
|
),
|
||||||
@@ -54,10 +51,9 @@ export const getBorderButtonTypeClasses = (type: ButtonType = 'primary') => {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
const borderByType = {
|
const borderByType = {
|
||||||
primary: 'border border-solid border-white dark-theme:border-neutral-900',
|
primary: 'border border-solid border-base-background',
|
||||||
secondary: 'border border-solid border-neutral-950 dark-theme:border-white',
|
secondary: 'border border-solid border-base-foreground',
|
||||||
transparent:
|
transparent: 'border border-solid border-base-foreground',
|
||||||
'border border-solid border-neutral-950 dark-theme:border-white',
|
|
||||||
accent: 'border border-solid border-primary-background'
|
accent: 'border border-solid border-primary-background'
|
||||||
} as const
|
} 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
|
* Creates CSS grid styles for responsive grid layouts
|
||||||
* @param options Grid configuration options
|
* @param options Grid configuration options
|
||||||
* @returns CSS properties object for grid styling
|
* @returns CSS properties object for grid styling
|
||||||
|
|||||||
Reference in New Issue
Block a user