mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-04 05:02:17 +00:00
Compare commits
23 Commits
glary/test
...
coderabbit
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a71450b85 | ||
|
|
6031e6ccf2 | ||
|
|
4417b0d907 | ||
|
|
ff988cd6f1 | ||
|
|
38cc93b8f6 | ||
|
|
23d8ccd394 | ||
|
|
23d01f0b34 | ||
|
|
64a7955eca | ||
|
|
a5bd37ef56 | ||
|
|
ca539a6fd0 | ||
|
|
585e47f2df | ||
|
|
aa13e4c385 | ||
|
|
57ed0afde6 | ||
|
|
3a18d27b9d | ||
|
|
f9e8dfdbe7 | ||
|
|
ed68b085cc | ||
|
|
bb9475fed6 | ||
|
|
ba43494d32 | ||
|
|
df82698f1e | ||
|
|
d4b993b16d | ||
|
|
04ab767649 | ||
|
|
0b7d0f1d35 | ||
|
|
0dfe36f1f8 |
@@ -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>
|
||||||
39
src/components/dialog/confirm/confirmDialog.ts
Normal file
39
src/components/dialog/confirm/confirmDialog.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
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>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open a confirmation dialog composed of the standard confirm header, body, and footer.
|
||||||
|
*
|
||||||
|
* Forwards any provided `headerProps`, `props`, and `footerProps` to the corresponding components.
|
||||||
|
*
|
||||||
|
* @param options - Optional configuration with `headerProps`, `props`, and `footerProps` to customize the header, body, and footer components
|
||||||
|
* @returns A dialog handle representing the shown confirmation dialog
|
||||||
|
*/
|
||||||
|
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,11 +9,20 @@ 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'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Composable for reactive access to server-side feature flags
|
* Provides reactive accessors for server-side feature flags.
|
||||||
|
*
|
||||||
|
* Exposes a readonly `flags` object containing convenience getters for known server feature keys
|
||||||
|
* and a `featureFlag` helper that returns a computed value for an arbitrary feature path,
|
||||||
|
* optionally using a supplied default when the feature is not present.
|
||||||
|
*
|
||||||
|
* @returns An object with:
|
||||||
|
* - `flags`: a readonly reactive object with predefined getters for common server feature flags
|
||||||
|
* - `featureFlag`: a generic function `(featurePath: string, defaultValue?) => ComputedRef<T>` that yields a computed feature value
|
||||||
*/
|
*/
|
||||||
export function useFeatureFlags() {
|
export function useFeatureFlags() {
|
||||||
const flags = reactive({
|
const flags = reactive({
|
||||||
@@ -31,6 +40,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,13 @@
|
|||||||
"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."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,133 @@
|
|||||||
</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 { 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 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 () => {
|
||||||
|
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)
|
||||||
|
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,
|
||||||
@@ -262,12 +265,10 @@ function createAssetService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes an asset by ID
|
* Delete the asset identified by `id` (cloud environments only).
|
||||||
* Only available in cloud environment
|
|
||||||
*
|
*
|
||||||
* @param id - The asset ID (UUID)
|
* @param id - The asset ID (UUID)
|
||||||
* @returns Promise<void>
|
* @throws Error if the server responds with a non-ok status; message includes the HTTP status
|
||||||
* @throws Error if deletion fails
|
|
||||||
*/
|
*/
|
||||||
async function deleteAsset(id: string): Promise<void> {
|
async function deleteAsset(id: string): Promise<void> {
|
||||||
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
|
const res = await api.fetchApi(`${ASSETS_ENDPOINT}/${id}`, {
|
||||||
@@ -281,6 +282,44 @@ function createAssetService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an asset's metadata by its ID.
|
||||||
|
*
|
||||||
|
* Only available in cloud environment.
|
||||||
|
*
|
||||||
|
* @param id - The asset ID (UUID)
|
||||||
|
* @param newData - Partial metadata fields to apply to the asset
|
||||||
|
* @returns The updated AssetItem
|
||||||
|
* @throws Error if the server responds with a non-OK status or the response cannot be validated as an AssetItem
|
||||||
|
*/
|
||||||
|
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 +399,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'
|
||||||
@@ -47,8 +47,13 @@ export type ConfirmationDialogType =
|
|||||||
export const useDialogService = () => {
|
export const useDialogService = () => {
|
||||||
const dialogStore = useDialogStore()
|
const dialogStore = useDialogStore()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open the global missing-nodes dialog and forward the provided props to its content component.
|
||||||
|
*
|
||||||
|
* @param props - Props passed through to the MissingNodesContent component
|
||||||
|
*/
|
||||||
function showLoadWorkflowWarning(
|
function showLoadWorkflowWarning(
|
||||||
props: ComponentProps<typeof MissingNodesContent>
|
props: ComponentAttrs<typeof MissingNodesContent>
|
||||||
) {
|
) {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-missing-nodes',
|
key: 'global-missing-nodes',
|
||||||
@@ -73,8 +78,13 @@ export const useDialogService = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the global missing-models warning dialog.
|
||||||
|
*
|
||||||
|
* @param props - Props forwarded to the MissingModelsWarning component
|
||||||
|
*/
|
||||||
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',
|
||||||
@@ -103,6 +113,11 @@ export const useDialogService = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the global settings dialog with the About panel selected.
|
||||||
|
*
|
||||||
|
* Displays the settings dialog and sets its default inner panel to "about".
|
||||||
|
*/
|
||||||
function showAboutDialog() {
|
function showAboutDialog() {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-settings',
|
key: 'global-settings',
|
||||||
@@ -114,8 +129,15 @@ export const useDialogService = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the global execution error dialog populated from a websocket execution error message.
|
||||||
|
*
|
||||||
|
* Displays a dialog containing the error details from `executionError` and records a telemetry event when the dialog is closed.
|
||||||
|
*
|
||||||
|
* @param executionError - Websocket execution error message containing `exception_type`, `exception_message`, `node_id`, `node_type`, and `traceback`
|
||||||
|
*/
|
||||||
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,
|
||||||
@@ -140,8 +162,13 @@ export const useDialogService = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the global manager dialog using the default manager layout and styling and forwards props to the dialog content.
|
||||||
|
*
|
||||||
|
* @param props - Props to pass through to ManagerDialogContent (defaults to an empty object)
|
||||||
|
*/
|
||||||
function showManagerDialog(
|
function showManagerDialog(
|
||||||
props: InstanceType<typeof ManagerDialogContent>['$props'] = {}
|
props: ComponentAttrs<typeof ManagerDialogContent> = {}
|
||||||
) {
|
) {
|
||||||
dialogStore.showDialog({
|
dialogStore.showDialog({
|
||||||
key: 'global-manager',
|
key: 'global-manager',
|
||||||
@@ -184,9 +211,12 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a error dialog to the user when an error occurs.
|
* Displays a global error dialog for the given error and tracks the dialog close event for telemetry.
|
||||||
* @param error The error to show
|
*
|
||||||
* @param options The options for the dialog
|
* @param error - An Error or any value to display; if an Error is provided it will be parsed for message, stack trace, and extension file.
|
||||||
|
* @param options - Optional configuration for the dialog
|
||||||
|
* @param options.title - Title used as the exception type shown in the dialog
|
||||||
|
* @param options.reportType - Optional report type forwarded to the dialog for reporting purposes
|
||||||
*/
|
*/
|
||||||
function showErrorDialog(
|
function showErrorDialog(
|
||||||
error: unknown,
|
error: unknown,
|
||||||
@@ -206,7 +236,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,
|
||||||
@@ -412,15 +442,10 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a dialog from a third party extension.
|
* Show a dialog provided by a third-party extension.
|
||||||
* @param options - The dialog options.
|
*
|
||||||
* @param options.key - The dialog key.
|
* @param options - Dialog configuration including `key`, optional `title`, header/footer components, dialog `component`, and `props` passed to the component.
|
||||||
* @param options.title - The dialog title.
|
* @returns An object with `dialog`, the dialog instance returned by the dialog store, and `closeDialog`, a function that closes the dialog using the provided `key`.
|
||||||
* @param options.headerComponent - The dialog header component.
|
|
||||||
* @param options.footerComponent - The dialog footer component.
|
|
||||||
* @param options.component - The dialog component.
|
|
||||||
* @param options.props - The dialog props.
|
|
||||||
* @returns The dialog instance and a function to close the dialog.
|
|
||||||
*/
|
*/
|
||||||
function showExtensionDialog(options: ShowDialogOptions & { key: string }) {
|
function showExtensionDialog(options: ShowDialogOptions & { key: string }) {
|
||||||
return {
|
return {
|
||||||
@@ -429,8 +454,15 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the global manager dialog's visibility.
|
||||||
|
*
|
||||||
|
* If the global manager dialog is open, it will be closed; otherwise it will be shown.
|
||||||
|
*
|
||||||
|
* @param props - Optional props to pass to the ManagerDialogContent when opening the dialog
|
||||||
|
*/
|
||||||
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' })
|
||||||
@@ -439,8 +471,13 @@ export const useDialogService = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the global manager progress dialog: closes it if open, otherwise opens it.
|
||||||
|
*
|
||||||
|
* @param props - Optional props to pass to the ManagerProgressDialogContent when opening the dialog
|
||||||
|
*/
|
||||||
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.
|
||||||
@@ -105,6 +116,13 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the dialog identified by the given key or the currently active dialog when no key is provided.
|
||||||
|
*
|
||||||
|
* Invokes the dialog's `onClose` callback if present, removes the dialog from the stack, updates the active dialog key, and adjusts close-on-Escape handling. If no matching dialog is found this function is a no-op.
|
||||||
|
*
|
||||||
|
* @param options - Optional object with a `key` specifying which dialog to close; when omitted the active dialog is closed.
|
||||||
|
*/
|
||||||
function closeDialog(options?: { key: string }) {
|
function closeDialog(options?: { key: string }) {
|
||||||
const targetDialog = options
|
const targetDialog = options
|
||||||
? dialogStack.value.find((d) => d.key === options.key)
|
? dialogStack.value.find((d) => d.key === options.key)
|
||||||
@@ -123,17 +141,19 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
updateCloseOnEscapeStates()
|
updateCloseOnEscapeStates()
|
||||||
}
|
}
|
||||||
|
|
||||||
function createDialog(options: {
|
/**
|
||||||
key: string
|
* Create and register a dialog instance from the given options and push it into the dialog stack.
|
||||||
title?: string
|
*
|
||||||
headerComponent?: Component
|
* @param options - Configuration for the dialog. Must include a unique `key`. Other fields configure the component to render (`component`), optional `title`, optional `headerComponent`/`footerComponent` and their props, additional `props` for the content component, `dialogComponentProps` for dialog behavior, and an optional numeric `priority`.
|
||||||
footerComponent?: Component
|
* @returns The created dialog instance that was inserted into the store's stack.
|
||||||
component: Component
|
*
|
||||||
props?: Record<string, any>
|
* Side effects: enforces a maximum stack size of 10 by removing the oldest dialog when necessary, inserts the new dialog according to its priority, sets the dialog as the active one, and updates close-on-escape handling for the stack.
|
||||||
footerProps?: Record<string, any>
|
*/
|
||||||
dialogComponentProps?: DialogComponentProps
|
function createDialog<
|
||||||
priority?: number
|
H extends Component = Component,
|
||||||
}) {
|
B extends Component = Component,
|
||||||
|
F extends Component = Component
|
||||||
|
>(options: ShowDialogOptions<H, B, F> & { key: string }) {
|
||||||
if (dialogStack.value.length >= 10) {
|
if (dialogStack.value.length >= 10) {
|
||||||
dialogStack.value.shift()
|
dialogStack.value.shift()
|
||||||
}
|
}
|
||||||
@@ -149,6 +169,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 +224,17 @@ export const useDialogStore = defineStore('dialog', () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDialog(options: ShowDialogOptions) {
|
/**
|
||||||
|
* Opens the dialog described by `options` and ensures it is the active (top-most) dialog, creating a new dialog if one with the same key does not exist.
|
||||||
|
*
|
||||||
|
* @param options - Configuration for the dialog to show; may include a `key` to target an existing dialog or omit it to generate a new key
|
||||||
|
* @returns The dialog instance that was shown or created
|
||||||
|
*/
|
||||||
|
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