refactor(ui): 统一全局确认框交互
This commit is contained in:
17 files changed
+275
-302
No files matched your search
@@ -0,0 +1,66 @@
|
||||
<script setup lang="ts">
|
||||
import AppButton from './AppButton.vue'
|
||||
import AppDialog from './AppDialog.vue'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
open: boolean
|
||||
title: string
|
||||
description: string
|
||||
closeLabel: string
|
||||
cancelLabel: string
|
||||
confirmLabel: string
|
||||
tone?: 'default' | 'danger'
|
||||
pending?: boolean
|
||||
confirmDisabled?: boolean
|
||||
dismissible?: boolean
|
||||
preventCloseAutoFocus?: boolean
|
||||
}>(),
|
||||
{
|
||||
tone: 'default',
|
||||
pending: false,
|
||||
confirmDisabled: false,
|
||||
dismissible: true,
|
||||
preventCloseAutoFocus: false,
|
||||
},
|
||||
)
|
||||
const emit = defineEmits<{
|
||||
'update:open': [open: boolean]
|
||||
confirm: []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog
|
||||
:open="open"
|
||||
:title="title"
|
||||
:description="description"
|
||||
:close-label="closeLabel"
|
||||
:dismissible="dismissible && !pending"
|
||||
:prevent-close-auto-focus="preventCloseAutoFocus"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<template v-if="$slots.trigger" #trigger><slot name="trigger" /></template>
|
||||
<template v-if="$slots.default" #body><slot /></template>
|
||||
<template #footer>
|
||||
<AppButton
|
||||
variant="secondary"
|
||||
size="compact"
|
||||
:disabled="pending"
|
||||
@click="emit('update:open', false)"
|
||||
>
|
||||
{{ cancelLabel }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
:variant="tone === 'danger' ? 'danger' : 'primary'"
|
||||
size="compact"
|
||||
:busy="pending"
|
||||
:disabled="confirmDisabled"
|
||||
@click="emit('confirm')"
|
||||
>
|
||||
<slot name="confirm-icon" />
|
||||
{{ confirmLabel }}
|
||||
</AppButton>
|
||||
</template>
|
||||
</AppDialog>
|
||||
</template>
|
||||
@@ -52,7 +52,8 @@ function guardDismiss(event: Event): void {
|
||||
</DialogClose>
|
||||
</header>
|
||||
<DialogDescription class="app-dialog__description">{{ description }}</DialogDescription>
|
||||
<div class="app-dialog__body"><slot /></div>
|
||||
<div v-if="$slots.body" class="app-dialog__body"><slot name="body" /></div>
|
||||
<footer v-if="$slots.footer" class="app-dialog__footer"><slot name="footer" /></footer>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</DialogRoot>
|
||||
@@ -71,7 +72,7 @@ function guardDismiss(event: Event): void {
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: grid;
|
||||
width: min(calc(100vw - 32px), 460px);
|
||||
width: min(calc(100vw - 32px), 420px);
|
||||
max-height: calc(100vh - 32px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
transform: translate(-50%, -50%);
|
||||
@@ -81,23 +82,26 @@ function guardDismiss(event: Event): void {
|
||||
background: var(--color-surface);
|
||||
box-shadow: var(--shadow-overlay);
|
||||
color: var(--color-text);
|
||||
padding: var(--space-5);
|
||||
padding: 0;
|
||||
}
|
||||
.app-dialog__header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
min-height: 58px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
padding: 10px 16px 10px 18px;
|
||||
}
|
||||
.app-dialog__title {
|
||||
font-size: 1.0625rem;
|
||||
font-size: var(--text-meta);
|
||||
font-weight: 700;
|
||||
}
|
||||
.app-dialog__close {
|
||||
display: inline-flex;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
flex: 0 0 44px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
flex: 0 0 36px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
@@ -111,13 +115,25 @@ function guardDismiss(event: Event): void {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.app-dialog__description {
|
||||
margin: var(--space-2) 0 0;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0;
|
||||
padding: 12px 18px 0;
|
||||
color: var(--color-text-faint);
|
||||
font-size: var(--text-sm);
|
||||
line-height: var(--line-normal);
|
||||
}
|
||||
.app-dialog__body {
|
||||
min-height: 0;
|
||||
margin-top: var(--space-5);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 14px 18px 16px;
|
||||
}
|
||||
.app-dialog__footer {
|
||||
display: flex;
|
||||
min-height: 58px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
border-top: 1px solid var(--color-border-subtle);
|
||||
padding: 10px 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -154,20 +154,36 @@ function guardDismiss(event: Event): void {
|
||||
padding: 10px 18px;
|
||||
}
|
||||
@keyframes app-drawer-overlay-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes app-drawer-overlay-out {
|
||||
from { opacity: 1; }
|
||||
to { opacity: 0; }
|
||||
from {
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
@keyframes app-drawer-content-in {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes app-drawer-content-out {
|
||||
from { transform: translateX(0); }
|
||||
to { transform: translateX(100%); }
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.app-drawer__content {
|
||||
|
||||
@@ -10,12 +10,7 @@ const current = toast.current
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="app-toast" mode="out-in">
|
||||
<InlineFeedback
|
||||
v-if="current"
|
||||
:key="current.id"
|
||||
:tone="current.tone"
|
||||
appearance="toast"
|
||||
>
|
||||
<InlineFeedback v-if="current" :key="current.id" :tone="current.tone" appearance="toast">
|
||||
{{ current.message }}
|
||||
</InlineFeedback>
|
||||
</Transition>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
id: string
|
||||
label: string
|
||||
modelValue: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
|
||||
const input = ref<HTMLInputElement>()
|
||||
|
||||
function focus(): void {
|
||||
input.value?.focus()
|
||||
}
|
||||
|
||||
defineExpose({ focus })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="app-typed-confirmation" :for="id">
|
||||
<span>{{ label }}</span>
|
||||
<input
|
||||
:id="id"
|
||||
ref="input"
|
||||
:value="modelValue"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:disabled="disabled"
|
||||
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-typed-confirmation {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
.app-typed-confirmation input {
|
||||
width: 100%;
|
||||
min-height: var(--control-md);
|
||||
border: 1px solid var(--color-border-control);
|
||||
border-radius: var(--radius-control);
|
||||
outline: 0;
|
||||
background: var(--color-surface-sunken);
|
||||
color: var(--color-text);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font: inherit;
|
||||
font-weight: 400;
|
||||
}
|
||||
.app-typed-confirmation input:focus {
|
||||
border-color: var(--color-action);
|
||||
box-shadow: var(--focus-ring);
|
||||
}
|
||||
.app-typed-confirmation input:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
</style>
|
||||
@@ -242,7 +242,6 @@ onBeforeUnmount(() => {
|
||||
overflow-wrap: anywhere;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
|
||||
@@ -8,7 +8,8 @@ import { deleteAccessKey } from '@/app/resources/access-keys'
|
||||
import type { AccessKeyDto } from '@/api/control/types'
|
||||
import { RequestCancelledError } from '@/api/errors'
|
||||
import AppButton from '@/components/ui/AppButton.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppConfirmDialog from '@/components/ui/AppConfirmDialog.vue'
|
||||
import AppTypedConfirmation from '@/components/ui/AppTypedConfirmation.vue'
|
||||
import InlineFeedback from '@/components/ui/InlineFeedback.vue'
|
||||
|
||||
const props = defineProps<{ accessKey: AccessKeyDto; total: number }>()
|
||||
@@ -17,7 +18,7 @@ const client = useApiClient()
|
||||
const { t } = useI18n()
|
||||
const open = ref(false)
|
||||
const typedName = ref('')
|
||||
const nameInput = ref<HTMLInputElement>()
|
||||
const nameInput = ref<InstanceType<typeof AppTypedConfirmation>>()
|
||||
const pending = ref(false)
|
||||
const failed = ref(false)
|
||||
let controller: AbortController | undefined
|
||||
@@ -65,13 +66,18 @@ onBeforeUnmount(() => controller?.abort())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog
|
||||
<AppConfirmDialog
|
||||
:open="open"
|
||||
:title="t('accessKeys.delete.title')"
|
||||
:description="t('accessKeys.delete.description', { name: accessKey.name })"
|
||||
:close-label="t('accessKeys.delete.close')"
|
||||
:dismissible="!pending"
|
||||
:cancel-label="t('common.cancel')"
|
||||
:confirm-label="t('accessKeys.delete.confirm')"
|
||||
tone="danger"
|
||||
:pending="pending"
|
||||
:confirm-disabled="!confirmed"
|
||||
@update:open="setOpen"
|
||||
@confirm="confirmDelete"
|
||||
>
|
||||
<template #trigger>
|
||||
<AppButton variant="danger" size="compact" @click="setOpen(true)">
|
||||
@@ -81,98 +87,53 @@ onBeforeUnmount(() => controller?.abort())
|
||||
</template>
|
||||
|
||||
<div class="access-key-delete__body">
|
||||
<InlineFeedback v-if="total === 1" tone="warning">
|
||||
<div class="access-key-delete__summary">
|
||||
<strong>{{ accessKey.name }}</strong>
|
||||
<code>{{ accessKey.masked_key }}</code>
|
||||
</div>
|
||||
<InlineFeedback v-if="total === 1" tone="warning" appearance="hint">
|
||||
{{ t('accessKeys.delete.lastWarning') }}
|
||||
</InlineFeedback>
|
||||
<dl class="access-key-delete__details">
|
||||
<dt>{{ t('accessKeys.delete.name') }}</dt>
|
||||
<dd>{{ accessKey.name }}</dd>
|
||||
<dt>{{ t('accessKeys.delete.key') }}</dt>
|
||||
<dd>
|
||||
<code>{{ accessKey.masked_key }}</code>
|
||||
</dd>
|
||||
</dl>
|
||||
<InlineFeedback tone="warning">
|
||||
<InlineFeedback tone="warning" appearance="hint">
|
||||
{{ t('accessKeys.delete.impact') }}
|
||||
</InlineFeedback>
|
||||
<label class="access-key-delete__label" for="access-key-delete-name">{{
|
||||
t('accessKeys.delete.typeName', { name: accessKey.name })
|
||||
}}</label>
|
||||
<input
|
||||
<AppTypedConfirmation
|
||||
id="access-key-delete-name"
|
||||
ref="nameInput"
|
||||
v-model="typedName"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:label="t('accessKeys.delete.typeName', { name: accessKey.name })"
|
||||
:disabled="pending"
|
||||
/>
|
||||
<InlineFeedback v-if="failed" tone="danger">{{
|
||||
t('accessKeys.delete.failed')
|
||||
}}</InlineFeedback>
|
||||
<div class="access-key-delete__actions">
|
||||
<AppButton variant="secondary" :disabled="pending" @click="setOpen(false)">
|
||||
{{ t('common.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
class="access-key-delete__confirm"
|
||||
variant="danger"
|
||||
:busy="pending"
|
||||
:disabled="!confirmed"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ t('accessKeys.delete.confirm') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</AppConfirmDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.access-key-delete__body {
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
gap: 12px;
|
||||
}
|
||||
.access-key-delete__label {
|
||||
margin-bottom: calc(var(--space-3) * -1);
|
||||
font-weight: 650;
|
||||
}
|
||||
.access-key-delete__details {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
gap: var(--space-2) var(--space-3);
|
||||
margin: 0;
|
||||
padding: var(--space-3);
|
||||
.access-key-delete__summary {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-surface-sunken);
|
||||
padding: 9px 10px;
|
||||
}
|
||||
.access-key-delete__details dt {
|
||||
color: var(--color-text-muted);
|
||||
.access-key-delete__summary strong {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
.access-key-delete__details dd {
|
||||
.access-key-delete__summary code {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
color: var(--color-code);
|
||||
font-size: var(--text-label-xs);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.access-key-delete__details code {
|
||||
color: var(--color-code);
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
min-height: var(--touch-target);
|
||||
border: 1px solid var(--color-border-control);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-surface-sunken);
|
||||
color: var(--color-text);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font: inherit;
|
||||
}
|
||||
.access-key-delete__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
@@ -708,11 +708,7 @@ onBeforeUnmount(clearLocalState)
|
||||
|
||||
<template #footer>
|
||||
<div v-if="editing && base" class="access-key-drawer__delete">
|
||||
<AccessKeyDeleteDialog
|
||||
:access-key="base"
|
||||
:total="total"
|
||||
@deleted="handleDeleted"
|
||||
/>
|
||||
<AccessKeyDeleteDialog :access-key="base" :total="total" @deleted="handleDeleted" />
|
||||
</div>
|
||||
<AppButton variant="secondary" :disabled="closeBlocked" @click="setOpen(false)">
|
||||
{{ t('common.cancel') }}
|
||||
|
||||
@@ -45,10 +45,7 @@ function optionDisabled(dimension: 'protocols' | 'models'): boolean {
|
||||
return props.disabled || props.modes[dimension] !== 'restricted' || catalogUnavailable()
|
||||
}
|
||||
|
||||
function requestScopeMode(
|
||||
dimension: AccessKeyScopeDimension,
|
||||
mode: AccessKeyScopeMode,
|
||||
): void {
|
||||
function requestScopeMode(dimension: AccessKeyScopeDimension, mode: AccessKeyScopeMode): void {
|
||||
emit('setScopeMode', dimension, mode)
|
||||
}
|
||||
|
||||
@@ -78,7 +75,6 @@ function toggleProtocol(protocol: AccessProtocol, event: Event): void {
|
||||
: props.filters.protocols.filter((value) => value !== protocol)
|
||||
emit('update:protocols', next)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -164,11 +160,7 @@ function toggleProtocol(protocol: AccessProtocol, event: Event): void {
|
||||
<i aria-hidden="true" />{{ t('accessKeys.drawer.allProtocolsAllowed') }}
|
||||
</div>
|
||||
<div v-else class="protocol-options">
|
||||
<label
|
||||
v-for="protocol in protocolOptions"
|
||||
:key="protocol"
|
||||
class="access-key-drawer__check"
|
||||
>
|
||||
<label v-for="protocol in protocolOptions" :key="protocol" class="access-key-drawer__check">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="filters.protocols.includes(protocol)"
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
import { applyInvalidationPlan, mutationInvalidationPlans } from '@/app/resources/invalidation'
|
||||
import { groupDetailLocation } from '@/app/route-locations'
|
||||
import AppButton from '@/components/ui/AppButton.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppConfirmDialog from '@/components/ui/AppConfirmDialog.vue'
|
||||
import DataTable from '@/components/ui/DataTable.vue'
|
||||
import EmptyState from '@/components/ui/EmptyState.vue'
|
||||
import QueryFeedback from '@/components/ui/QueryFeedback.vue'
|
||||
@@ -290,13 +290,17 @@ const effectiveLabels = computed(() => ({
|
||||
>
|
||||
{{ t('group.keys.saveWeight') }}
|
||||
</AppButton>
|
||||
<AppDialog
|
||||
<AppConfirmDialog
|
||||
:open="deleteKeyId === key.id"
|
||||
:title="t('group.keys.deleteTitle')"
|
||||
:description="t('group.keys.deleteDescription', { mask: key.mask })"
|
||||
:close-label="t('group.keys.closeDialog')"
|
||||
:dismissible="!pending(key.id)"
|
||||
:cancel-label="t('group.keys.cancel')"
|
||||
:confirm-label="t('group.keys.confirmDelete')"
|
||||
tone="danger"
|
||||
:pending="pending(key.id)"
|
||||
@update:open="setDeleteDialog($event, key.id)"
|
||||
@confirm="confirmDelete(key)"
|
||||
>
|
||||
<template #trigger>
|
||||
<AppButton
|
||||
@@ -308,25 +312,7 @@ const effectiveLabels = computed(() => ({
|
||||
{{ t('group.keys.delete') }}
|
||||
</AppButton>
|
||||
</template>
|
||||
<div class="group-keys__dialog-actions">
|
||||
<AppButton
|
||||
variant="secondary"
|
||||
:disabled="pending(key.id)"
|
||||
@click="setDeleteDialog(false, key.id)"
|
||||
>
|
||||
{{ t('group.keys.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
class="group-keys__confirm-delete"
|
||||
variant="secondary"
|
||||
:busy="pending(key.id)"
|
||||
:disabled="pending(key.id)"
|
||||
@click="confirmDelete(key)"
|
||||
>
|
||||
{{ t('group.keys.confirmDelete') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</AppConfirmDialog>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -381,8 +367,7 @@ const effectiveLabels = computed(() => ({
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
.group-keys__actions,
|
||||
.group-keys__dialog-actions {
|
||||
.group-keys__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
@@ -392,16 +377,9 @@ const effectiveLabels = computed(() => ({
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.group-keys__delete :deep(button),
|
||||
.group-keys__delete,
|
||||
.group-keys__confirm-delete {
|
||||
.group-keys__delete {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.group-keys__confirm-delete {
|
||||
border-color: var(--color-danger);
|
||||
}
|
||||
.group-keys__dialog-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.group-keys__action-error {
|
||||
margin: 0;
|
||||
border-radius: var(--radius-control);
|
||||
|
||||
@@ -19,7 +19,7 @@ import ModelDraftEditor, {
|
||||
type ModelDraftEditorItem,
|
||||
} from '@/components/config/ModelDraftEditor.vue'
|
||||
import AppButton from '@/components/ui/AppButton.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppConfirmDialog from '@/components/ui/AppConfirmDialog.vue'
|
||||
import InlineFeedback from '@/components/ui/InlineFeedback.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
|
||||
@@ -219,14 +219,18 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<RefreshCw :size="16" aria-hidden="true" />{{ t('group.modelEditor.rediscover') }}
|
||||
</AppButton>
|
||||
<AppDialog
|
||||
<AppConfirmDialog
|
||||
v-if="emptySelection"
|
||||
:open="emptyConfirmOpen"
|
||||
:title="t('group.modelEditor.emptyConfirm.title')"
|
||||
:description="t('group.modelEditor.emptyConfirm.description')"
|
||||
:close-label="t('group.modelEditor.emptyConfirm.close')"
|
||||
:dismissible="!pending"
|
||||
:cancel-label="t('group.modelEditor.emptyConfirm.cancel')"
|
||||
:confirm-label="t('group.modelEditor.emptyConfirm.confirm')"
|
||||
tone="danger"
|
||||
:pending="pending"
|
||||
@update:open="setEmptyConfirmOpen"
|
||||
@confirm="confirmEmptyReplace"
|
||||
>
|
||||
<template #trigger>
|
||||
<AppButton
|
||||
@@ -240,21 +244,7 @@ onBeforeUnmount(() => {
|
||||
<InlineFeedback v-if="saveError" tone="danger">
|
||||
{{ t('group.modelEditor.saveFailed') }}
|
||||
</InlineFeedback>
|
||||
<div class="group-models__dialog-actions">
|
||||
<AppButton variant="secondary" :disabled="pending" @click="setEmptyConfirmOpen(false)">
|
||||
{{ t('group.modelEditor.emptyConfirm.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
class="group-models__confirm-empty"
|
||||
variant="secondary"
|
||||
:busy="pending"
|
||||
:disabled="pending"
|
||||
@click="confirmEmptyReplace"
|
||||
>
|
||||
{{ t('group.modelEditor.emptyConfirm.confirm') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</AppConfirmDialog>
|
||||
<AppButton
|
||||
v-else
|
||||
:busy="pendingAction === 'save'"
|
||||
@@ -333,8 +323,7 @@ onBeforeUnmount(() => {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.group-models__actions,
|
||||
.group-models__guidance-actions,
|
||||
.group-models__dialog-actions {
|
||||
.group-models__guidance-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
@@ -358,13 +347,6 @@ onBeforeUnmount(() => {
|
||||
.group-models__guidance-actions .button-link {
|
||||
min-height: 44px;
|
||||
}
|
||||
.group-models__dialog-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.group-models__confirm-empty {
|
||||
border-color: var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.group-models__header {
|
||||
align-items: stretch;
|
||||
|
||||
@@ -12,7 +12,8 @@ import { controlQueryKeys } from '@/app/query-keys'
|
||||
import { applyInvalidationPlan, mutationInvalidationPlans } from '@/app/resources/invalidation'
|
||||
import { homeLocation } from '@/app/route-locations'
|
||||
import AppButton from '@/components/ui/AppButton.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppConfirmDialog from '@/components/ui/AppConfirmDialog.vue'
|
||||
import AppTypedConfirmation from '@/components/ui/AppTypedConfirmation.vue'
|
||||
import InlineFeedback from '@/components/ui/InlineFeedback.vue'
|
||||
|
||||
import GroupInUseFeedback from './GroupInUseFeedback.vue'
|
||||
@@ -24,7 +25,7 @@ const router = useRouter()
|
||||
const { t } = useI18n()
|
||||
const open = ref(false)
|
||||
const typedName = ref('')
|
||||
const nameInput = ref<HTMLInputElement>()
|
||||
const nameInput = ref<InstanceType<typeof AppTypedConfirmation>>()
|
||||
const pending = ref(false)
|
||||
const genericError = ref(false)
|
||||
const references = ref<AccessKeyReferenceDto[]>([])
|
||||
@@ -90,13 +91,18 @@ onBeforeUnmount(() => controller?.abort())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog
|
||||
<AppConfirmDialog
|
||||
:open="open"
|
||||
:title="t('group.settings.delete.title')"
|
||||
:description="t('group.settings.delete.description', { name: groupName })"
|
||||
:close-label="t('group.settings.delete.close')"
|
||||
:dismissible="!pending"
|
||||
:cancel-label="t('group.settings.delete.cancel')"
|
||||
:confirm-label="t('group.settings.delete.confirm')"
|
||||
tone="danger"
|
||||
:pending="pending"
|
||||
:confirm-disabled="!confirmed"
|
||||
@update:open="setOpen"
|
||||
@confirm="confirmDelete"
|
||||
>
|
||||
<template #trigger>
|
||||
<AppButton class="group-delete__open" variant="secondary" @click="setOpen(true)">
|
||||
@@ -105,43 +111,23 @@ onBeforeUnmount(() => controller?.abort())
|
||||
</template>
|
||||
|
||||
<div class="group-delete__body">
|
||||
<label for="group-delete-name">{{
|
||||
t('group.settings.delete.typeName', { name: groupName })
|
||||
}}</label>
|
||||
<input
|
||||
<AppTypedConfirmation
|
||||
id="group-delete-name"
|
||||
ref="nameInput"
|
||||
v-model="typedName"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:label="t('group.settings.delete.typeName', { name: groupName })"
|
||||
:disabled="pending"
|
||||
/>
|
||||
<GroupInUseFeedback v-if="references.length" :references="references" />
|
||||
<InlineFeedback v-else-if="genericError" tone="danger">
|
||||
{{ t('group.settings.delete.failed') }}
|
||||
</InlineFeedback>
|
||||
<div class="group-delete__actions">
|
||||
<AppButton variant="secondary" :disabled="pending" @click="setOpen(false)">
|
||||
{{ t('group.settings.delete.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
class="group-delete__confirm"
|
||||
variant="secondary"
|
||||
:busy="pending"
|
||||
:disabled="!confirmed"
|
||||
@click="confirmDelete"
|
||||
>
|
||||
{{ t('group.settings.delete.confirm') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</AppConfirmDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.group-delete__open,
|
||||
.group-delete__confirm {
|
||||
.group-delete__open {
|
||||
border-color: var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
@@ -153,23 +139,4 @@ onBeforeUnmount(() => controller?.abort())
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
label {
|
||||
font-weight: 650;
|
||||
}
|
||||
input {
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
border: 1px solid var(--color-border-control);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--color-surface-sunken);
|
||||
color: var(--color-text);
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font: inherit;
|
||||
}
|
||||
.group-delete__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
</style>
|
||||
@@ -19,7 +19,7 @@ import { groupDetailLocation } from '@/app/route-locations'
|
||||
import { useUnsavedChanges } from '@/app/unsaved-changes'
|
||||
import HeaderRulesEditor from '@/components/config/HeaderRulesEditor.vue'
|
||||
import AppButton from '@/components/ui/AppButton.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppConfirmDialog from '@/components/ui/AppConfirmDialog.vue'
|
||||
import InlineFeedback from '@/components/ui/InlineFeedback.vue'
|
||||
import StatusBadge from '@/components/ui/StatusBadge.vue'
|
||||
import SurfaceCard from '@/components/ui/SurfaceCard.vue'
|
||||
@@ -394,29 +394,18 @@ onBeforeUnmount(() => {
|
||||
<GroupDeleteDialog :group-id="groupId" :group-name="savedGroup.name" />
|
||||
</SurfaceCard>
|
||||
|
||||
<AppDialog
|
||||
<AppConfirmDialog
|
||||
:open="urlConfirmOpen"
|
||||
:title="t('group.settings.urlConfirm.title')"
|
||||
:description="t('group.settings.urlConfirm.description')"
|
||||
:close-label="t('group.settings.urlConfirm.close')"
|
||||
:dismissible="!pending"
|
||||
:cancel-label="t('group.settings.urlConfirm.cancel')"
|
||||
:confirm-label="t('group.settings.urlConfirm.confirm')"
|
||||
:pending="pending"
|
||||
prevent-close-auto-focus
|
||||
@update:open="setURLConfirmOpen"
|
||||
>
|
||||
<div class="group-settings__dialog-actions">
|
||||
<AppButton variant="secondary" :disabled="pending" @click="setURLConfirmOpen(false)">
|
||||
{{ t('group.settings.urlConfirm.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
class="group-settings__confirm-url"
|
||||
variant="secondary"
|
||||
:busy="pending"
|
||||
@click="runSave(true)"
|
||||
>
|
||||
{{ t('group.settings.urlConfirm.confirm') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</AppDialog>
|
||||
@confirm="runSave(true)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -428,8 +417,7 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
.group-settings__header,
|
||||
.group-settings__section-heading,
|
||||
.group-settings__runtime-row,
|
||||
.group-settings__dialog-actions {
|
||||
.group-settings__runtime-row {
|
||||
display: flex;
|
||||
}
|
||||
.group-settings__header,
|
||||
@@ -538,15 +526,6 @@ small {
|
||||
.group-settings__danger {
|
||||
border-color: color-mix(in srgb, var(--color-danger) 38%, var(--color-border-subtle));
|
||||
}
|
||||
.group-settings__dialog-actions {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.group-settings__confirm-url {
|
||||
border-color: var(--color-warning);
|
||||
color: var(--color-warning);
|
||||
}
|
||||
@media (max-width: 759px) {
|
||||
.group-settings__header {
|
||||
align-items: stretch;
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { HomeBaseDto } from '@/app/resources/home'
|
||||
import { accessKeysLocation } from '@/app/route-locations'
|
||||
import { revealAccessKey } from '@/app/resources/access-keys'
|
||||
import AppButton from '@/components/ui/AppButton.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppConfirmDialog from '@/components/ui/AppConfirmDialog.vue'
|
||||
import AppSelect from '@/components/ui/AppSelect.vue'
|
||||
import CodeBlock from '@/components/ui/CodeBlock.vue'
|
||||
import CopyAction from '@/components/ui/CopyAction.vue'
|
||||
@@ -455,26 +455,16 @@ onBeforeUnmount(() => {
|
||||
{{ feedbackMessage }}
|
||||
</InlineFeedback>
|
||||
|
||||
<AppDialog
|
||||
<AppConfirmDialog
|
||||
v-model:open="nextChatConfirmationOpen"
|
||||
:title="t('home.ledger.connection.nextChatConfirmTitle')"
|
||||
:description="t('home.ledger.connection.nextChatConfirmDescription')"
|
||||
:close-label="t('common.close')"
|
||||
:dismissible="!actionBusy"
|
||||
>
|
||||
<div class="gateway-connection__dialog-actions">
|
||||
<AppButton
|
||||
variant="secondary"
|
||||
:disabled="actionBusy"
|
||||
@click="nextChatConfirmationOpen = false"
|
||||
>
|
||||
{{ t('common.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton :busy="actionBusy" @click="openNextChat">
|
||||
{{ t('home.ledger.connection.openNextChat') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</AppDialog>
|
||||
:cancel-label="t('common.cancel')"
|
||||
:confirm-label="t('home.ledger.connection.openNextChat')"
|
||||
:pending="actionBusy"
|
||||
@confirm="openNextChat"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -603,12 +593,6 @@ onBeforeUnmount(() => {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.gateway-connection__dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.gateway-connection__toolbar {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -8,8 +8,7 @@ import { useApiClient } from '@/api/client-context'
|
||||
import { resetModelPrice, type ModelPriceRuleDto } from '@/app/resources/model-prices'
|
||||
import { applyInvalidationPlan, mutationInvalidationPlans } from '@/app/resources/invalidation'
|
||||
import { RequestCancelledError } from '@/api/errors'
|
||||
import AppButton from '@/components/ui/AppButton.vue'
|
||||
import AppDialog from '@/components/ui/AppDialog.vue'
|
||||
import AppConfirmDialog from '@/components/ui/AppConfirmDialog.vue'
|
||||
import InlineFeedback from '@/components/ui/InlineFeedback.vue'
|
||||
|
||||
const props = defineProps<{ rule: ModelPriceRuleDto }>()
|
||||
@@ -71,13 +70,17 @@ onBeforeUnmount(clearRequest)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<AppDialog
|
||||
<AppConfirmDialog
|
||||
:open="open"
|
||||
:title="t('modelPrices.reset.title')"
|
||||
:description="t('modelPrices.reset.description', { pattern: rule.pattern })"
|
||||
:close-label="t('modelPrices.reset.close')"
|
||||
:dismissible="!pending"
|
||||
:cancel-label="t('common.cancel')"
|
||||
:confirm-label="t('modelPrices.reset.confirm')"
|
||||
tone="danger"
|
||||
:pending="pending"
|
||||
@update:open="setOpen"
|
||||
@confirm="confirmReset"
|
||||
>
|
||||
<template #trigger>
|
||||
<button type="button" class="model-price-reset__trigger" @click="setOpen(true)">
|
||||
@@ -90,21 +93,8 @@ onBeforeUnmount(clearRequest)
|
||||
<InlineFeedback v-if="failed" tone="danger">
|
||||
{{ t('modelPrices.reset.failed') }}
|
||||
</InlineFeedback>
|
||||
<div class="model-price-reset__actions">
|
||||
<AppButton variant="secondary" :disabled="pending" @click="setOpen(false)">
|
||||
{{ t('common.cancel') }}
|
||||
</AppButton>
|
||||
<AppButton
|
||||
class="model-price-reset__confirm"
|
||||
variant="secondary"
|
||||
:busy="pending"
|
||||
@click="confirmReset"
|
||||
>
|
||||
{{ t('modelPrices.reset.confirm') }}
|
||||
</AppButton>
|
||||
</div>
|
||||
</div>
|
||||
</AppDialog>
|
||||
</AppConfirmDialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@@ -124,14 +114,4 @@ onBeforeUnmount(clearRequest)
|
||||
display: grid;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
.model-price-reset__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.model-price-reset__confirm {
|
||||
border-color: var(--color-danger);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
</style>
|
||||
@@ -57,10 +57,7 @@ async function setDrawerOpen(open: boolean): Promise<void> {
|
||||
<ArrowLeft :size="16" aria-hidden="true" />{{ t('modelPrices.back') }}
|
||||
</RouterLink>
|
||||
|
||||
<PageHeader
|
||||
id="model-prices-title"
|
||||
:title="t('modelPrices.title')"
|
||||
>
|
||||
<PageHeader id="model-prices-title" :title="t('modelPrices.title')">
|
||||
<template #actions>
|
||||
<AppButton @click="addOverride">
|
||||
<Plus :size="16" aria-hidden="true" />{{ t('modelPrices.add') }}
|
||||
|
||||
@@ -92,7 +92,8 @@ export default {
|
||||
'Edit routing filters and the RPM limit. Plaintext is masked unless explicitly revealed.',
|
||||
close: 'Close AccessKey editor',
|
||||
basicInformation: 'Basic information',
|
||||
basicInformationDescription: 'The name identifies the client and is not used for authentication.',
|
||||
basicInformationDescription:
|
||||
'The name identifies the client and is not used for authentication.',
|
||||
permissionScope: 'Permission scope',
|
||||
permissionScopeDescription: 'Values within a dimension use OR; all three dimensions use AND.',
|
||||
scopeLogic: 'Permission logic',
|
||||
|
||||
Reference in new issue
Block a user