refactor: minor fixes to overall design.
This commit is contained in:
parent
dbff4c6798
commit
360d2f3df8
14 changed files with 1776 additions and 801 deletions
|
|
@ -40,7 +40,8 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-import"
|
||||
size="lg"
|
||||
class="justify-center sm:min-w-28"
|
||||
|
|
@ -248,7 +249,7 @@
|
|||
<div class="flex items-end">
|
||||
<UButton
|
||||
type="button"
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
:disabled="addInProgress"
|
||||
|
|
@ -306,7 +307,8 @@
|
|||
<div class="flex items-end">
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-plus"
|
||||
class="justify-center"
|
||||
:disabled="addInProgress || !newExtraKey || !newExtraValue"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-import"
|
||||
size="lg"
|
||||
class="justify-center sm:min-w-28"
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-import"
|
||||
size="lg"
|
||||
class="justify-center sm:min-w-28"
|
||||
|
|
@ -382,7 +383,7 @@
|
|||
<div class="flex items-end">
|
||||
<UButton
|
||||
type="button"
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
:disabled="addInProgress"
|
||||
|
|
|
|||
|
|
@ -71,7 +71,8 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-import"
|
||||
size="lg"
|
||||
:disabled="!importString"
|
||||
|
|
|
|||
|
|
@ -1,83 +1,17 @@
|
|||
<template>
|
||||
<div class="space-y-6">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:icon="showImport ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||
:disabled="isBusy"
|
||||
@click="showImport = !showImport"
|
||||
>
|
||||
{{ showImport ? 'Hide import' : 'Show import' }}
|
||||
</UButton>
|
||||
|
||||
<div class="inline-flex rounded-md border border-default bg-muted/20 p-1">
|
||||
<UButton
|
||||
type="button"
|
||||
size="sm"
|
||||
icon="i-lucide-sliders-horizontal"
|
||||
:color="mode === 'gui' ? 'primary' : 'neutral'"
|
||||
:variant="mode === 'gui' ? 'solid' : 'ghost'"
|
||||
:disabled="!guiSupported || isBusy"
|
||||
@click="switchMode('gui')"
|
||||
>
|
||||
GUI
|
||||
</UButton>
|
||||
<UButton
|
||||
type="button"
|
||||
size="sm"
|
||||
icon="i-lucide-code"
|
||||
:color="mode === 'advanced' ? 'primary' : 'neutral'"
|
||||
:variant="mode === 'advanced' ? 'solid' : 'ghost'"
|
||||
:disabled="isBusy"
|
||||
@click="switchMode('advanced')"
|
||||
>
|
||||
Advanced
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2">
|
||||
<UButton
|
||||
v-if="mode === 'advanced'"
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-wand-sparkles"
|
||||
:disabled="isBusy"
|
||||
@click="beautify"
|
||||
>
|
||||
Format
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-x"
|
||||
:disabled="submitting"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
size="sm"
|
||||
icon="i-lucide-save"
|
||||
:loading="submitting"
|
||||
:disabled="isBusy"
|
||||
@click="submit"
|
||||
>
|
||||
Save
|
||||
</UButton>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
:icon="showImport ? 'i-lucide-chevron-up' : 'i-lucide-chevron-down'"
|
||||
:disabled="isBusy"
|
||||
@click="showImport = !showImport"
|
||||
>
|
||||
{{ showImport ? 'Hide import' : 'Show import' }}
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
@ -132,7 +66,8 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-import"
|
||||
class="justify-center sm:min-w-28"
|
||||
:disabled="isBusy || !importString.trim()"
|
||||
|
|
@ -219,7 +154,7 @@
|
|||
<UFormField
|
||||
class="md:col-span-3"
|
||||
:ui="fieldUi"
|
||||
description="Disabled definitions won't match tasks."
|
||||
description="Whether this definition is active or not."
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
|
|
@ -364,7 +299,7 @@
|
|||
</div>
|
||||
|
||||
<div class="grid gap-4 md:grid-cols-12">
|
||||
<UFormField class="md:col-span-4" :ui="fieldUi">
|
||||
<UFormField class="md:col-span-4" :ui="fieldUi" description="Selector type">
|
||||
<template #label>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UIcon name="i-lucide-shapes" class="size-4 text-toned" />
|
||||
|
|
@ -383,7 +318,7 @@
|
|||
/>
|
||||
</UFormField>
|
||||
|
||||
<UFormField class="md:col-span-8" :ui="fieldUi">
|
||||
<UFormField class="md:col-span-8" :ui="fieldUi" description="Match expression">
|
||||
<template #label>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UIcon name="i-lucide-crosshair" class="size-4 text-toned" />
|
||||
|
|
@ -415,7 +350,8 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-plus"
|
||||
:disabled="isBusy"
|
||||
|
|
@ -517,7 +453,7 @@
|
|||
<td class="px-3 py-3 text-right">
|
||||
<UButton
|
||||
type="button"
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -571,6 +507,77 @@
|
|||
:description="errorMessage"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="flex flex-col gap-3 border-t border-default pt-5 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div class="inline-flex self-start rounded-md border border-default bg-muted/20 p-1">
|
||||
<UButton
|
||||
type="button"
|
||||
size="sm"
|
||||
icon="i-lucide-sliders-horizontal"
|
||||
color="neutral"
|
||||
:variant="mode === 'gui' ? 'soft' : 'ghost'"
|
||||
:disabled="!guiSupported || isBusy"
|
||||
@click="switchMode('gui')"
|
||||
>
|
||||
GUI
|
||||
</UButton>
|
||||
<UButton
|
||||
type="button"
|
||||
size="sm"
|
||||
icon="i-lucide-code"
|
||||
color="neutral"
|
||||
:variant="mode === 'advanced' ? 'soft' : 'ghost'"
|
||||
:disabled="isBusy"
|
||||
@click="switchMode('advanced')"
|
||||
>
|
||||
Advanced
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<UButton
|
||||
v-if="mode === 'advanced'"
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
icon="i-lucide-wand-sparkles"
|
||||
:disabled="isBusy"
|
||||
class="justify-center"
|
||||
@click="beautify"
|
||||
>
|
||||
Format
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
type="button"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="lg"
|
||||
icon="i-lucide-x"
|
||||
:disabled="submitting"
|
||||
class="justify-center"
|
||||
@click="cancel"
|
||||
>
|
||||
Cancel
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
size="lg"
|
||||
icon="i-lucide-save"
|
||||
:loading="submitting"
|
||||
:disabled="isBusy"
|
||||
class="justify-center"
|
||||
@click="submit"
|
||||
>
|
||||
Save
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:loading="convertInProgress"
|
||||
|
|
@ -94,7 +94,8 @@
|
|||
|
||||
<UButton
|
||||
type="button"
|
||||
color="primary"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-import"
|
||||
size="lg"
|
||||
:disabled="!import_string"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@
|
|||
<div class="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
||||
<UButton
|
||||
type="button"
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-rotate-ccw"
|
||||
:disabled="loading"
|
||||
|
|
|
|||
50
ui/app/composables/useExpandableMeta.ts
Normal file
50
ui/app/composables/useExpandableMeta.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
export const useExpandableMeta = () => {
|
||||
const expandedItems = ref<Record<string, Set<string>>>({});
|
||||
|
||||
const normalizeId = (itemId: string | number | undefined | null): string | null => {
|
||||
if (itemId === undefined || itemId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return String(itemId);
|
||||
};
|
||||
|
||||
const toggleExpand = (itemId: string | number | undefined | null, field: string): void => {
|
||||
const key = normalizeId(itemId);
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expandedItems.value[key]) {
|
||||
expandedItems.value[key] = new Set();
|
||||
}
|
||||
|
||||
if (expandedItems.value[key]?.has(field)) {
|
||||
expandedItems.value[key]?.delete(field);
|
||||
return;
|
||||
}
|
||||
|
||||
expandedItems.value[key]?.add(field);
|
||||
};
|
||||
|
||||
const isExpanded = (itemId: string | number | undefined | null, field: string): boolean => {
|
||||
const key = normalizeId(itemId);
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expandedItems.value[key]?.has(field) ?? false;
|
||||
};
|
||||
|
||||
const expandClass = (itemId: string | number | undefined | null, field: string): string => {
|
||||
return isExpanded(itemId, field)
|
||||
? 'block max-w-full whitespace-pre-wrap break-words'
|
||||
: 'block max-w-full truncate';
|
||||
};
|
||||
|
||||
return {
|
||||
toggleExpand,
|
||||
isExpanded,
|
||||
expandClass,
|
||||
};
|
||||
};
|
||||
|
|
@ -84,6 +84,41 @@
|
|||
@navigate="navigatePage"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!isLoading && filteredItems.length > 0"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ allSelected ? 'Unselect' : 'Select' }}
|
||||
</UButton>
|
||||
|
||||
<UBadge v-if="selectedIds.length > 0" color="error" variant="soft" size="sm">
|
||||
{{ selectedIds.length }}
|
||||
</UBadge>
|
||||
|
||||
<UDropdownMenu :items="bulkActionGroups" :modal="false">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-list"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-toned">{{ filteredItems.length }} displayed</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayStyle === 'list' && filteredItems.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
|
|
@ -92,6 +127,14 @@
|
|||
<table class="min-w-225 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
:name="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Condition</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
|
|
@ -99,57 +142,51 @@
|
|||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="cond in filteredItems" :key="cond.id" class="hover:bg-muted/20">
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="cond.id"
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 align-middle">
|
||||
<div class="space-y-2">
|
||||
<div class="font-semibold text-highlighted">{{ cond.name }}</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs text-toned">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
<UTooltip
|
||||
:text="`Click to ${cond.enabled !== false ? 'disable' : 'enable'} condition`"
|
||||
>
|
||||
<USwitch
|
||||
:model-value="cond.enabled !== false"
|
||||
:disabled="conditions.addInProgress.value"
|
||||
@update:model-value="() => void toggleEnabled(cond)"
|
||||
/>
|
||||
</UTooltip>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
:disabled="conditions.addInProgress.value"
|
||||
@click="() => void toggleEnabled(cond)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
class="size-3.5"
|
||||
:class="cond.enabled !== false ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ cond.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<span v-if="cond.priority > 0" class="inline-flex items-center gap-1">
|
||||
<span
|
||||
v-if="cond.priority > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority: {{ cond.priority }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 text-xs text-toned">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-filter" class="mt-0.5 size-3.5 shrink-0" />
|
||||
<code class="wrap-break-word">{{ cond.filter }}</code>
|
||||
</div>
|
||||
|
||||
<div v-if="cond.cli" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-3.5 shrink-0" />
|
||||
<code class="wrap-break-word">{{ cond.cli }}</code>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="([key, value], index) in extrasEntries(cond.extras)"
|
||||
:key="`${cond.id}-${key}-${index}`"
|
||||
class="flex items-start gap-2"
|
||||
>
|
||||
<UIcon name="i-lucide-list" class="mt-0.5 size-3.5 shrink-0" />
|
||||
<code class="wrap-break-word">{{ key }}: {{ value }}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
|
|
@ -159,7 +196,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
|
|
@ -169,7 +206,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -186,135 +223,172 @@
|
|||
</div>
|
||||
|
||||
<div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<UCard
|
||||
v-for="cond in filteredItems"
|
||||
:key="cond.id"
|
||||
class="flex h-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 w-full text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(cond.id, 'name')"
|
||||
>
|
||||
<span :class="expandClass(cond.id, 'name')">{{ cond.name }}</span>
|
||||
</button>
|
||||
<div v-for="cond in filteredItems" :key="cond.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(cond.id, 'name')"
|
||||
>
|
||||
<span :class="['block', expandClass(cond.id, 'name')]">{{ cond.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
:disabled="conditions.addInProgress.value"
|
||||
@click="() => void toggleEnabled(cond)"
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(cond)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
class="size-3.5"
|
||||
:class="cond.enabled !== false ? 'text-success' : 'text-error'"
|
||||
<span class="hidden sm:inline">Export Condition</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="cond.id"
|
||||
/>
|
||||
<span>{{ cond.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
v-if="cond.priority > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority {{ cond.priority }}</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
:disabled="conditions.addInProgress.value"
|
||||
@click="() => void toggleEnabled(cond)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
class="size-3.5"
|
||||
:class="cond.enabled !== false ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ cond.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
v-if="cond.priority > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority: {{ cond.priority }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(cond.id, 'filter')"
|
||||
>
|
||||
<UIcon name="i-lucide-filter" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Filter</div>
|
||||
<span :class="['block', expandClass(cond.id, 'filter')]">{{ cond.filter }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="cond.cli || cond.description" class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
v-if="cond.cli"
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!cond.description && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(cond.id, 'cli')"
|
||||
>
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">CLI</div>
|
||||
<span :class="['block', expandClass(cond.id, 'cli')]">{{ cond.cli }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="cond.description"
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!cond.cli && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(cond.id, 'description')"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-message-square-text"
|
||||
class="mt-0.5 size-4 shrink-0 text-toned"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Description</div>
|
||||
<span :class="['block', expandClass(cond.id, 'description')]">{{
|
||||
cond.description
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="extrasEntries(cond.extras).length > 0"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2 text-toned">
|
||||
<UIcon name="i-lucide-list" class="size-4" />
|
||||
<span class="text-sm font-medium">Extras</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UBadge
|
||||
v-for="([key, value], index) in extrasEntries(cond.extras)"
|
||||
:key="`${cond.id}-${key}-${index}`"
|
||||
color="info"
|
||||
variant="soft"
|
||||
size="sm"
|
||||
>
|
||||
<span class="font-semibold">{{ key }}</span
|
||||
>: {{ value }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(cond)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="info"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(cond)"
|
||||
/>
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(cond)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(cond.id, 'filter')"
|
||||
>
|
||||
<UIcon name="i-lucide-filter" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span :class="expandClass(cond.id, 'filter')">{{ cond.filter }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="cond.cli"
|
||||
type="button"
|
||||
class="flex w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(cond.id, 'cli')"
|
||||
>
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span :class="expandClass(cond.id, 'cli')">{{ cond.cli }}</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
v-if="extrasEntries(cond.extras).length > 0"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2"
|
||||
>
|
||||
<div class="mb-2 flex items-center gap-2 text-toned">
|
||||
<UIcon name="i-lucide-list" class="size-4" />
|
||||
<span class="text-sm font-medium">Extras</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<UBadge
|
||||
v-for="([key, value], index) in extrasEntries(cond.extras)"
|
||||
:key="`${cond.id}-${key}-${index}`"
|
||||
color="info"
|
||||
variant="soft"
|
||||
size="sm"
|
||||
>
|
||||
<span class="font-semibold">{{ key }}</span
|
||||
>: {{ value }}
|
||||
</UBadge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="cond.description"
|
||||
type="button"
|
||||
class="flex w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(cond.id, 'description')"
|
||||
>
|
||||
<UIcon name="i-lucide-message-square-text" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span :class="expandClass(cond.id, 'description')">{{ cond.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(cond)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(cond)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
|
|
@ -402,7 +476,10 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useDialog } from '~/composables/useDialog';
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { useConditions } from '~/composables/useConditions';
|
||||
import type { Condition } from '~/types/conditions';
|
||||
|
|
@ -414,9 +491,11 @@ type ConditionItemWithUI = Condition & { raw?: boolean };
|
|||
const box = useConfirm();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const displayStyle = useStorage<'list' | 'grid'>('conditions_display_style', 'grid');
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const conditions = useConditions();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { confirmDialog } = useDialog();
|
||||
|
||||
const items = conditions.conditions as Ref<ConditionItemWithUI[]>;
|
||||
const paging = conditions.pagination;
|
||||
|
|
@ -428,9 +507,10 @@ const editorOpen = ref(false);
|
|||
const query = ref('');
|
||||
const showFilter = ref(false);
|
||||
const filterInput = ref<HTMLInputElement | null>(null);
|
||||
const selectedIds = ref<number[]>([]);
|
||||
const massDelete = ref(false);
|
||||
|
||||
const removeKeys = ['raw', 'toggle_description'];
|
||||
const expandedItems = reactive<Record<number, Set<string>>>({});
|
||||
|
||||
const filteredItems = computed<ConditionItemWithUI[]>(() => {
|
||||
const normalizedQuery = query.value?.toLowerCase();
|
||||
|
|
@ -441,6 +521,30 @@ const filteredItems = computed<ConditionItemWithUI[]>(() => {
|
|||
return items.value.filter((entry) => deepIncludes(entry, normalizedQuery, new WeakSet()));
|
||||
});
|
||||
|
||||
const selectableConditionIds = computed(() =>
|
||||
filteredItems.value.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
|
||||
const allSelected = computed(
|
||||
() =>
|
||||
selectableConditionIds.value.length > 0 &&
|
||||
selectableConditionIds.value.every((id) => selectedIds.value.includes(id)),
|
||||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
label: 'Remove Selected',
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error',
|
||||
disabled: !hasSelected.value || massDelete.value,
|
||||
onSelect: () => void deleteSelected(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const modalTitle = computed(() =>
|
||||
itemRef.value ? `Edit - ${item.value.name}` : 'Add new condition',
|
||||
);
|
||||
|
|
@ -457,6 +561,17 @@ watch(showFilter, (value) => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
filteredItems,
|
||||
(items) => {
|
||||
const validIds = new Set(
|
||||
items.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
selectedIds.value = selectedIds.value.filter((id) => validIds.has(id));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const syncPageQuery = async (pageNumber: number): Promise<void> => {
|
||||
const totalPages = conditions.pagination.value.total_pages;
|
||||
const nextQuery = { ...route.query };
|
||||
|
|
@ -515,32 +630,13 @@ const toggleDisplayStyle = (): void => {
|
|||
displayStyle.value = displayStyle.value === 'list' ? 'grid' : 'list';
|
||||
};
|
||||
|
||||
const toggleExpand = (itemId: number | undefined, field: string): void => {
|
||||
if (!itemId) {
|
||||
const toggleMasterSelection = (): void => {
|
||||
if (allSelected.value) {
|
||||
selectedIds.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!expandedItems[itemId]) {
|
||||
expandedItems[itemId] = new Set();
|
||||
}
|
||||
|
||||
if (expandedItems[itemId].has(field)) {
|
||||
expandedItems[itemId].delete(field);
|
||||
} else {
|
||||
expandedItems[itemId].add(field);
|
||||
}
|
||||
};
|
||||
|
||||
const isExpanded = (itemId: number | undefined, field: string): boolean => {
|
||||
if (!itemId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expandedItems[itemId]?.has(field) ?? false;
|
||||
};
|
||||
|
||||
const expandClass = (itemId: number | undefined, field: string): string => {
|
||||
return isExpanded(itemId, field) ? 'whitespace-pre-wrap break-words' : 'truncate';
|
||||
selectedIds.value = [...selectableConditionIds.value];
|
||||
};
|
||||
|
||||
const extrasEntries = (extras?: Record<string, unknown>): Array<[string, unknown]> => {
|
||||
|
|
@ -559,6 +655,50 @@ const deleteItem = async (cond: Condition): Promise<void> => {
|
|||
await conditions.deleteCondition(cond.id!);
|
||||
};
|
||||
|
||||
const deleteSelected = async (): Promise<void> => {
|
||||
if (selectedIds.value.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = await confirmDialog({
|
||||
title: 'Delete Selected Conditions',
|
||||
rawHTML:
|
||||
`Delete <strong class="text-red-500">${selectedIds.value.length}</strong> condition/s?<ul>` +
|
||||
selectedIds.value
|
||||
.map((id) => {
|
||||
const item = filteredItems.value.find((cond) => cond.id === id);
|
||||
return item ? `<li>${item.id}: ${item.name}</li>` : '';
|
||||
})
|
||||
.join('') +
|
||||
'</ul>',
|
||||
confirmText: 'Delete',
|
||||
confirmColor: 'error',
|
||||
});
|
||||
|
||||
if (true !== status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsToDelete = filteredItems.value.filter(
|
||||
(item) => item.id && selectedIds.value.includes(item.id),
|
||||
);
|
||||
if (itemsToDelete.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
massDelete.value = true;
|
||||
|
||||
for (const item of itemsToDelete) {
|
||||
if (!item.id) {
|
||||
continue;
|
||||
}
|
||||
await conditions.deleteCondition(item.id);
|
||||
}
|
||||
|
||||
selectedIds.value = [];
|
||||
massDelete.value = false;
|
||||
};
|
||||
|
||||
const updateItem = async ({
|
||||
reference,
|
||||
item: updatedItem,
|
||||
|
|
|
|||
|
|
@ -84,6 +84,41 @@
|
|||
@navigate="navigatePage"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!isLoading && filteredItems.length > 0"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ allSelected ? 'Unselect' : 'Select' }}
|
||||
</UButton>
|
||||
|
||||
<UBadge v-if="selectedIds.length > 0" color="error" variant="soft" size="sm">
|
||||
{{ selectedIds.length }}
|
||||
</UBadge>
|
||||
|
||||
<UDropdownMenu :items="bulkActionGroups" :modal="false">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-list"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-toned">{{ filteredItems.length }} displayed</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayStyle === 'list' && filteredItems.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
|
|
@ -92,6 +127,14 @@
|
|||
<table class="min-w-225 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
:name="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Field</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
|
|
@ -99,29 +142,42 @@
|
|||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="field in filteredItems" :key="field.id" class="hover:bg-muted/20">
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="field.id"
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 align-middle">
|
||||
<div class="space-y-2">
|
||||
<div class="font-semibold text-highlighted">{{ field.name }}</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs text-toned">
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<UIcon name="i-lucide-terminal" class="size-3.5" />
|
||||
<span>{{ field.field }}</span>
|
||||
</span>
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Order: {{ field.order }}</span>
|
||||
</span>
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-shapes" class="size-3.5" />
|
||||
<span>{{ field.kind }}</span>
|
||||
<span>Type: {{ field.kind }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="field.description" class="text-xs text-toned">
|
||||
{{ field.description }}
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-terminal" class="size-3.5" />
|
||||
<span>Option: {{ field.field }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -129,7 +185,7 @@
|
|||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
|
|
@ -139,7 +195,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
|
|
@ -149,7 +205,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -166,89 +222,125 @@
|
|||
</div>
|
||||
|
||||
<div v-else-if="filteredItems.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<UCard
|
||||
v-for="field in filteredItems"
|
||||
:key="field.id"
|
||||
class="flex h-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<div class="truncate text-sm font-semibold text-highlighted">{{ field.name }}</div>
|
||||
<div v-for="field in filteredItems" :key="field.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(field.id, 'title')"
|
||||
>
|
||||
<span :class="['block', expandClass(field.id, 'title')]">{{ field.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(field)"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Order {{ field.order }}</span>
|
||||
</span>
|
||||
<span class="hidden sm:inline">Export</span>
|
||||
</UButton>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-shapes" class="size-3.5" />
|
||||
<span>{{ field.kind }}</span>
|
||||
</span>
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="field.id"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Order: {{ field.order }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-shapes" class="size-3.5" />
|
||||
<span>Type: {{ field.kind }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!field.description && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(field.id, 'field')"
|
||||
>
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Associated option</div>
|
||||
<span :class="['block', expandClass(field.id, 'field')]">{{ field.field }}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="field.description"
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(field.id, 'description')"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-message-square-text"
|
||||
class="mt-0.5 size-4 shrink-0 text-toned"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Description</div>
|
||||
<span :class="['block', expandClass(field.id, 'description')]">
|
||||
{{ field.description }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(field)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="info"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(field)"
|
||||
/>
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(field)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="rounded-md border border-default bg-muted/20 px-3 py-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span class="wrap-break-word">{{ field.field }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="field.description"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon
|
||||
name="i-lucide-message-square-text"
|
||||
class="mt-0.5 size-4 shrink-0 text-toned"
|
||||
/>
|
||||
<span class="wrap-break-word">{{ field.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(field)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(field)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
|
|
@ -307,7 +399,10 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useDialog } from '~/composables/useDialog';
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { useDlFields } from '~/composables/useDlFields';
|
||||
import type { DLField } from '~/types/dl_fields';
|
||||
|
|
@ -316,10 +411,12 @@ import { copyText, encode } from '~/utils';
|
|||
|
||||
const box = useConfirm();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const displayStyle = useStorage<'list' | 'grid'>('dl_fields_display_style', 'grid');
|
||||
const dlFields = useDlFields();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const { confirmDialog } = useDialog();
|
||||
|
||||
const items = dlFields.dlFields as Ref<DLField[]>;
|
||||
const paging = dlFields.pagination;
|
||||
|
|
@ -331,6 +428,8 @@ const editorOpen = ref(false);
|
|||
const query = ref('');
|
||||
const showFilter = ref(false);
|
||||
const filterInput = ref<HTMLInputElement | null>(null);
|
||||
const selectedIds = ref<number[]>([]);
|
||||
const massDelete = ref(false);
|
||||
|
||||
const filteredItems = computed<DLField[]>(() => {
|
||||
const normalizedQuery = query.value?.toLowerCase();
|
||||
|
|
@ -341,6 +440,30 @@ const filteredItems = computed<DLField[]>(() => {
|
|||
return items.value.filter((entry) => deepIncludes(entry, normalizedQuery, new WeakSet()));
|
||||
});
|
||||
|
||||
const selectableFieldIds = computed(() =>
|
||||
filteredItems.value.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
|
||||
const allSelected = computed(
|
||||
() =>
|
||||
selectableFieldIds.value.length > 0 &&
|
||||
selectableFieldIds.value.every((id) => selectedIds.value.includes(id)),
|
||||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
label: 'Remove Selected',
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error',
|
||||
disabled: !hasSelected.value || massDelete.value,
|
||||
onSelect: () => void deleteSelected(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const modalTitle = computed(() => (itemRef.value ? `Edit - ${item.value.name}` : 'Add new field'));
|
||||
const modalDescription = computed(
|
||||
() => 'Custom fields allow you to add new fields to the download form.',
|
||||
|
|
@ -355,6 +478,17 @@ watch(showFilter, (value) => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
filteredItems,
|
||||
(items) => {
|
||||
const validIds = new Set(
|
||||
items.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
selectedIds.value = selectedIds.value.filter((id) => validIds.has(id));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const syncPageQuery = async (pageNumber: number): Promise<void> => {
|
||||
const totalPages = dlFields.pagination.value.total_pages;
|
||||
const nextQuery = { ...route.query };
|
||||
|
|
@ -413,6 +547,15 @@ const toggleDisplayStyle = (): void => {
|
|||
displayStyle.value = displayStyle.value === 'list' ? 'grid' : 'list';
|
||||
};
|
||||
|
||||
const toggleMasterSelection = (): void => {
|
||||
if (allSelected.value) {
|
||||
selectedIds.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIds.value = [...selectableFieldIds.value];
|
||||
};
|
||||
|
||||
const deleteItem = async (field: DLField): Promise<void> => {
|
||||
if (true !== (await box.confirm(`Delete '${field.name}'?`))) {
|
||||
return;
|
||||
|
|
@ -421,6 +564,50 @@ const deleteItem = async (field: DLField): Promise<void> => {
|
|||
await dlFields.deleteDlField(field.id!);
|
||||
};
|
||||
|
||||
const deleteSelected = async (): Promise<void> => {
|
||||
if (selectedIds.value.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = await confirmDialog({
|
||||
title: 'Delete Selected Fields',
|
||||
rawHTML:
|
||||
`Delete <strong class="text-red-500">${selectedIds.value.length}</strong> field/s?<ul>` +
|
||||
selectedIds.value
|
||||
.map((id) => {
|
||||
const item = filteredItems.value.find((field) => field.id === id);
|
||||
return item ? `<li>${item.id}: ${item.name}</li>` : '';
|
||||
})
|
||||
.join('') +
|
||||
'</ul>',
|
||||
confirmText: 'Delete',
|
||||
confirmColor: 'error',
|
||||
});
|
||||
|
||||
if (true !== status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsToDelete = filteredItems.value.filter(
|
||||
(item) => item.id && selectedIds.value.includes(item.id),
|
||||
);
|
||||
if (itemsToDelete.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
massDelete.value = true;
|
||||
|
||||
for (const item of itemsToDelete) {
|
||||
if (!item.id) {
|
||||
continue;
|
||||
}
|
||||
await dlFields.deleteDlField(item.id);
|
||||
}
|
||||
|
||||
selectedIds.value = [];
|
||||
massDelete.value = false;
|
||||
};
|
||||
|
||||
const updateItem = async ({
|
||||
reference,
|
||||
item: updatedItem,
|
||||
|
|
|
|||
|
|
@ -97,6 +97,41 @@
|
|||
@navigate="navigatePage"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!isLoading && filteredTargets.length > 0"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ allSelected ? 'Unselect' : 'Select' }}
|
||||
</UButton>
|
||||
|
||||
<UBadge v-if="selectedIds.length > 0" color="error" variant="soft" size="sm">
|
||||
{{ selectedIds.length }}
|
||||
</UBadge>
|
||||
|
||||
<UDropdownMenu :items="bulkActionGroups" :modal="false">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-list"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-toned">{{ filteredTargets.length }} displayed</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="displayStyle === 'list' && filteredTargets.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
|
|
@ -105,6 +140,14 @@
|
|||
<table class="min-w-225 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
:name="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Targets</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
|
|
@ -112,6 +155,17 @@
|
|||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in filteredTargets" :key="item.id" class="hover:bg-muted/20">
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="item.id"
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 align-middle">
|
||||
<div class="space-y-2">
|
||||
<div class="min-w-0 text-sm font-semibold text-highlighted">
|
||||
|
|
@ -141,19 +195,26 @@
|
|||
<span>{{ item.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-bell-ring" class="size-3.5" />
|
||||
<span>On: {{ joinEvents(item.on) }}</span>
|
||||
</span>
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-3.5" />
|
||||
<span>Presets: {{ joinPresets(item.presets) }}</span>
|
||||
</span>
|
||||
|
||||
<span v-if="headerKeys(item).length > 0" class="inline-flex items-center gap-1">
|
||||
<span
|
||||
v-if="headerKeys(item).length > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-key" class="size-3.5" />
|
||||
<span>Headers: {{ headerKeys(item).join(', ') }}</span>
|
||||
<span>Headers: {{ headerKeys(item).length }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -162,7 +223,7 @@
|
|||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
|
|
@ -172,7 +233,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
|
|
@ -182,7 +243,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -199,104 +260,185 @@
|
|||
</div>
|
||||
|
||||
<div v-else-if="filteredTargets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<UCard
|
||||
v-for="item in filteredTargets"
|
||||
:key="item.id"
|
||||
class="flex h-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<span class="text-sm font-semibold text-highlighted">
|
||||
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }})
|
||||
</span>
|
||||
@
|
||||
<a
|
||||
:href="item.request.url"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="text-sm text-primary hover:underline"
|
||||
>
|
||||
{{ item.name }}
|
||||
</a>
|
||||
<div v-for="item in filteredTargets" :key="item.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(item.id, 'title')"
|
||||
>
|
||||
<span :class="['block', expandClass(item.id, 'title')]">
|
||||
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @
|
||||
{{ item.name }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-2 text-xs text-toned">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
:disabled="addInProgress"
|
||||
@click="() => void toggleEnabled(item)"
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(item)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
class="size-3.5"
|
||||
:class="item.enabled !== false ? 'text-success' : 'text-error'"
|
||||
<span class="hidden sm:inline">Export Target</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="item.id"
|
||||
/>
|
||||
<span>{{ item.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
:disabled="addInProgress"
|
||||
@click="() => void toggleEnabled(item)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
class="size-3.5"
|
||||
:class="item.enabled !== false ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ item.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-bell-ring" class="size-3.5" />
|
||||
<span>Events: {{ item.on.length || 'All' }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-3.5" />
|
||||
<span>Presets: {{ item.presets.length || 'All' }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="headerKeys(item).length > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-key" class="size-3.5" />
|
||||
<span>Headers: {{ headerKeys(item).length }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'url')"
|
||||
>
|
||||
<UIcon name="i-lucide-link" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Target URL</div>
|
||||
<a
|
||||
:href="item.request.url"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="block text-highlighted hover:underline"
|
||||
@click.stop
|
||||
>
|
||||
<span :class="['block', expandClass(item.id, 'url')]">
|
||||
{{ item.request.url }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'events')"
|
||||
>
|
||||
<UIcon name="i-lucide-bell-ring" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Events</div>
|
||||
<span :class="['block', expandClass(item.id, 'events')]">{{
|
||||
joinEvents(item.on)
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'presets')"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-sliders-horizontal"
|
||||
class="mt-0.5 size-4 shrink-0 text-toned"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Presets</div>
|
||||
<span :class="['block', expandClass(item.id, 'presets')]">
|
||||
{{ joinPresets(item.presets) }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="headerKeys(item).length > 0"
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'headers')"
|
||||
>
|
||||
<UIcon name="i-lucide-key" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Headers</div>
|
||||
<span :class="['block', expandClass(item.id, 'headers')]">
|
||||
{{ headerKeys(item).join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="info"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(item)"
|
||||
/>
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="rounded-md border border-default bg-muted/20 px-3 py-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-bell-ring" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span class="wrap-break-word">On: {{ joinEvents(item.on) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-default bg-muted/20 px-3 py-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span class="wrap-break-word">Presets: {{ joinPresets(item.presets) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="headerKeys(item).length > 0"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-key" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span class="wrap-break-word">Headers: {{ headerKeys(item).join(', ') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editItem(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
|
|
@ -388,7 +530,10 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import { useDialog } from '~/composables/useDialog';
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { useNotifications } from '~/composables/useNotifications';
|
||||
import { copyText, encode, parse_api_error, request, ucFirst } from '~/utils';
|
||||
|
|
@ -397,7 +542,9 @@ import type { notification } from '~/types/notification';
|
|||
|
||||
const toast = useNotification();
|
||||
const box = useConfirm();
|
||||
const { confirmDialog } = useDialog();
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const displayStyleState = useStorage<'list' | 'grid' | 'cards'>(
|
||||
'notification_display_style',
|
||||
'cards',
|
||||
|
|
@ -419,6 +566,8 @@ const sendingTest = ref(false);
|
|||
const query = ref('');
|
||||
const showFilter = ref(false);
|
||||
const filterInput = ref<HTMLInputElement | null>(null);
|
||||
const selectedIds = ref<number[]>([]);
|
||||
const massDelete = ref(false);
|
||||
|
||||
const displayStyle = computed<'list' | 'grid'>(() =>
|
||||
displayStyleState.value === 'list' ? 'list' : 'grid',
|
||||
|
|
@ -445,12 +594,47 @@ const filteredTargets = computed<notification[]>(() => {
|
|||
return items.filter((item) => deepIncludes(item, normalizedQuery, new WeakSet()));
|
||||
});
|
||||
|
||||
const selectableNotificationIds = computed(() =>
|
||||
filteredTargets.value.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
|
||||
const allSelected = computed(
|
||||
() =>
|
||||
selectableNotificationIds.value.length > 0 &&
|
||||
selectableNotificationIds.value.every((id) => selectedIds.value.includes(id)),
|
||||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
label: 'Remove Selected',
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error',
|
||||
disabled: !hasSelected.value || massDelete.value,
|
||||
onSelect: () => void deleteSelected(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
watch(showFilter, (value) => {
|
||||
if (!value) {
|
||||
query.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
filteredTargets,
|
||||
(items) => {
|
||||
const validIds = new Set(
|
||||
items.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
selectedIds.value = selectedIds.value.filter((id) => validIds.has(id));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
function defaultState(): notification {
|
||||
return {
|
||||
name: '',
|
||||
|
|
@ -500,6 +684,15 @@ const openCreate = (): void => {
|
|||
editorOpen.value = true;
|
||||
};
|
||||
|
||||
const toggleMasterSelection = (): void => {
|
||||
if (allSelected.value) {
|
||||
selectedIds.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIds.value = [...selectableNotificationIds.value];
|
||||
};
|
||||
|
||||
const editItem = (item: notification): void => {
|
||||
target.value = JSON.parse(JSON.stringify(item)) as notification;
|
||||
targetRef.value = item.id ?? undefined;
|
||||
|
|
@ -519,6 +712,50 @@ const deleteItem = async (item: notification): Promise<void> => {
|
|||
await notificationsStore.deleteNotification(item.id);
|
||||
};
|
||||
|
||||
const deleteSelected = async (): Promise<void> => {
|
||||
if (selectedIds.value.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = await confirmDialog({
|
||||
title: 'Delete Selected Notifications',
|
||||
rawHTML:
|
||||
`Delete <strong class="text-red-500">${selectedIds.value.length}</strong> notification target/s?<ul>` +
|
||||
selectedIds.value
|
||||
.map((id) => {
|
||||
const item = filteredTargets.value.find((target) => target.id === id);
|
||||
return item ? `<li>${item.id}: ${item.name}</li>` : '';
|
||||
})
|
||||
.join('') +
|
||||
'</ul>',
|
||||
confirmText: 'Delete',
|
||||
confirmColor: 'error',
|
||||
});
|
||||
|
||||
if (true !== status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsToDelete = filteredTargets.value.filter(
|
||||
(item) => item.id && selectedIds.value.includes(item.id),
|
||||
);
|
||||
if (itemsToDelete.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
massDelete.value = true;
|
||||
|
||||
for (const item of itemsToDelete) {
|
||||
if (!item.id) {
|
||||
continue;
|
||||
}
|
||||
await notificationsStore.deleteNotification(item.id);
|
||||
}
|
||||
|
||||
selectedIds.value = [];
|
||||
massDelete.value = false;
|
||||
};
|
||||
|
||||
const toggleEnabled = async (item: notification): Promise<void> => {
|
||||
if (!item.id) {
|
||||
toast.error('Notification target not found.');
|
||||
|
|
|
|||
|
|
@ -76,6 +76,41 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!isLoading && filteredPresets.length > 0"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ allSelected ? 'Unselect' : 'Select' }}
|
||||
</UButton>
|
||||
|
||||
<UBadge v-if="selectedIds.length > 0" color="error" variant="soft" size="sm">
|
||||
{{ selectedIds.length }}
|
||||
</UBadge>
|
||||
|
||||
<UDropdownMenu :items="bulkActionGroups" :modal="false">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-list"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-toned">{{ filteredPresets.length }} displayed</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="display_style === 'list' && filteredPresets.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
|
|
@ -84,6 +119,14 @@
|
|||
<table class="min-w-190 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
:name="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Preset</th>
|
||||
<th class="w-44 whitespace-nowrap">Actions</th>
|
||||
</tr>
|
||||
|
|
@ -91,6 +134,17 @@
|
|||
|
||||
<tbody class="divide-y divide-default">
|
||||
<tr v-for="item in filteredPresets" :key="item.id" class="hover:bg-muted/20">
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="item.id"
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 align-middle">
|
||||
<div class="space-y-1">
|
||||
<div class="font-semibold text-highlighted">
|
||||
|
|
@ -99,14 +153,20 @@
|
|||
|
||||
<div class="flex flex-wrap items-center gap-3 text-xs text-toned">
|
||||
<span
|
||||
class="inline-flex items-center gap-1"
|
||||
:class="item.cookies ? 'text-info' : ''"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-cookie" class="size-3.5" />
|
||||
<UIcon
|
||||
name="i-lucide-cookie"
|
||||
class="size-3.5"
|
||||
:class="item.cookies ? 'text-success' : ''"
|
||||
/>
|
||||
<span>{{ item.cookies ? 'Has cookies' : 'No cookies' }}</span>
|
||||
</span>
|
||||
|
||||
<span v-if="item.priority > 0" class="inline-flex items-center gap-1">
|
||||
<span
|
||||
v-if="item.priority > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority: {{ item.priority }}</span>
|
||||
</span>
|
||||
|
|
@ -117,7 +177,7 @@
|
|||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
|
|
@ -127,7 +187,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
|
|
@ -137,7 +197,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -154,121 +214,163 @@
|
|||
</div>
|
||||
|
||||
<div v-else-if="filteredPresets.length > 0" class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
<UCard
|
||||
v-for="item in filteredPresets"
|
||||
:key="item.id"
|
||||
class="flex h-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(item.id, 'title')"
|
||||
>
|
||||
<div v-for="item in filteredPresets" :key="item.id" class="min-w-0 w-full max-w-full">
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(item.id, 'title')"
|
||||
>
|
||||
<span :class="['block', expandClass(item.id, 'title')]">
|
||||
{{ prettyName(item.name) }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(item)"
|
||||
>
|
||||
<span class="hidden sm:inline">Export Preset</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="item.id"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<span
|
||||
:class="
|
||||
!isExpanded(item.id, 'title') ? 'block truncate' : 'block whitespace-pre-wrap'
|
||||
"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
{{ prettyName(item.name) }}
|
||||
<UIcon
|
||||
name="i-lucide-cookie"
|
||||
class="size-3.5"
|
||||
:class="item.cookies ? 'text-success' : ''"
|
||||
/>
|
||||
<span>Cookies: {{ item.cookies ? 'Configured' : 'Not set' }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
v-if="item.priority > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority: {{ item.priority }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="item.folder"
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'folder')"
|
||||
>
|
||||
<UIcon name="i-lucide-folder-output" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Download path</div>
|
||||
<span :class="['block', expandClass(item.id, 'folder')]">{{
|
||||
calcPath(item.folder)
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="item.template || item.cli" class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
v-if="item.template"
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!item.cli && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(item.id, 'template')"
|
||||
>
|
||||
<UIcon name="i-lucide-file-code-2" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Output template</div>
|
||||
<span :class="['block', expandClass(item.id, 'template')]">{{
|
||||
item.template
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="item.cli"
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!item.template && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(item.id, 'cli')"
|
||||
>
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">CLI options</div>
|
||||
<span :class="['block', expandClass(item.id, 'cli')]">{{ item.cli }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
v-if="item.description"
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'description')"
|
||||
>
|
||||
<UIcon name="i-lucide-align-left" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Description</div>
|
||||
<span :class="['block', expandClass(item.id, 'description')]">{{
|
||||
item.description
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="info"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="exportItem(item)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
:class="item.cookies ? 'border-info/40 text-info' : ''"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editor.openEdit(item)"
|
||||
>
|
||||
<UIcon name="i-lucide-cookie" class="size-3.5" />
|
||||
<span>{{ item.cookies ? 'Has cookies' : 'No cookies' }}</span>
|
||||
</span>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<span
|
||||
v-if="item.priority > 0"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority {{ item.priority }}</span>
|
||||
</span>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<button
|
||||
v-if="item.folder"
|
||||
type="button"
|
||||
class="flex w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'folder')"
|
||||
>
|
||||
<UIcon name="i-lucide-folder-output" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span :class="expandClass(item.id, 'folder')">{{ calcPath(item.folder) }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="item.template"
|
||||
type="button"
|
||||
class="flex w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'template')"
|
||||
>
|
||||
<UIcon name="i-lucide-file-code-2" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span :class="expandClass(item.id, 'template')">{{ item.template }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="item.cli"
|
||||
type="button"
|
||||
class="flex w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'cli')"
|
||||
>
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span :class="expandClass(item.id, 'cli')">{{ item.cli }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="item.description"
|
||||
type="button"
|
||||
class="flex w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'description')"
|
||||
>
|
||||
<UIcon name="i-lucide-align-left" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span :class="expandClass(item.id, 'description')">{{ item.description }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="editor.openEdit(item)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void deleteItem(item)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
|
|
@ -308,7 +410,8 @@
|
|||
<ul class="list-disc space-y-2 pl-5 text-sm text-default">
|
||||
<li>
|
||||
When you export preset, it doesn't include the cookies field contents for security
|
||||
reasons.
|
||||
reasons. However, there are some CLI options that could contain sensitive data like
|
||||
username or password. Remove them before sharing your preset.
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
|
@ -339,8 +442,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
import type { Preset } from '~/types/presets';
|
||||
import { useDialog } from '~/composables/useDialog';
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { prettyName } from '~/utils';
|
||||
|
||||
|
|
@ -350,6 +456,8 @@ const presetsStore = usePresets();
|
|||
const config = useConfigStore();
|
||||
const box = useConfirm();
|
||||
const editor = usePresetEditor();
|
||||
const { confirmDialog } = useDialog();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
const display_style = useStorage<string>('preset_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
|
|
@ -357,10 +465,11 @@ const isMobile = useMediaQuery({ maxWidth: 1024 });
|
|||
const query = ref('');
|
||||
const showFilter = ref(false);
|
||||
const filterInput = ref<HTMLInputElement | null>(null);
|
||||
const selectedIds = ref<number[]>([]);
|
||||
const massDelete = ref(false);
|
||||
|
||||
const presets = computed(() => presetsStore.presets.value as PresetWithUI[]);
|
||||
const isLoading = presetsStore.isLoading;
|
||||
const expandedItems = ref<Record<string, Set<string>>>({});
|
||||
|
||||
const presetsNoDefault = computed(() => presets.value.filter((item) => !item.default));
|
||||
|
||||
|
|
@ -375,12 +484,47 @@ const filteredPresets = computed<PresetWithUI[]>(() => {
|
|||
);
|
||||
});
|
||||
|
||||
const selectablePresetIds = computed(() =>
|
||||
filteredPresets.value.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
|
||||
const allSelected = computed(
|
||||
() =>
|
||||
selectablePresetIds.value.length > 0 &&
|
||||
selectablePresetIds.value.every((id) => selectedIds.value.includes(id)),
|
||||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
label: 'Remove Selected',
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error',
|
||||
disabled: !hasSelected.value || massDelete.value,
|
||||
onSelect: () => void deleteSelected(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
watch(showFilter, (value) => {
|
||||
if (!value) {
|
||||
query.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
filteredPresets,
|
||||
(items) => {
|
||||
const validIds = new Set(
|
||||
items.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
selectedIds.value = selectedIds.value.filter((id) => validIds.has(id));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const toggleFilterPanel = async (): Promise<void> => {
|
||||
showFilter.value = !showFilter.value;
|
||||
if (!showFilter.value) {
|
||||
|
|
@ -396,6 +540,59 @@ const reloadContent = async (): Promise<void> => {
|
|||
await presetsStore.loadPresets(1, 1000);
|
||||
};
|
||||
|
||||
const toggleMasterSelection = (): void => {
|
||||
if (allSelected.value) {
|
||||
selectedIds.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIds.value = [...selectablePresetIds.value];
|
||||
};
|
||||
|
||||
const deleteSelected = async (): Promise<void> => {
|
||||
if (selectedIds.value.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = await confirmDialog({
|
||||
title: 'Delete Selected Presets',
|
||||
rawHTML:
|
||||
`Delete <strong class="text-red-500">${selectedIds.value.length}</strong> preset/s?<ul>` +
|
||||
selectedIds.value
|
||||
.map((id) => {
|
||||
const item = filteredPresets.value.find((preset) => preset.id === id);
|
||||
return item ? `<li>${item.id}: ${prettyName(item.name)}</li>` : '';
|
||||
})
|
||||
.join('') +
|
||||
'</ul>',
|
||||
confirmText: 'Delete',
|
||||
confirmColor: 'error',
|
||||
});
|
||||
|
||||
if (true !== status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsToDelete = filteredPresets.value.filter(
|
||||
(item) => item.id && selectedIds.value.includes(item.id),
|
||||
);
|
||||
if (itemsToDelete.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
massDelete.value = true;
|
||||
|
||||
for (const item of itemsToDelete) {
|
||||
if (!item.id) {
|
||||
continue;
|
||||
}
|
||||
await presetsStore.deletePreset(item.id);
|
||||
}
|
||||
|
||||
selectedIds.value = [];
|
||||
massDelete.value = false;
|
||||
};
|
||||
|
||||
const deleteItem = async (item: Preset): Promise<void> => {
|
||||
if (true !== (await box.confirm(`Delete preset '${item.name}'?`))) {
|
||||
return;
|
||||
|
|
@ -410,35 +607,6 @@ const toggleDisplayStyle = (): void => {
|
|||
display_style.value = display_style.value === 'list' ? 'grid' : 'list';
|
||||
};
|
||||
|
||||
const toggleExpand = (itemId: number | string | undefined, field: string): void => {
|
||||
if (itemId === undefined || itemId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = String(itemId);
|
||||
if (!expandedItems.value[key]) {
|
||||
expandedItems.value[key] = new Set();
|
||||
}
|
||||
|
||||
if (expandedItems.value[key]?.has(field)) {
|
||||
expandedItems.value[key]?.delete(field);
|
||||
} else {
|
||||
expandedItems.value[key]?.add(field);
|
||||
}
|
||||
};
|
||||
|
||||
const isExpanded = (itemId: number | string | undefined, field: string): boolean => {
|
||||
if (itemId === undefined || itemId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return expandedItems.value[String(itemId)]?.has(field) ?? false;
|
||||
};
|
||||
|
||||
const expandClass = (itemId: number | string | undefined, field: string): string => {
|
||||
return isExpanded(itemId, field) ? 'whitespace-pre-wrap break-words' : 'truncate';
|
||||
};
|
||||
|
||||
const exportItem = (item: Preset): void => {
|
||||
const excludedKeys = ['id', 'default', 'raw', 'cookies', 'toggle_description'];
|
||||
const userData = Object.fromEntries(
|
||||
|
|
|
|||
|
|
@ -93,6 +93,41 @@
|
|||
:description="lastError"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="!isLoading && filteredDefinitions.length > 0"
|
||||
class="flex flex-wrap items-center justify-between gap-3 rounded-lg border border-default bg-default px-3 py-3"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:icon="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
@click="toggleMasterSelection"
|
||||
>
|
||||
{{ allSelected ? 'Unselect' : 'Select' }}
|
||||
</UButton>
|
||||
|
||||
<UBadge v-if="selectedIds.length > 0" color="error" variant="soft" size="sm">
|
||||
{{ selectedIds.length }}
|
||||
</UBadge>
|
||||
|
||||
<UDropdownMenu :items="bulkActionGroups" :modal="false">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
icon="i-lucide-list"
|
||||
trailing-icon="i-lucide-chevron-down"
|
||||
>
|
||||
Actions
|
||||
</UButton>
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-toned">{{ filteredDefinitions.length }} displayed</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="display_style === 'list' && filteredDefinitions.length > 0"
|
||||
class="w-full min-w-0 max-w-full overflow-hidden rounded-lg border border-default bg-default"
|
||||
|
|
@ -101,6 +136,14 @@
|
|||
<table class="min-w-245 w-full text-sm">
|
||||
<thead class="bg-muted/40 text-xs uppercase tracking-wide text-toned">
|
||||
<tr class="text-center [&>th]:px-3 [&>th]:py-3 [&>th]:font-semibold">
|
||||
<th class="w-12">
|
||||
<button type="button" class="cursor-pointer" @click="toggleMasterSelection">
|
||||
<UIcon
|
||||
:name="allSelected ? 'i-lucide-square' : 'i-lucide-square-check-big'"
|
||||
class="size-4"
|
||||
/>
|
||||
</button>
|
||||
</th>
|
||||
<th class="w-full text-left">Definition</th>
|
||||
<th class="w-28 whitespace-nowrap">Priority</th>
|
||||
<th class="w-36 whitespace-nowrap">Updated</th>
|
||||
|
|
@ -114,6 +157,17 @@
|
|||
:key="definition.id"
|
||||
class="hover:bg-muted/20"
|
||||
>
|
||||
<td class="px-3 py-3 text-center align-middle">
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="definition.id"
|
||||
/>
|
||||
</label>
|
||||
</td>
|
||||
|
||||
<td class="px-3 py-3 align-middle">
|
||||
<div class="space-y-1">
|
||||
<div class="font-semibold text-highlighted">
|
||||
|
|
@ -123,7 +177,7 @@
|
|||
<div class="flex flex-wrap items-center gap-3 text-xs text-toned">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 transition hover:bg-muted"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggle(definition)"
|
||||
>
|
||||
<UIcon
|
||||
|
|
@ -134,13 +188,11 @@
|
|||
<span>{{ definition.enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-link" class="size-3.5" />
|
||||
<span
|
||||
>{{ definition.match_url.length }} match pattern{{
|
||||
definition.match_url.length === 1 ? '' : 's'
|
||||
}}</span
|
||||
>
|
||||
<span>Patterns: {{ definition.match_url.length }} match/s</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -161,7 +213,7 @@
|
|||
<td class="w-44 px-3 py-3 align-middle whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
|
|
@ -171,7 +223,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
|
|
@ -181,7 +233,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -201,110 +253,116 @@
|
|||
v-else-if="filteredDefinitions.length > 0"
|
||||
class="grid gap-4 md:grid-cols-2 xl:grid-cols-3"
|
||||
>
|
||||
<UCard
|
||||
<div
|
||||
v-for="definition in filteredDefinitions"
|
||||
:key="definition.id"
|
||||
class="flex h-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
class="min-w-0 w-full max-w-full"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1 space-y-2">
|
||||
<div class="text-sm font-semibold text-highlighted wrap-break-word">
|
||||
{{ definition.name || '(Unnamed definition)' }}
|
||||
<UCard
|
||||
class="flex h-full min-w-0 w-full max-w-full flex-col border bg-default"
|
||||
:ui="{ header: 'p-4 pb-3', body: 'flex flex-1 flex-col gap-4 p-4 pt-0' }"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex min-w-0 items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-start gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(definition.id, 'title')"
|
||||
>
|
||||
<span :class="['block', expandClass(definition.id, 'title')]">
|
||||
{{ definition.name || '(Unnamed)' }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggle(definition)"
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="() => void exportDefinition(definition)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
class="size-3.5"
|
||||
:class="definition.enabled ? 'text-success' : 'text-error'"
|
||||
<span class="hidden sm:inline">Export Definition</span>
|
||||
</UButton>
|
||||
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedIds"
|
||||
class="completed-checkbox size-4 rounded border-default"
|
||||
type="checkbox"
|
||||
:value="definition.id"
|
||||
/>
|
||||
<span>{{ definition.enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority {{ definition.priority }}</span>
|
||||
</span>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-link" class="size-3.5" />
|
||||
<span
|
||||
>{{ definition.match_url.length }} match pattern{{
|
||||
definition.match_url.length === 1 ? '' : 's'
|
||||
}}</span
|
||||
>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-2 text-sm text-default">
|
||||
<div class="flex flex-wrap gap-2 text-xs text-toned *:min-w-32 *:flex-1">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggle(definition)"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
class="size-3.5"
|
||||
:class="definition.enabled ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ definition.enabled ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-list-ordered" class="size-3.5" />
|
||||
<span>Priority: {{ definition.priority }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(definition.id, 'patterns')"
|
||||
>
|
||||
<UIcon name="i-lucide-link" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">URL patterns</div>
|
||||
<span :class="['block', expandClass(definition.id, 'patterns')]">
|
||||
{{ definition.match_url.join('\n') }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="() => void openEdit(definition)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="info"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="() => void exportDefinition(definition)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="space-y-3 text-sm text-default">
|
||||
<div class="rounded-md border border-default bg-muted/20 px-3 py-2">
|
||||
<div
|
||||
class="mb-2 flex items-center gap-2 text-xs font-semibold uppercase tracking-wide text-toned"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void remove(definition)"
|
||||
>
|
||||
<UIcon name="i-lucide-link" class="size-3.5" />
|
||||
<span>Match patterns</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 text-sm">
|
||||
<div
|
||||
v-for="pattern in definition.match_url.slice(0, 3)"
|
||||
:key="pattern"
|
||||
class="truncate text-default"
|
||||
>
|
||||
{{ pattern }}
|
||||
</div>
|
||||
<div v-if="definition.match_url.length > 3" class="text-xs text-toned">
|
||||
+{{ definition.match_url.length - 3 }} more
|
||||
</div>
|
||||
</div>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
@click="() => void openEdit(definition)"
|
||||
>
|
||||
Edit
|
||||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
@click="() => void remove(definition)"
|
||||
>
|
||||
Delete
|
||||
</UButton>
|
||||
</div>
|
||||
</UCard>
|
||||
</UCard>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<UAlert
|
||||
|
|
@ -378,10 +436,12 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { DropdownMenuItem } from '@nuxt/ui';
|
||||
import moment from 'moment';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useStorage } from '@vueuse/core';
|
||||
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import useTaskDefinitionsComposable from '~/composables/useTaskDefinitions';
|
||||
import { useDialog } from '~/composables/useDialog';
|
||||
import { useMediaQuery } from '~/composables/useMediaQuery';
|
||||
|
|
@ -413,6 +473,7 @@ const DEFAULT_DEFINITION: TaskDefinitionDocument = {
|
|||
};
|
||||
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
|
||||
const taskDefs = useTaskDefinitionsComposable();
|
||||
const definitionsRef = taskDefs.definitions;
|
||||
|
|
@ -442,6 +503,8 @@ const query = ref('');
|
|||
const showFilter = ref(false);
|
||||
const filterInput = ref<HTMLInputElement | null>(null);
|
||||
const hideImportByDefault = ref(false);
|
||||
const selectedIds = ref<number[]>([]);
|
||||
const massDelete = ref(false);
|
||||
|
||||
const filteredDefinitions = computed<TaskDefinitionSummary[]>(() => {
|
||||
const normalizedQuery = query.value.trim().toLowerCase();
|
||||
|
|
@ -463,6 +526,32 @@ const filteredDefinitions = computed<TaskDefinitionSummary[]>(() => {
|
|||
});
|
||||
});
|
||||
|
||||
const selectableDefinitionIds = computed(() =>
|
||||
filteredDefinitions.value
|
||||
.map((item) => item.id)
|
||||
.filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
|
||||
const allSelected = computed(
|
||||
() =>
|
||||
selectableDefinitionIds.value.length > 0 &&
|
||||
selectableDefinitionIds.value.every((id) => selectedIds.value.includes(id)),
|
||||
);
|
||||
|
||||
const hasSelected = computed(() => selectedIds.value.length > 0);
|
||||
|
||||
const bulkActionGroups = computed<DropdownMenuItem[][]>(() => [
|
||||
[
|
||||
{
|
||||
label: 'Remove Selected',
|
||||
icon: 'i-lucide-trash',
|
||||
color: 'error',
|
||||
disabled: !hasSelected.value || massDelete.value,
|
||||
onSelect: () => void deleteSelected(),
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const currentSummary = computed<TaskDefinitionSummary | undefined>(() => {
|
||||
if (editorMode.value !== 'edit' || !workingId.value) {
|
||||
return undefined;
|
||||
|
|
@ -495,6 +584,17 @@ watch(showFilter, (value) => {
|
|||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
filteredDefinitions,
|
||||
(items) => {
|
||||
const validIds = new Set(
|
||||
items.map((item) => item.id).filter((id): id is number => typeof id === 'number'),
|
||||
);
|
||||
selectedIds.value = selectedIds.value.filter((id) => validIds.has(id));
|
||||
},
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
const cloneDocument = (document: TaskDefinitionDocument): TaskDefinitionDocument => {
|
||||
return JSON.parse(JSON.stringify(document)) as TaskDefinitionDocument;
|
||||
};
|
||||
|
|
@ -518,6 +618,15 @@ const toggleDisplayStyle = (): void => {
|
|||
display_style.value = display_style.value === 'list' ? 'grid' : 'list';
|
||||
};
|
||||
|
||||
const toggleMasterSelection = (): void => {
|
||||
if (allSelected.value) {
|
||||
selectedIds.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
selectedIds.value = [...selectableDefinitionIds.value];
|
||||
};
|
||||
|
||||
const openCreate = (): void => {
|
||||
editorMode.value = 'create';
|
||||
workingId.value = null;
|
||||
|
|
@ -625,6 +734,47 @@ const remove = async (summary: TaskDefinitionSummary): Promise<void> => {
|
|||
await deleteDefinition(summary.id);
|
||||
};
|
||||
|
||||
const deleteSelected = async (): Promise<void> => {
|
||||
if (selectedIds.value.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { status } = await confirmDialog({
|
||||
title: 'Delete Selected Task Definitions',
|
||||
rawHTML:
|
||||
`Delete <strong class="text-red-500">${selectedIds.value.length}</strong> task definition/s?<ul>` +
|
||||
selectedIds.value
|
||||
.map((id) => {
|
||||
const item = filteredDefinitions.value.find((definition) => definition.id === id);
|
||||
return item ? `<li>${item.id}: ${item.name || '(Unnamed definition)'}</li>` : '';
|
||||
})
|
||||
.join('') +
|
||||
'</ul>',
|
||||
confirmText: 'Delete',
|
||||
confirmColor: 'error',
|
||||
});
|
||||
|
||||
if (true !== status) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemsToDelete = filteredDefinitions.value.filter(
|
||||
(item) => item.id && selectedIds.value.includes(item.id),
|
||||
);
|
||||
if (itemsToDelete.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
massDelete.value = true;
|
||||
|
||||
for (const item of itemsToDelete) {
|
||||
await deleteDefinition(item.id);
|
||||
}
|
||||
|
||||
selectedIds.value = [];
|
||||
massDelete.value = false;
|
||||
};
|
||||
|
||||
const toggle = async (summary: TaskDefinitionSummary): Promise<void> => {
|
||||
await toggleEnabled(summary.id, !summary.enabled);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -184,7 +184,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggleEnabled(item)"
|
||||
@click="() => void toggleFlag(item, 'enabled')"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
|
|
@ -194,55 +194,42 @@
|
|||
<span>{{ item.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggleFlag(item, 'auto_start')"
|
||||
>
|
||||
<UIcon
|
||||
:name="item.auto_start ? 'i-lucide-circle-pause' : 'i-lucide-circle-play'"
|
||||
name="i-lucide-circle-play"
|
||||
class="size-3.5"
|
||||
:class="item.auto_start ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ item.auto_start ? 'Auto' : 'Manual' }}</span>
|
||||
</span>
|
||||
<span>Auto start: {{ item.auto_start ? 'Yes' : 'No' }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggleHandlerEnabled(item)"
|
||||
@click="() => void toggleFlag(item, 'handler_enabled')"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-rss"
|
||||
class="size-3.5"
|
||||
:class="item.handler_enabled !== false ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ item.handler_enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
<span>Handler: {{ item.handler_enabled !== false ? 'On' : 'Off' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-3.5" />
|
||||
Preset:
|
||||
<span class="capitalize">
|
||||
{{ item.preset ?? config.app.default_preset }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1 text-xs text-toned">
|
||||
<div v-if="item.folder" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-folder-output" class="mt-0.5 size-3.5 shrink-0" />
|
||||
<span class="break-all">{{ calcPath(item.folder) }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.template" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-file-code-2" class="mt-0.5 size-3.5 shrink-0" />
|
||||
<span class="break-all">{{ item.template }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="item.cli" class="flex items-start gap-2">
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-3.5 shrink-0" />
|
||||
<span class="break-all">{{ item.cli }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -273,7 +260,7 @@
|
|||
>
|
||||
<span class="inline-flex items-center gap-1 whitespace-nowrap">
|
||||
<UIcon name="i-lucide-triangle-alert" class="size-3.5" />
|
||||
<span>No timer or handler</span>
|
||||
<span>Not configured</span>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
|
|
@ -289,7 +276,7 @@
|
|||
<td class="w-44 px-3 py-3 align-top whitespace-nowrap">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-pencil"
|
||||
|
|
@ -299,7 +286,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
size="xs"
|
||||
icon="i-lucide-trash"
|
||||
|
|
@ -343,10 +330,20 @@
|
|||
<NuxtLink
|
||||
target="_blank"
|
||||
:href="item.url"
|
||||
class="min-w-0 flex-1 truncate text-sm font-semibold text-highlighted hover:underline"
|
||||
class="mt-0.5 shrink-0 text-toned transition hover:text-highlighted"
|
||||
aria-label="Open source URL"
|
||||
>
|
||||
{{ remove_tags(item.name) }}
|
||||
<UIcon name="i-lucide-external-link" class="size-4" />
|
||||
</NuxtLink>
|
||||
<button
|
||||
type="button"
|
||||
class="min-w-0 flex-1 text-left text-sm font-semibold text-highlighted"
|
||||
@click="toggleExpand(item.id, 'title')"
|
||||
>
|
||||
<span :class="['block', expandClass(item.id, 'title')]">
|
||||
{{ remove_tags(item.name) }}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<UIcon
|
||||
v-if="item.id && isTaskInProgress(item.id)"
|
||||
|
|
@ -355,7 +352,10 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center gap-1">
|
||||
<div
|
||||
v-if="get_tags(item.name).length > 0"
|
||||
class="flex flex-wrap items-center gap-1"
|
||||
>
|
||||
<UBadge
|
||||
v-for="tag in get_tags(item.name)"
|
||||
:key="`${item.id}-${tag}`"
|
||||
|
|
@ -370,14 +370,15 @@
|
|||
|
||||
<div class="flex shrink-0 items-center gap-2">
|
||||
<UButton
|
||||
color="info"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-lucide-file-up"
|
||||
square
|
||||
@click="() => void exportItem(item)"
|
||||
/>
|
||||
|
||||
>
|
||||
<span class="hidden sm:inline">Export Task</span>
|
||||
</UButton>
|
||||
<label class="inline-flex cursor-pointer items-center justify-center">
|
||||
<input
|
||||
v-model="selectedElms"
|
||||
|
|
@ -395,7 +396,7 @@
|
|||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggleEnabled(item)"
|
||||
@click="() => void toggleFlag(item, 'enabled')"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-power"
|
||||
|
|
@ -405,39 +406,49 @@
|
|||
<span>{{ item.enabled !== false ? 'Enabled' : 'Disabled' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggleFlag(item, 'auto_start')"
|
||||
>
|
||||
<UIcon
|
||||
:name="item.auto_start ? 'i-lucide-circle-pause' : 'i-lucide-circle-play'"
|
||||
name="i-lucide-circle-play"
|
||||
class="size-3.5"
|
||||
:class="item.auto_start ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ item.auto_start ? 'Auto' : 'Manual' }}</span>
|
||||
</span>
|
||||
<span>Auto start: {{ item.auto_start ? 'Yes' : 'No' }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1 transition hover:border-primary hover:text-default"
|
||||
@click="() => void toggleHandlerEnabled(item)"
|
||||
@click="() => void toggleFlag(item, 'handler_enabled')"
|
||||
>
|
||||
<UIcon
|
||||
name="i-lucide-rss"
|
||||
class="size-3.5"
|
||||
:class="item.handler_enabled !== false ? 'text-success' : 'text-error'"
|
||||
/>
|
||||
<span>{{ item.handler_enabled !== false ? 'Handler on' : 'Handler off' }}</span>
|
||||
<span>Handler: {{ item.handler_enabled !== false ? 'On' : 'Off' }}</span>
|
||||
</button>
|
||||
|
||||
<span
|
||||
class="inline-flex items-center gap-1 rounded-md border border-default px-2 py-1"
|
||||
>
|
||||
<UIcon name="i-lucide-sliders-horizontal" class="size-3.5" />
|
||||
<span class="capitalize">{{ item.preset ?? config.app.default_preset }}</span>
|
||||
<span>Preset: {{ item.preset ?? config.app.default_preset }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border border-default bg-muted/20 px-3 py-2">
|
||||
<div class="flex items-start gap-2 text-sm">
|
||||
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!item.folder && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(item.id, 'schedule')"
|
||||
>
|
||||
<UIcon
|
||||
:name="
|
||||
item.timer
|
||||
|
|
@ -451,58 +462,88 @@
|
|||
/>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Schedule</div>
|
||||
<template v-if="item.timer">
|
||||
<a
|
||||
target="_blank"
|
||||
:href="`https://crontab.guru/#${item.timer.replace(/ /g, '_')}`"
|
||||
class="break-all text-highlighted hover:underline"
|
||||
class="block text-highlighted hover:underline"
|
||||
@click.stop
|
||||
>
|
||||
{{ item.timer }}
|
||||
<span :class="['block', expandClass(item.id, 'schedule')]">
|
||||
{{ item.timer }} ( {{ tryParse(item.timer) }} )
|
||||
</span>
|
||||
</a>
|
||||
<p
|
||||
class="mt-1 text-xs"
|
||||
:class="tryParse(item.timer) === 'Invalid' ? 'text-error' : 'text-toned'"
|
||||
>
|
||||
{{ tryParse(item.timer) }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<p v-else-if="willTaskBeProcessed(item)" class="text-toned whitespace-nowrap">
|
||||
<p
|
||||
v-else-if="willTaskBeProcessed(item)"
|
||||
:class="['text-sm text-default', expandClass(item.id, 'schedule')]"
|
||||
>
|
||||
Handler only
|
||||
</p>
|
||||
<p v-else class="text-error whitespace-nowrap">No timer or handler</p>
|
||||
<p v-else :class="['text-sm text-error', expandClass(item.id, 'schedule')]">
|
||||
Not configured
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="item.folder" class="rounded-md border border-default bg-muted/20 px-3 py-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<button
|
||||
v-if="item.folder"
|
||||
type="button"
|
||||
class="flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left"
|
||||
@click="toggleExpand(item.id, 'folder')"
|
||||
>
|
||||
<UIcon name="i-lucide-folder-output" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span class="break-all text-toned">{{ calcPath(item.folder) }}</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Download path</div>
|
||||
<span :class="['block', expandClass(item.id, 'folder')]">
|
||||
{{ calcPath(item.folder) }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.template"
|
||||
class="rounded-md border border-default bg-muted/20 px-3 py-2"
|
||||
>
|
||||
<div class="flex items-start gap-2">
|
||||
<div v-if="item.template || item.cli" class="grid grid-cols-1 gap-2 sm:grid-cols-2">
|
||||
<button
|
||||
v-if="item.template"
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!item.cli && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(item.id, 'template')"
|
||||
>
|
||||
<UIcon name="i-lucide-file-code-2" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span class="break-all text-toned">{{ item.template }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">Output template</div>
|
||||
<span :class="['block', expandClass(item.id, 'template')]">{{
|
||||
item.template
|
||||
}}</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="item.cli" class="rounded-md border border-default bg-muted/20 px-3 py-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<button
|
||||
v-if="item.cli"
|
||||
type="button"
|
||||
:class="[
|
||||
'flex min-w-0 w-full items-start gap-2 rounded-md border border-default bg-muted/20 px-3 py-2 text-left',
|
||||
!item.template && 'sm:col-span-2',
|
||||
]"
|
||||
@click="toggleExpand(item.id, 'cli')"
|
||||
>
|
||||
<UIcon name="i-lucide-terminal" class="mt-0.5 size-4 shrink-0 text-toned" />
|
||||
<span class="break-all text-toned">{{ item.cli }}</span>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="text-xs font-medium text-toned">CLI options</div>
|
||||
<span :class="['block', expandClass(item.id, 'cli')]">{{ item.cli }}</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto flex flex-wrap gap-2 pt-2 *:min-w-32 *:flex-1">
|
||||
<UButton
|
||||
color="warning"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-pencil"
|
||||
class="w-full justify-center"
|
||||
|
|
@ -512,7 +553,7 @@
|
|||
</UButton>
|
||||
|
||||
<UButton
|
||||
color="error"
|
||||
color="neutral"
|
||||
variant="outline"
|
||||
icon="i-lucide-trash"
|
||||
class="w-full justify-center"
|
||||
|
|
@ -642,6 +683,7 @@ import type { DropdownMenuItem } from '@nuxt/ui';
|
|||
import { useStorage } from '@vueuse/core';
|
||||
import { CronExpressionParser } from 'cron-parser';
|
||||
import { useConfirm } from '~/composables/useConfirm';
|
||||
import { useExpandableMeta } from '~/composables/useExpandableMeta';
|
||||
import { useTasks } from '~/composables/useTasks';
|
||||
import TaskInspect from '~/components/TaskInspect.vue';
|
||||
import type { ExportedTask, Task } from '~/types/tasks';
|
||||
|
|
@ -657,6 +699,7 @@ const socket = useSocketStore();
|
|||
const stateStore = useStateStore();
|
||||
const { confirmDialog } = useDialog();
|
||||
const sessionCache = useSessionCache();
|
||||
const { toggleExpand, expandClass } = useExpandableMeta();
|
||||
const display_style = useStorage<'list' | 'grid' | 'cards'>('tasks_display_style', 'grid');
|
||||
const isMobile = useMediaQuery({ maxWidth: 1024 });
|
||||
|
||||
|
|
@ -956,34 +999,23 @@ const deleteItem = async (item: Task) => {
|
|||
await tasksComposable.deleteTask(item.id);
|
||||
};
|
||||
|
||||
const toggleEnabled = async (item: Task) => {
|
||||
const toggleFlag = async (item: Task, field: 'enabled' | 'auto_start' | 'handler_enabled') => {
|
||||
if (!item.id) {
|
||||
toast.error('Task ID is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await tasksComposable.patchTask(item.id, { enabled: !item.enabled });
|
||||
const currentValue = item[field] !== false;
|
||||
const updated = await tasksComposable.patchTask(item.id, { [field]: !currentValue });
|
||||
|
||||
if (updated) {
|
||||
item.enabled = updated.enabled;
|
||||
if (updated.enabled) {
|
||||
item[field] = updated[field];
|
||||
|
||||
if (field === 'enabled' && updated.enabled) {
|
||||
await checkHandlerSupport(updated);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleHandlerEnabled = async (item: Task) => {
|
||||
if (!item.id) {
|
||||
toast.error('Task ID is missing');
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await tasksComposable.patchTask(item.id, {
|
||||
handler_enabled: !item.handler_enabled,
|
||||
});
|
||||
|
||||
if (updated) {
|
||||
item.handler_enabled = updated.handler_enabled;
|
||||
if (updated.handler_enabled) {
|
||||
if (field === 'handler_enabled' && updated.handler_enabled) {
|
||||
await checkHandlerSupport(updated);
|
||||
}
|
||||
}
|
||||
|
|
@ -1302,7 +1334,6 @@ const itemActionGroups = (item: Task): DropdownMenuItem[][] => [
|
|||
{
|
||||
label: 'Run now',
|
||||
icon: 'i-lucide-square-play',
|
||||
color: 'primary',
|
||||
onSelect: () => void runNow(item),
|
||||
},
|
||||
{
|
||||
|
|
@ -1336,7 +1367,6 @@ const itemActionGroups = (item: Task): DropdownMenuItem[][] => [
|
|||
{
|
||||
label: 'Export Task',
|
||||
icon: 'i-lucide-file-up',
|
||||
color: 'info',
|
||||
onSelect: () => void exportItem(item),
|
||||
},
|
||||
],
|
||||
|
|
|
|||
Loading…
Reference in a new issue