refac
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
<script lang="ts">
|
||||
import { getContext, onMount, setContext } from 'svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
import { config, mobile, showSidebar, user } from '$lib/stores';
|
||||
import { getAutomationItems } from '$lib/apis/automations';
|
||||
import type i18nType from '$lib/i18n';
|
||||
import { formatNumber } from '$lib/utils';
|
||||
import SidebarIcon from '$lib/components/icons/Sidebar.svelte';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
|
||||
const i18n: typeof i18nType = getContext('i18n');
|
||||
|
||||
const total = writable<number | null>(null);
|
||||
|
||||
let itemName: string | null = null;
|
||||
let HeaderActions: any = null;
|
||||
let headerActionProps: Record<string, any> = {};
|
||||
|
||||
const setHeader = ({
|
||||
itemName: nextItemName = null,
|
||||
actions = null,
|
||||
actionProps = {}
|
||||
}: {
|
||||
itemName?: string | null;
|
||||
actions?: any;
|
||||
actionProps?: Record<string, any>;
|
||||
}) => {
|
||||
itemName = nextItemName;
|
||||
HeaderActions = actions;
|
||||
headerActionProps = actionProps;
|
||||
};
|
||||
|
||||
const clearHeader = () => {
|
||||
itemName = null;
|
||||
HeaderActions = null;
|
||||
headerActionProps = {};
|
||||
};
|
||||
|
||||
const refreshCount = async () => {
|
||||
const res = await getAutomationItems(localStorage.token, null, 'all', 1).catch(() => null);
|
||||
if (res) {
|
||||
total.set(res.total);
|
||||
}
|
||||
};
|
||||
|
||||
setContext('automationsLayout', {
|
||||
total,
|
||||
setTotal: (value: number | null) => total.set(value),
|
||||
refreshCount,
|
||||
setHeader,
|
||||
clearHeader
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
if (
|
||||
!($config?.features as any)?.enable_automations ||
|
||||
($user?.role !== 'admin' && !($user?.permissions?.features?.automations ?? false))
|
||||
) {
|
||||
goto('/');
|
||||
return;
|
||||
}
|
||||
|
||||
await refreshCount();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex flex-col w-full h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
|
||||
? 'md:max-w-[calc(100%-var(--sidebar-width))]'
|
||||
: ''} max-w-full"
|
||||
>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<div class="shrink-0 px-2.5 pt-2 pb-1">
|
||||
<div class="flex items-center gap-0.5 md:gap-1">
|
||||
{#if $mobile}
|
||||
<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center">
|
||||
<Tooltip content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}>
|
||||
<button
|
||||
id="sidebar-toggle-button"
|
||||
class="flex size-7 items-center justify-center text-gray-400 transition"
|
||||
aria-label={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
|
||||
on:click={() => {
|
||||
showSidebar.set(!$showSidebar);
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<SidebarIcon className="size-4" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex w-full min-w-0 items-center">
|
||||
<div class="flex min-w-0 flex-1 items-center gap-1 py-1">
|
||||
{#if itemName}
|
||||
<button
|
||||
class="min-w-fit px-1 text-sm select-none transition"
|
||||
aria-label={$i18n.t('Back')}
|
||||
on:click={() => goto('/automations')}
|
||||
type="button"
|
||||
>
|
||||
{$i18n.t('Automations')}
|
||||
</button>
|
||||
{:else}
|
||||
<span class="min-w-fit px-1 text-sm select-none">{$i18n.t('Automations')}</span>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-500 dark:text-gray-500">
|
||||
{$total === null ? '' : formatNumber($total)}
|
||||
</span>
|
||||
{#if itemName}
|
||||
<span class="text-sm text-gray-300 dark:text-gray-800 px-2">/</span>
|
||||
<span class="min-w-0 flex-1 truncate text-sm text-gray-900 dark:text-white">
|
||||
{itemName}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if HeaderActions}
|
||||
<svelte:component this={HeaderActions} {...headerActionProps} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-h-0 flex-1 overflow-hidden">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { WEBUI_NAME, mobile, showSidebar, user, config } from '$lib/stores';
|
||||
import { WEBUI_NAME, user, config } from '$lib/stores';
|
||||
|
||||
import {
|
||||
createAutomation,
|
||||
@@ -14,17 +14,18 @@
|
||||
type AutomationForm,
|
||||
type AutomationResponse
|
||||
} from '$lib/apis/automations';
|
||||
import type i18nType from '$lib/i18n';
|
||||
// @ts-ignore file-saver ships without local typings in this repo.
|
||||
import fileSaver from 'file-saver';
|
||||
|
||||
import AutomationModal from '$lib/components/AutomationModal.svelte';
|
||||
import AutomationListHeaderActions from '$lib/components/automations/AutomationListHeaderActions.svelte';
|
||||
import AutomationMenu from '$lib/components/automations/AutomationMenu.svelte';
|
||||
import DeleteConfirmDialog from '$lib/components/common/ConfirmDialog.svelte';
|
||||
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 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';
|
||||
import XMark from '$lib/components/icons/XMark.svelte';
|
||||
import EllipsisHorizontal from '$lib/components/icons/EllipsisHorizontal.svelte';
|
||||
@@ -35,11 +36,11 @@
|
||||
import Check from '$lib/components/icons/Check.svelte';
|
||||
import CheckCircle from '$lib/components/icons/CheckCircle.svelte';
|
||||
import Minus from '$lib/components/icons/Minus.svelte';
|
||||
import { formatNumber } from '$lib/utils';
|
||||
|
||||
const { saveAs } = fileSaver;
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
const i18n: typeof i18nType = getContext('i18n');
|
||||
const automationsLayout: any = getContext('automationsLayout');
|
||||
|
||||
let loaded = false;
|
||||
let automations: AutomationResponse[] | null = null;
|
||||
@@ -61,6 +62,35 @@
|
||||
let importFiles: FileList | null = null;
|
||||
let automationsImportInputElement: HTMLInputElement;
|
||||
|
||||
const syncHeader = () => {
|
||||
automationsLayout?.setHeader({
|
||||
itemName: null,
|
||||
actions: AutomationListHeaderActions,
|
||||
actionProps: {
|
||||
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
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSearchInput = () => {
|
||||
if (!loaded) return;
|
||||
|
||||
@@ -99,6 +129,7 @@
|
||||
if (res) {
|
||||
automations = res.items;
|
||||
total = res.total;
|
||||
automationsLayout?.setTotal(total);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
@@ -275,9 +306,9 @@
|
||||
return 'th';
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
onMount(() => {
|
||||
if (
|
||||
!$config?.features?.enable_automations ||
|
||||
!($config?.features as any)?.enable_automations ||
|
||||
($user?.role !== 'admin' && !($user?.permissions?.features?.automations ?? false))
|
||||
) {
|
||||
goto('/');
|
||||
@@ -285,6 +316,7 @@
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
syncHeader();
|
||||
|
||||
return () => {
|
||||
clearTimeout(searchDebounceTimer);
|
||||
@@ -338,313 +370,242 @@
|
||||
bind:show={showCreateModal}
|
||||
automation={null}
|
||||
{cloneFrom}
|
||||
on:save={(e) => {
|
||||
getAutomationList();
|
||||
on:save={async (e) => {
|
||||
await getAutomationList();
|
||||
if (e.detail?.id) {
|
||||
goto(`/automations/${e.detail.id}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<div
|
||||
class="flex flex-col w-full h-screen max-h-[100dvh] transition-width duration-200 ease-in-out {$showSidebar
|
||||
? 'md:max-w-[calc(100%-var(--sidebar-width))]'
|
||||
: ''} max-w-full"
|
||||
>
|
||||
<div class="flex-1 max-h-full overflow-y-auto">
|
||||
{#if loaded}
|
||||
<div class="pb-1 px-2.5 pt-2">
|
||||
<div class="flex items-center gap-0.5 md:gap-1 mb-1">
|
||||
{#if $mobile}
|
||||
<div class="{$showSidebar ? 'md:hidden' : ''} flex flex-none items-center">
|
||||
<Tooltip
|
||||
content={$showSidebar ? $i18n.t('Close Sidebar') : $i18n.t('Open Sidebar')}
|
||||
interactive={true}
|
||||
<div class="h-full overflow-y-auto px-2.5 pb-1">
|
||||
{#if loaded}
|
||||
<div class="space-y-1">
|
||||
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
||||
<div class="flex min-w-0 flex-1 items-center">
|
||||
<div class="self-center ml-1 mr-3">
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
class="w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
on:input={handleSearchInput}
|
||||
aria-label={$i18n.t('Search Automations')}
|
||||
placeholder={$i18n.t('Search Automations')}
|
||||
maxlength="500"
|
||||
/>
|
||||
|
||||
{#if query}
|
||||
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||
<button
|
||||
class="p-0.5 rounded-full transition"
|
||||
aria-label={$i18n.t('Clear search')}
|
||||
on:click={() => {
|
||||
query = '';
|
||||
handleSearchInput();
|
||||
}}
|
||||
>
|
||||
<button
|
||||
id="sidebar-toggle-button"
|
||||
class="cursor-pointer flex rounded-lg hover:bg-gray-100 dark:hover:bg-gray-850 transition"
|
||||
on:click={() => {
|
||||
showSidebar.set(!$showSidebar);
|
||||
}}
|
||||
>
|
||||
<div class="self-center p-1.5">
|
||||
<SidebarIcon className="size-4" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
<XMark className="size-3" strokeWidth="2" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex w-full items-center">
|
||||
<div class="flex items-center gap-1 py-1 min-w-0">
|
||||
<span class="min-w-fit px-1 text-sm select-none">{$i18n.t('Automations')}</span>
|
||||
<span class="text-sm text-gray-500 dark:text-gray-500">
|
||||
{total === null ? '' : formatNumber(total)}
|
||||
</span>
|
||||
<div
|
||||
class="flex max-w-[55%] shrink-0 overflow-x-auto scrollbar-none"
|
||||
on:wheel={(e) => {
|
||||
if (e.deltaY !== 0) {
|
||||
e.preventDefault();
|
||||
e.currentTarget.scrollLeft += e.deltaY;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="flex w-fit gap-0.5 text-center text-sm rounded-full bg-transparent whitespace-nowrap"
|
||||
>
|
||||
<Select
|
||||
bind:value={statusFilter}
|
||||
align="end"
|
||||
items={[
|
||||
{ value: 'all', label: $i18n.t('All') },
|
||||
{ value: 'active', label: $i18n.t('Active') },
|
||||
{ value: 'paused', label: $i18n.t('Paused') }
|
||||
]}
|
||||
onChange={() => {
|
||||
page = 1;
|
||||
}}
|
||||
triggerClass="relative h-8 w-full flex items-center gap-0.5 px-1.5 py-1.5 bg-transparent rounded-xl text-[13px] font-normal text-gray-700 transition dark:text-gray-200"
|
||||
>
|
||||
<svelte:fragment slot="trigger" let:selectedLabel>
|
||||
<span
|
||||
class="inline-flex h-input w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
|
||||
>
|
||||
{selectedLabel}
|
||||
</span>
|
||||
<ChevronDown className="size-3.5" strokeWidth="2.5" />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="item" let:item let:selected>
|
||||
{item.label}
|
||||
<div class="ml-auto {selected ? '' : 'invisible'}">
|
||||
<Check />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Select>
|
||||
|
||||
<Dropdown align="end">
|
||||
<Tooltip content={$i18n.t('Actions')}>
|
||||
<button
|
||||
class="flex h-8 items-center gap-1.5 rounded-xl bg-transparent px-1.5 text-[13px] font-normal text-gray-700 transition dark:text-gray-200"
|
||||
type="button"
|
||||
>
|
||||
<span>{$i18n.t('Actions')}</span>
|
||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="w-[170px] shadow-sm">
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px]"
|
||||
type="button"
|
||||
on:click={() => bulkToggleHandler(true)}
|
||||
>
|
||||
<CheckCircle className="size-3.5" />
|
||||
{$i18n.t('Enable All')}
|
||||
</button>
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px]"
|
||||
type="button"
|
||||
on:click={() => bulkToggleHandler(false)}
|
||||
>
|
||||
<Minus className="size-3.5" />
|
||||
{$i18n.t('Disable All')}
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if automations === null || loading}
|
||||
<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="flex min-h-[calc(100dvh-13rem)] w-full flex-col items-center justify-center">
|
||||
<div class="max-w-sm text-center text-gray-900 dark:text-gray-100">
|
||||
<div class="mb-1.5 text-sm">
|
||||
{query ? $i18n.t('No results found') : $i18n.t('No automations found')}
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-1">
|
||||
<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 class="text-center text-xs leading-5 text-gray-500">
|
||||
{query
|
||||
? $i18n.t('Try adjusting your search or filter to find what you are looking for.')
|
||||
: $i18n.t('Create scheduled prompts that run automatically on a recurring basis.')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="flex h-8 flex-1 items-center w-full gap-2">
|
||||
<div class="flex min-w-0 flex-1 items-center">
|
||||
<div class="self-center ml-1 mr-3">
|
||||
<Search className="size-3.5" />
|
||||
</div>
|
||||
<input
|
||||
class="w-full text-sm py-1 rounded-r-xl outline-hidden bg-transparent"
|
||||
bind:value={query}
|
||||
on:input={handleSearchInput}
|
||||
aria-label={$i18n.t('Search Automations')}
|
||||
placeholder={$i18n.t('Search Automations')}
|
||||
maxlength="500"
|
||||
/>
|
||||
|
||||
{#if query}
|
||||
<div class="self-center pl-1.5 translate-y-[0.5px] rounded-l-xl bg-transparent">
|
||||
<button
|
||||
class="p-0.5 rounded-full hover:bg-gray-100 dark:hover:bg-gray-900 transition"
|
||||
aria-label={$i18n.t('Clear search')}
|
||||
on:click={() => {
|
||||
query = '';
|
||||
handleSearchInput();
|
||||
}}
|
||||
>
|
||||
<XMark className="size-3" strokeWidth="2" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{:else}
|
||||
<div class="gap-y-0.5 grid my-1">
|
||||
{#each automations as automation (automation.id)}
|
||||
<div
|
||||
class="flex max-w-[55%] shrink-0 overflow-x-auto scrollbar-none"
|
||||
on:wheel={(e) => {
|
||||
if (e.deltaY !== 0) {
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={$i18n.t('Open automation')}
|
||||
class="group flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1 text-left transition"
|
||||
on:click={() => {
|
||||
goto(`/automations/${automation.id}`);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
e.currentTarget.scrollLeft += e.deltaY;
|
||||
goto(`/automations/${automation.id}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="flex w-fit gap-0.5 text-center text-sm rounded-full bg-transparent whitespace-nowrap"
|
||||
>
|
||||
<Select
|
||||
bind:value={statusFilter}
|
||||
align="end"
|
||||
items={[
|
||||
{ value: 'all', label: $i18n.t('All') },
|
||||
{ value: 'active', label: $i18n.t('Active') },
|
||||
{ value: 'paused', label: $i18n.t('Paused') }
|
||||
]}
|
||||
onChange={() => {
|
||||
page = 1;
|
||||
}}
|
||||
triggerClass="relative h-8 w-full flex items-center gap-0.5 px-1.5 py-1.5 bg-transparent rounded-xl text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
|
||||
>
|
||||
<svelte:fragment slot="trigger" let:selectedLabel>
|
||||
<span
|
||||
class="inline-flex h-input w-full outline-hidden bg-transparent truncate placeholder-gray-400 focus:outline-hidden"
|
||||
>
|
||||
{selectedLabel}
|
||||
</span>
|
||||
<ChevronDown className="size-3.5" strokeWidth="2.5" />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="item" let:item let:selected>
|
||||
{item.label}
|
||||
<div class="ml-auto {selected ? '' : 'invisible'}">
|
||||
<Check />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Select>
|
||||
|
||||
<Dropdown align="end">
|
||||
<Tooltip content={$i18n.t('Actions')}>
|
||||
<button
|
||||
class="flex h-8 items-center gap-1.5 rounded-xl bg-transparent px-1.5 text-[13px] font-normal text-gray-700 transition hover:text-gray-900 dark:text-gray-200 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
>
|
||||
<span>{$i18n.t('Actions')}</span>
|
||||
<ChevronDown className="size-3" strokeWidth="2.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<div slot="content">
|
||||
<DropdownMenu className="w-[170px] shadow-sm">
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
on:click={() => bulkToggleHandler(true)}
|
||||
>
|
||||
<CheckCircle className="size-3.5" />
|
||||
{$i18n.t('Enable All')}
|
||||
</button>
|
||||
<button
|
||||
class="select-none flex h-[1.6875rem] w-full cursor-pointer items-center gap-2 rounded-xl bg-transparent px-2 text-[13px] hover:text-gray-900 dark:hover:text-gray-100"
|
||||
type="button"
|
||||
on:click={() => bulkToggleHandler(false)}
|
||||
>
|
||||
<Minus className="size-3.5" />
|
||||
{$i18n.t('Disable All')}
|
||||
</button>
|
||||
</DropdownMenu>
|
||||
<div class="min-w-0 flex-1">
|
||||
<Tooltip content={automation.name} placement="top-start">
|
||||
<div
|
||||
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{automation.name}
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
<div class="truncate text-[11px] leading-4 text-gray-500 dark:text-gray-500">
|
||||
{formatRRule(automation.data.rrule)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if automations === null || loading}
|
||||
<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="flex min-h-[calc(100dvh-13rem)] w-full flex-col items-center justify-center"
|
||||
>
|
||||
<div class="max-w-sm text-center text-gray-900 dark:text-gray-100">
|
||||
<div class="mb-1.5 text-sm">
|
||||
{query ? $i18n.t('No results found') : $i18n.t('No automations found')}
|
||||
</div>
|
||||
<div class="text-center text-xs leading-5 text-gray-500">
|
||||
{query
|
||||
? $i18n.t(
|
||||
'Try adjusting your search or filter to find what you are looking for.'
|
||||
)
|
||||
: $i18n.t(
|
||||
'Create scheduled prompts that run automatically on a recurring basis.'
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="gap-y-0.5 grid my-1">
|
||||
{#each automations as automation (automation.id)}
|
||||
<div
|
||||
role="button"
|
||||
tabindex="0"
|
||||
aria-label={$i18n.t('Open automation')}
|
||||
class="group flex min-h-10 w-full cursor-pointer items-center gap-2 rounded-lg px-2 py-1 text-left transition hover:bg-gray-50 focus:bg-gray-50 dark:hover:bg-gray-900 dark:focus:bg-gray-900"
|
||||
on:click={() => {
|
||||
<div class="flex shrink-0 flex-row items-center gap-1.5 self-center">
|
||||
<AutomationMenu
|
||||
show={openAutomationMenuId === automation.id}
|
||||
editHandler={() => {
|
||||
goto(`/automations/${automation.id}`);
|
||||
}}
|
||||
on:keydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
goto(`/automations/${automation.id}`);
|
||||
}
|
||||
cloneHandler={() => {
|
||||
cloneHandler(automation);
|
||||
}}
|
||||
runHandler={() => {
|
||||
runNowHandler(automation);
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
deleteTarget = automation;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {
|
||||
openAutomationMenuId = null;
|
||||
}}
|
||||
>
|
||||
<div class="min-w-0 flex-1">
|
||||
<Tooltip content={automation.name} placement="top-start">
|
||||
<div
|
||||
class="truncate text-[13px] leading-5 text-gray-800 group-hover:underline dark:text-gray-200"
|
||||
>
|
||||
{automation.name}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div class="truncate text-[11px] leading-4 text-gray-500 dark:text-gray-500">
|
||||
{formatRRule(automation.data.rrule)}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition dark:text-gray-500"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Automation Menu')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openAutomationMenuId =
|
||||
openAutomationMenuId === automation.id ? null : automation.id;
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
</AutomationMenu>
|
||||
|
||||
<div class="flex shrink-0 flex-row items-center gap-1.5 self-center">
|
||||
<AutomationMenu
|
||||
show={openAutomationMenuId === automation.id}
|
||||
editHandler={() => {
|
||||
goto(`/automations/${automation.id}`);
|
||||
<button
|
||||
class="flex h-6 items-center"
|
||||
type="button"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={automation.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
>
|
||||
<Switch
|
||||
bind:state={automation.is_active}
|
||||
on:change={() => {
|
||||
toggleHandler(automation);
|
||||
}}
|
||||
cloneHandler={() => {
|
||||
cloneHandler(automation);
|
||||
}}
|
||||
runHandler={() => {
|
||||
runNowHandler(automation);
|
||||
}}
|
||||
deleteHandler={() => {
|
||||
deleteTarget = automation;
|
||||
showDeleteConfirm = true;
|
||||
}}
|
||||
onClose={() => {
|
||||
openAutomationMenuId = null;
|
||||
}}
|
||||
>
|
||||
<button
|
||||
class="flex size-6 items-center justify-center rounded-lg text-gray-400 transition hover:bg-gray-100 hover:text-gray-700 dark:text-gray-500 dark:hover:bg-gray-800 dark:hover:text-gray-200"
|
||||
type="button"
|
||||
aria-label={$i18n.t('Automation Menu')}
|
||||
on:click={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
openAutomationMenuId =
|
||||
openAutomationMenuId === automation.id ? null : automation.id;
|
||||
}}
|
||||
>
|
||||
<EllipsisHorizontal className="size-4" />
|
||||
</button>
|
||||
</AutomationMenu>
|
||||
|
||||
<button
|
||||
class="flex h-6 items-center"
|
||||
type="button"
|
||||
on:click={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
content={automation.is_active ? $i18n.t('Enabled') : $i18n.t('Disabled')}
|
||||
>
|
||||
<Switch
|
||||
bind:state={automation.is_active}
|
||||
on:change={() => {
|
||||
toggleHandler(automation);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
{#if total > 30}
|
||||
<div class="flex justify-center mt-4 mb-2">
|
||||
<Pagination bind:page count={total} perPage={30} />
|
||||
/>
|
||||
</Tooltip>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if (total ?? 0) > 30}
|
||||
<div class="flex justify-center mt-4 mb-2">
|
||||
<Pagination bind:page count={total ?? 0} perPage={30} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="w-full h-full flex justify-center items-center">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,25 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount, getContext } from 'svelte';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { page } from '$app/stores';
|
||||
import { user, showSidebar, config } from '$lib/stores';
|
||||
import { getAutomationById } from '$lib/apis/automations';
|
||||
import { user, config } from '$lib/stores';
|
||||
import { getAutomationById, type AutomationResponse } from '$lib/apis/automations';
|
||||
|
||||
import AutomationEditor from '$lib/components/automations/AutomationEditor.svelte';
|
||||
import Spinner from '$lib/components/common/Spinner.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
let automation = null;
|
||||
let automation: AutomationResponse | null = null;
|
||||
let loaded = false;
|
||||
|
||||
$: automationId = $page.params.id;
|
||||
|
||||
onMount(async () => {
|
||||
if (
|
||||
!$config?.features?.enable_automations ||
|
||||
!($config?.features as any)?.enable_automations ||
|
||||
($user?.role !== 'admin' && !($user?.permissions?.features?.automations ?? false))
|
||||
) {
|
||||
goto('/');
|
||||
@@ -47,11 +45,7 @@
|
||||
{#if loaded && automation}
|
||||
<AutomationEditor {automation} />
|
||||
{:else}
|
||||
<div
|
||||
class="w-full h-screen max-h-[100dvh] flex justify-center items-center transition-width duration-200 ease-in-out {$showSidebar
|
||||
? 'md:max-w-[calc(100%-var(--sidebar-width))]'
|
||||
: ''}"
|
||||
>
|
||||
<div class="flex h-full w-full items-center justify-center">
|
||||
<Spinner className="size-5" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user