refac
This commit is contained in:
@@ -63,7 +63,7 @@
|
||||
|
||||
<div class="">
|
||||
<div
|
||||
class="flex gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
||||
class="flex gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent pt-1 touch-auto pointer-events-auto"
|
||||
>
|
||||
<a
|
||||
draggable="false"
|
||||
|
||||
@@ -6,12 +6,15 @@
|
||||
import { WEBUI_NAME, mobile, showSidebar, user, config } from '$lib/stores';
|
||||
|
||||
import {
|
||||
createAutomation,
|
||||
getAutomationItems,
|
||||
toggleAutomationById,
|
||||
runAutomationById,
|
||||
deleteAutomationById,
|
||||
type AutomationForm,
|
||||
type AutomationResponse
|
||||
} from '$lib/apis/automations';
|
||||
import fileSaver from 'file-saver';
|
||||
|
||||
import AutomationModal from '$lib/components/AutomationModal.svelte';
|
||||
import AutomationMenu from '$lib/components/automations/AutomationMenu.svelte';
|
||||
@@ -19,7 +22,7 @@
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Pagination from '$lib/components/common/Pagination.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import SplitCreateButton from '$lib/components/common/SplitCreateButton.svelte';
|
||||
import Switch from '$lib/components/common/Switch.svelte';
|
||||
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
||||
import Search from '$lib/components/icons/Search.svelte';
|
||||
@@ -33,6 +36,8 @@
|
||||
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
|
||||
import Minus from '$lib/components/icons/Minus.svelte';
|
||||
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let loaded = false;
|
||||
@@ -51,6 +56,8 @@
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
let page = 1;
|
||||
let importFiles: FileList | null = null;
|
||||
let automationsImportInputElement: HTMLInputElement;
|
||||
|
||||
const handleSearchInput = () => {
|
||||
if (!loaded) return;
|
||||
@@ -157,6 +164,69 @@
|
||||
showCreateModal = true;
|
||||
};
|
||||
|
||||
const getAllAutomations = async () => {
|
||||
let currentPage = 1;
|
||||
let allAutomations: AutomationResponse[] = [];
|
||||
let totalAutomations = 0;
|
||||
|
||||
do {
|
||||
const res = await getAutomationItems(localStorage.token, null, 'all', currentPage);
|
||||
const pageItems = res?.items ?? [];
|
||||
totalAutomations = res?.total ?? pageItems.length;
|
||||
allAutomations = [...allAutomations, ...pageItems];
|
||||
currentPage += 1;
|
||||
|
||||
if (pageItems.length === 0) break;
|
||||
} while (allAutomations.length < totalAutomations);
|
||||
|
||||
return allAutomations;
|
||||
};
|
||||
|
||||
const toAutomationForm = (automation: AutomationResponse): AutomationForm => ({
|
||||
name: automation.name,
|
||||
data: automation.data,
|
||||
meta: automation.meta ?? undefined,
|
||||
is_active: automation.is_active
|
||||
});
|
||||
|
||||
const exportAutomations = async () => {
|
||||
const allAutomations = await getAllAutomations().catch((err) => {
|
||||
toast.error(`${err}`);
|
||||
return null;
|
||||
});
|
||||
|
||||
if (!allAutomations) return;
|
||||
|
||||
const blob = new Blob([JSON.stringify(allAutomations.map(toAutomationForm), null, 2)], {
|
||||
type: 'application/json'
|
||||
});
|
||||
saveAs(blob, `automations-export-${Date.now()}.json`);
|
||||
};
|
||||
|
||||
const importAutomations = async (file: File) => {
|
||||
const text = await file.text();
|
||||
const savedAutomations = JSON.parse(text);
|
||||
const automationItems = Array.isArray(savedAutomations)
|
||||
? savedAutomations
|
||||
: (savedAutomations?.automations ?? []);
|
||||
|
||||
if (!Array.isArray(automationItems)) {
|
||||
throw new Error($i18n.t('Invalid JSON format'));
|
||||
}
|
||||
|
||||
for (const automation of automationItems) {
|
||||
await createAutomation(localStorage.token, {
|
||||
name: automation.name,
|
||||
data: automation.data,
|
||||
meta: automation.meta ?? undefined,
|
||||
is_active: automation.is_active ?? true
|
||||
});
|
||||
}
|
||||
|
||||
page = 1;
|
||||
await getAutomationList();
|
||||
};
|
||||
|
||||
const formatRRule = (rrule: string): string => {
|
||||
// Detect one-time schedule (ONCE)
|
||||
if (rrule.includes('COUNT=1')) {
|
||||
@@ -240,6 +310,28 @@
|
||||
</div>
|
||||
</DeleteConfirmDialog>
|
||||
|
||||
<input
|
||||
id="automations-import-input"
|
||||
bind:this={automationsImportInputElement}
|
||||
bind:files={importFiles}
|
||||
type="file"
|
||||
accept=".json"
|
||||
hidden
|
||||
on:change={async () => {
|
||||
if (!importFiles || importFiles.length === 0) return;
|
||||
|
||||
try {
|
||||
await importAutomations(importFiles[0]);
|
||||
toast.success($i18n.t('Imported automations successfully'));
|
||||
} catch (error) {
|
||||
toast.error(`${error}`);
|
||||
} finally {
|
||||
importFiles = null;
|
||||
automationsImportInputElement.value = '';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<AutomationModal
|
||||
bind:show={showCreateModal}
|
||||
automation={null}
|
||||
@@ -291,18 +383,28 @@
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<button
|
||||
class="ml-1 px-2 py-1.5 text-xs gap-1 rounded-xl bg-black text-white dark:bg-white dark:text-black transition flex items-center"
|
||||
on:click={() => {
|
||||
cloneFrom = null;
|
||||
showCreateModal = true;
|
||||
}}
|
||||
>
|
||||
<Plus className="size-3" strokeWidth="2.5" />
|
||||
<div class="hidden md:block md:ml-1 text-xs">
|
||||
{$i18n.t('New Automation')}
|
||||
</div>
|
||||
</button>
|
||||
<SplitCreateButton
|
||||
actions={[
|
||||
{
|
||||
id: 'automations-new',
|
||||
label: $i18n.t('Create'),
|
||||
onClick: () => {
|
||||
cloneFrom = null;
|
||||
showCreateModal = true;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'automations-import',
|
||||
label: $i18n.t('Import JSON'),
|
||||
onClick: () => automationsImportInputElement?.click()
|
||||
},
|
||||
{
|
||||
id: 'automations-export',
|
||||
label: $i18n.t('Export JSON'),
|
||||
onClick: exportAutomations
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -417,11 +519,13 @@
|
||||
</div>
|
||||
|
||||
{#if automations === null || loading}
|
||||
<div class="w-full h-full flex justify-center items-center my-16 mb-24">
|
||||
<div class="flex min-h-[calc(100dvh-13rem)] w-full items-center justify-center">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{:else if (automations ?? []).length === 0}
|
||||
<div class="w-full h-full flex flex-col justify-center items-center my-16 mb-24">
|
||||
<div
|
||||
class="flex min-h-[calc(100dvh-13rem)] w-full flex-col items-center justify-center"
|
||||
>
|
||||
<div class="max-w-md text-center">
|
||||
<div class="text-3xl mb-3">⚡</div>
|
||||
<div class="text-lg font-normal mb-1">
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
import CalendarEventModal from '$lib/components/calendar/CalendarEventModal.svelte';
|
||||
import CreateCalendarModal from '$lib/components/calendar/CreateCalendarModal.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
import Plus from '$lib/components/icons/Plus.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
||||
import Select from '$lib/components/common/Select.svelte';
|
||||
@@ -331,24 +330,10 @@
|
||||
</Select>
|
||||
|
||||
<button
|
||||
class="ml-1 px-2 py-1.5 text-xs gap-1 rounded-xl bg-black text-white dark:bg-white dark:text-black transition text-sm flex items-center"
|
||||
class="ml-1 rounded-lg bg-gray-50 px-2.5 py-1 text-xs text-gray-900 transition ring-1 ring-gray-200 hover:bg-gray-100 dark:bg-gray-850 dark:text-gray-100 dark:ring-gray-800 dark:hover:bg-gray-800"
|
||||
on:click={handleNewEvent}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2.5"
|
||||
stroke="currentColor"
|
||||
class="size-3"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12 4.5v15m7.5-7.5h-15"
|
||||
/></svg
|
||||
>
|
||||
|
||||
<span class="hidden sm:inline">{$i18n.t('New Event')}</span>
|
||||
{$i18n.t('Create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,23 +19,15 @@
|
||||
import { getSkillItems } from '$lib/apis/skills';
|
||||
import { getToolList } from '$lib/apis/tools';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Dropdown from '$lib/components/common/Dropdown.svelte';
|
||||
import DropdownMenu from '$lib/components/common/DropdownMenu.svelte';
|
||||
import Sidebar from '$lib/components/icons/Sidebar.svelte';
|
||||
import ChevronDown from '$lib/components/icons/ChevronDown.svelte';
|
||||
import DocumentArrowDown from '$lib/components/icons/DocumentArrowDown.svelte';
|
||||
import DocumentArrowUp from '$lib/components/icons/DocumentArrowUp.svelte';
|
||||
import Link from '$lib/components/icons/Link.svelte';
|
||||
import Pencil from '$lib/components/icons/Pencil.svelte';
|
||||
import SplitCreateButton from '$lib/components/common/SplitCreateButton.svelte';
|
||||
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
let loaded = false;
|
||||
let lastPath = '';
|
||||
let activeWorkspaceSection = '';
|
||||
let showCreateMenu = false;
|
||||
let visibleActions = [];
|
||||
let primaryCreateAction = null;
|
||||
|
||||
$: if ($page.url.pathname !== lastPath) {
|
||||
lastPath = $page.url.pathname;
|
||||
@@ -48,28 +40,9 @@
|
||||
|
||||
$: activeWorkspaceSection = $page.url.pathname.split('/')[2] ?? '';
|
||||
$: visibleActions = $workspaceActions.filter((action) => action.visible ?? true);
|
||||
$: primaryCreateAction =
|
||||
visibleActions.find((action) => action.id.endsWith('-new')) ?? visibleActions[0] ?? null;
|
||||
|
||||
const getCount = (res: any) => res?.total ?? (Array.isArray(res) ? res.length : null);
|
||||
|
||||
const runWorkspaceAction = async (action) => {
|
||||
if (!action) return;
|
||||
if (action.href) {
|
||||
await goto(action.href);
|
||||
} else {
|
||||
await action.onClick?.();
|
||||
}
|
||||
};
|
||||
|
||||
const getActionIcon = (id: string) => {
|
||||
if (id.endsWith('-new')) return Pencil;
|
||||
if (id.endsWith('-import-link')) return Link;
|
||||
if (id.endsWith('-import')) return DocumentArrowUp;
|
||||
if (id.endsWith('-export')) return DocumentArrowDown;
|
||||
return Pencil;
|
||||
};
|
||||
|
||||
const loadWorkspaceCounts = async () => {
|
||||
const canViewModels = $user?.role === 'admin' || $user?.permissions?.workspace?.models;
|
||||
const canViewKnowledge = $user?.role === 'admin' || $user?.permissions?.workspace?.knowledge;
|
||||
@@ -168,7 +141,7 @@
|
||||
|
||||
<div class="flex w-full items-center">
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
||||
class="flex min-w-0 mr-1.5 items-center gap-0.5 md:gap-1 scrollbar-none overflow-x-auto w-fit text-center text-sm font-normal rounded-full bg-transparent py-1 touch-auto pointer-events-auto"
|
||||
>
|
||||
{#if $user?.role === 'admin' || $user?.permissions?.workspace?.models}
|
||||
<a
|
||||
@@ -257,58 +230,7 @@
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex shrink-0 items-center gap-1">
|
||||
{#if visibleActions.length}
|
||||
<div
|
||||
class="ml-1 flex overflow-hidden rounded-lg bg-gray-50 text-xs text-gray-900 transition ring-1 ring-gray-200 dark:bg-gray-850 dark:text-gray-100 dark:ring-gray-800"
|
||||
>
|
||||
<button
|
||||
class="px-2.5 py-1 transition hover:bg-gray-100 dark:hover:bg-gray-800"
|
||||
on:click={() => {
|
||||
runWorkspaceAction(primaryCreateAction);
|
||||
}}
|
||||
>
|
||||
{$i18n.t('Create')}
|
||||
</button>
|
||||
|
||||
<Dropdown bind:show={showCreateMenu} align="end" sideOffset={6}>
|
||||
<button
|
||||
class="flex items-center border-l border-gray-200 px-1.5 py-1 transition hover:bg-gray-100 dark:border-gray-800 dark:hover:bg-gray-800"
|
||||
aria-label={$i18n.t('Open create menu')}
|
||||
>
|
||||
<ChevronDown className="size-2.5" strokeWidth="2.5" />
|
||||
</button>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="min-w-[170px]">
|
||||
{#each visibleActions as action (action.id)}
|
||||
{@const Icon = getActionIcon(action.id)}
|
||||
{#if action.href}
|
||||
<a
|
||||
href={action.href}
|
||||
on:click={() => {
|
||||
showCreateMenu = false;
|
||||
}}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
<span class="self-center truncate">{action.label}</span>
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
on:click={async () => {
|
||||
await action.onClick?.();
|
||||
showCreateMenu = false;
|
||||
}}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
<span class="self-center truncate">{action.label}</span>
|
||||
</button>
|
||||
{/if}
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
{/if}
|
||||
<SplitCreateButton actions={visibleActions} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user