mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
feat(frontend): add treeview (#2542)
* feat(frontend): add treeview * feat(frontend): fix size * feat(frontend): add treeview for users * feat(frontend): fix build + remove console.og * feat(frontend): fix actions * feat(frontend): add icon for fake folder * feat(frontend): fix add sort
This commit is contained in:
@@ -34,6 +34,7 @@
|
||||
export let moveDrawer: MoveDrawer
|
||||
export let deploymentDrawer: DeployWorkspaceDrawer
|
||||
export let deleteConfirmedCallback: (() => void) | undefined
|
||||
export let depth: number = 0
|
||||
|
||||
let {
|
||||
summary,
|
||||
@@ -77,6 +78,7 @@
|
||||
{starred}
|
||||
on:change
|
||||
canFavorite={!draft_only}
|
||||
{depth}
|
||||
>
|
||||
<svelte:fragment slot="badges">
|
||||
{#if execution_mode == 'anonymous'}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
export let deleteConfirmedCallback: (() => void) | undefined
|
||||
export let deploymentDrawer: DeployWorkspaceDrawer
|
||||
export let errorHandlerMuted: boolean
|
||||
export let depth: number = 0
|
||||
|
||||
let { summary, path, extra_perms, canWrite, workspace_id, archived, draft_only, has_draft } = flow
|
||||
|
||||
@@ -78,6 +79,7 @@
|
||||
{errorHandlerMuted}
|
||||
on:change
|
||||
canFavorite={!draft_only}
|
||||
{depth}
|
||||
>
|
||||
<svelte:fragment slot="badges">
|
||||
{#if archived}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
export let moveDrawer: MoveDrawer
|
||||
export let deleteConfirmedCallback: (() => void) | undefined
|
||||
export let deploymentDrawer: DeployWorkspaceDrawer
|
||||
export let depth: number = 0
|
||||
|
||||
let updateAppDrawer: Drawer
|
||||
|
||||
@@ -60,6 +61,7 @@
|
||||
{starred}
|
||||
on:change
|
||||
canFavorite={true}
|
||||
{depth}
|
||||
>
|
||||
<svelte:fragment slot="badges">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import RowIcon from './RowIcon.svelte'
|
||||
import { BellOff } from 'lucide-svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let marked: string | undefined
|
||||
export let starred: boolean
|
||||
@@ -16,11 +17,15 @@
|
||||
export let path: string
|
||||
export let href: string
|
||||
export let workspaceId: string
|
||||
export let depth: number = 0
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="hover:bg-surface-hover w-full inline-flex items-center gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md [*:not(:last-child)]:border-b px-4 py-2.5 border-b last:border-b-0"
|
||||
class={twMerge(
|
||||
'hover:bg-surface-hover w-full inline-flex items-center gap-4 first-of-type:!border-t-0 first-of-type:rounded-t-md last-of-type:rounded-b-md [*:not(:last-child)]:border-b px-4 py-2.5 border-b last:border-b-0',
|
||||
depth > 0 ? '!rounded-none' : ''
|
||||
)}
|
||||
style={depth > 0 ? `padding-left: ${depth * 32}px;` : ''}
|
||||
>
|
||||
<a {href} class="min-w-0 grow hover:underline decoration-gray-400 inline-flex items-center gap-4">
|
||||
<div class="shrink">
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
export let deploymentDrawer: DeployWorkspaceDrawer
|
||||
export let deleteConfirmedCallback: (() => void) | undefined
|
||||
export let errorHandlerMuted: boolean
|
||||
export let depth: number = 0
|
||||
|
||||
let {
|
||||
summary,
|
||||
@@ -96,6 +97,7 @@
|
||||
workspaceId={$workspaceStore ?? ''}
|
||||
on:change
|
||||
canFavorite={!draft_only}
|
||||
{depth}
|
||||
>
|
||||
<svelte:fragment slot="badges">
|
||||
{#if lock_error_logs}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import AppRow from '../common/table/AppRow.svelte'
|
||||
import FlowRow from '../common/table/FlowRow.svelte'
|
||||
import RawAppRow from '../common/table/RawAppRow.svelte'
|
||||
import ScriptRow from '../common/table/ScriptRow.svelte'
|
||||
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { Alert, Badge } from '$lib/components/common'
|
||||
import DeployWorkspaceDrawer from '../DeployWorkspaceDrawer.svelte'
|
||||
import MoveDrawer from '../MoveDrawer.svelte'
|
||||
import ShareModal from '../ShareModal.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let item
|
||||
export let depth: number = 0
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let deleteConfirmedCallback: (() => void) | undefined = undefined
|
||||
let shareModal: ShareModal
|
||||
let moveDrawer: MoveDrawer
|
||||
let deploymentDrawer: DeployWorkspaceDrawer
|
||||
</script>
|
||||
|
||||
{#key item.summary}
|
||||
{#key item.starred}
|
||||
{#key item.has_draft}
|
||||
{#if item.type == 'script'}
|
||||
<ScriptRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={() => dispatch('scriptChanged')}
|
||||
script={item}
|
||||
errorHandlerMuted={item.ws_error_handler_muted === undefined ||
|
||||
item.ws_error_handler_muted === null
|
||||
? false
|
||||
: item.ws_error_handler_muted}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
{deploymentDrawer}
|
||||
{depth}
|
||||
/>
|
||||
{:else if item.type == 'flow'}
|
||||
<FlowRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={() => dispatch('flowChanged')}
|
||||
flow={item}
|
||||
errorHandlerMuted={item.ws_error_handler_muted === undefined ||
|
||||
item.ws_error_handler_muted === null
|
||||
? false
|
||||
: item.ws_error_handler_muted}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
{deploymentDrawer}
|
||||
{depth}
|
||||
/>
|
||||
{:else if item.type == 'app'}
|
||||
<AppRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={() => dispatch('appChanged')}
|
||||
app={item}
|
||||
{moveDrawer}
|
||||
{shareModal}
|
||||
{deploymentDrawer}
|
||||
{depth}
|
||||
/>
|
||||
{:else if item.type == 'raw_app'}
|
||||
<RawAppRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={() => dispatch('rawAppChanged')}
|
||||
app={item}
|
||||
{moveDrawer}
|
||||
{shareModal}
|
||||
{deploymentDrawer}
|
||||
{depth}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
|
||||
<ConfirmationModal
|
||||
open={Boolean(deleteConfirmedCallback)}
|
||||
title="Remove"
|
||||
confirmationText="Remove"
|
||||
on:canceled={() => {
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
if (deleteConfirmedCallback) {
|
||||
deleteConfirmedCallback()
|
||||
}
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>Are you sure you want to remove it?</span>
|
||||
<Alert type="info" title="Bypass confirmation">
|
||||
<div>
|
||||
You can press
|
||||
<Badge color="dark-gray">SHIFT</Badge>
|
||||
while removing to bypass confirmation.
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModal}
|
||||
on:change={() => {
|
||||
dispatch('reload')
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
|
||||
<MoveDrawer
|
||||
bind:this={moveDrawer}
|
||||
on:update={() => {
|
||||
dispatch('reload')
|
||||
}}
|
||||
/>
|
||||
@@ -1,7 +1,6 @@
|
||||
<script lang="ts">
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Alert, Badge, Button, Skeleton } from '$lib/components/common'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import { Button, Skeleton } from '$lib/components/common'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import {
|
||||
AppService,
|
||||
@@ -16,30 +15,25 @@
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import type uFuzzy from '@leeoniya/ufuzzy'
|
||||
import { Code2, LayoutDashboard, SearchCode } from 'lucide-svelte'
|
||||
import TreeView from './TreeView.svelte'
|
||||
|
||||
export let filter = ''
|
||||
export let subtab: 'flow' | 'script' | 'app' = 'script'
|
||||
|
||||
import AppRow from '$lib/components/common/table/AppRow.svelte'
|
||||
import FlowRow from '$lib/components/common/table/FlowRow.svelte'
|
||||
import ScriptRow from '$lib/components/common/table/ScriptRow.svelte'
|
||||
|
||||
import { HOME_SEARCH_SHOW_FLOW, HOME_SEARCH_PLACEHOLDER } from '$lib/consts'
|
||||
|
||||
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
||||
import MoveDrawer from '../MoveDrawer.svelte'
|
||||
import SearchItems from '../SearchItems.svelte'
|
||||
import ListFilters from './ListFilters.svelte'
|
||||
import NoItemFound from './NoItemFound.svelte'
|
||||
import ToggleButtonGroup from '../common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '../common/toggleButton-v2/ToggleButton.svelte'
|
||||
import FlowIcon from './FlowIcon.svelte'
|
||||
import RawAppRow from '../common/table/RawAppRow.svelte'
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { page } from '$app/stores'
|
||||
import { setQuery } from '$lib/navigation'
|
||||
import DeployWorkspaceDrawer from '../DeployWorkspaceDrawer.svelte'
|
||||
import ContentSearch from '../ContentSearch.svelte'
|
||||
import { groupItems } from './treeViewUtils'
|
||||
import Item from './Item.svelte'
|
||||
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
|
||||
canWrite: boolean
|
||||
@@ -64,16 +58,10 @@
|
||||
|
||||
let itemKind = ($page.url.searchParams.get('kind') as 'script' | 'flow' | 'app' | 'all') ?? 'all'
|
||||
|
||||
let shareModal: ShareModal
|
||||
let moveDrawer: MoveDrawer
|
||||
let deploymentDrawer: DeployWorkspaceDrawer
|
||||
|
||||
let loading = true
|
||||
|
||||
let nbDisplayed = 30
|
||||
|
||||
export let deleteConfirmedCallback: (() => void) | undefined = undefined
|
||||
|
||||
async function loadScripts(): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -255,6 +243,7 @@
|
||||
$: items && resetScroll()
|
||||
|
||||
let archived = false
|
||||
let treeView = false
|
||||
|
||||
let contentSearch: ContentSearch
|
||||
</script>
|
||||
@@ -267,27 +256,6 @@
|
||||
{opts}
|
||||
/>
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModal}
|
||||
on:change={() => {
|
||||
loadScripts()
|
||||
loadApps()
|
||||
loadFlows()
|
||||
loadRawApps()
|
||||
}}
|
||||
/>
|
||||
|
||||
<DeployWorkspaceDrawer bind:this={deploymentDrawer} />
|
||||
<MoveDrawer
|
||||
bind:this={moveDrawer}
|
||||
on:update={() => {
|
||||
loadScripts()
|
||||
loadApps()
|
||||
loadFlows()
|
||||
loadRawApps()
|
||||
}}
|
||||
/>
|
||||
|
||||
<ContentSearch bind:this={contentSearch} />
|
||||
<CenteredPage>
|
||||
<div class="flex flex-wrap gap-2 items-center justify-between w-full mt-2">
|
||||
@@ -371,8 +339,9 @@
|
||||
<div class="mt-10" />
|
||||
{/if}
|
||||
{#if !loading}
|
||||
<div class="flex w-full flex-row-reverse">
|
||||
<div class="flex w-full flex-row-reverse gap-2">
|
||||
<Toggle size="xs" bind:checked={archived} options={{ right: 'Show archived' }} />
|
||||
<Toggle size="xs" bind:checked={treeView} options={{ right: 'Tree view' }} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -387,69 +356,39 @@
|
||||
<NoItemFound />
|
||||
{:else}
|
||||
<div class="border rounded-md">
|
||||
<!-- <VirtualList {items} let:item bind:start bind:end> -->
|
||||
{#each (items ?? []).slice(0, nbDisplayed) as item (item.type + '/' + item.path)}
|
||||
{#key item.summary}
|
||||
{#key item.starred}
|
||||
{#key item.has_draft}
|
||||
{#if item.type == 'script'}
|
||||
<ScriptRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadScripts}
|
||||
script={item}
|
||||
errorHandlerMuted={item.ws_error_handler_muted === undefined ||
|
||||
item.ws_error_handler_muted === null
|
||||
? false
|
||||
: item.ws_error_handler_muted}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
{deploymentDrawer}
|
||||
/>
|
||||
{:else if item.type == 'flow'}
|
||||
<FlowRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadFlows}
|
||||
flow={item}
|
||||
errorHandlerMuted={item.ws_error_handler_muted === undefined ||
|
||||
item.ws_error_handler_muted === null
|
||||
? false
|
||||
: item.ws_error_handler_muted}
|
||||
{shareModal}
|
||||
{moveDrawer}
|
||||
{deploymentDrawer}
|
||||
/>
|
||||
{:else if item.type == 'app'}
|
||||
<AppRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadApps}
|
||||
app={item}
|
||||
{moveDrawer}
|
||||
{shareModal}
|
||||
{deploymentDrawer}
|
||||
/>
|
||||
{:else if item.type == 'raw_app'}
|
||||
<RawAppRow
|
||||
bind:deleteConfirmedCallback
|
||||
starred={item.starred ?? false}
|
||||
marked={item.marked}
|
||||
on:change={loadRawApps}
|
||||
app={item}
|
||||
{moveDrawer}
|
||||
{shareModal}
|
||||
{deploymentDrawer}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
{/key}
|
||||
{/key}
|
||||
{/each}
|
||||
<!-- </VirtualList> -->
|
||||
{#if treeView}
|
||||
{#each (groupItems(items) ?? []).slice(0, nbDisplayed) as item}
|
||||
<TreeView
|
||||
{item}
|
||||
on:scriptChanged={loadScripts}
|
||||
on:flowChanged={loadFlows}
|
||||
on:appChanged={loadApps}
|
||||
on:rawAppChanged={loadRawApps}
|
||||
on:reload={() => {
|
||||
loadScripts()
|
||||
loadFlows()
|
||||
loadApps()
|
||||
loadRawApps()
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each (items ?? []).slice(0, nbDisplayed) as item (item.type + '/' + item.path)}
|
||||
<Item
|
||||
{item}
|
||||
on:scriptChanged={loadScripts}
|
||||
on:flowChanged={loadFlows}
|
||||
on:appChanged={loadApps}
|
||||
on:rawAppChanged={loadRawApps}
|
||||
on:reload={() => {
|
||||
loadScripts()
|
||||
loadFlows()
|
||||
loadApps()
|
||||
loadRawApps()
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
{#if items && items?.length > 30}
|
||||
<span class="text-xs"
|
||||
@@ -460,29 +399,3 @@
|
||||
{/if}
|
||||
</div>
|
||||
</CenteredPage>
|
||||
|
||||
<ConfirmationModal
|
||||
open={Boolean(deleteConfirmedCallback)}
|
||||
title="Remove"
|
||||
confirmationText="Remove"
|
||||
on:canceled={() => {
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
on:confirmed={() => {
|
||||
if (deleteConfirmedCallback) {
|
||||
deleteConfirmedCallback()
|
||||
}
|
||||
deleteConfirmedCallback = undefined
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-col w-full space-y-4">
|
||||
<span>Are you sure you want to remove it?</span>
|
||||
<Alert type="info" title="Bypass confirmation">
|
||||
<div>
|
||||
You can press
|
||||
<Badge color="dark-gray">SHIFT</Badge>
|
||||
while removing to bypass confirmation.
|
||||
</div>
|
||||
</Alert>
|
||||
</div>
|
||||
</ConfirmationModal>
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
<script lang="ts">
|
||||
import { ChevronDown, ChevronUp, Folder, FolderTree, User } from 'lucide-svelte'
|
||||
import Item from './Item.svelte'
|
||||
import type { FolderItem, ItemType, UserItem } from './treeViewUtils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { slide } from 'svelte/transition'
|
||||
import { pluralize } from '$lib/utils'
|
||||
|
||||
export let item: ItemType | FolderItem | UserItem | undefined
|
||||
|
||||
export let depth: number = 0
|
||||
|
||||
const isFolder = (i: any): i is FolderItem => i && 'folderName' in i
|
||||
const isUser = (i: any): i is UserItem => i && 'username' in i
|
||||
|
||||
let opened: boolean = true
|
||||
</script>
|
||||
|
||||
{#if item}
|
||||
{#if isFolder(item)}
|
||||
<div>
|
||||
<div class={twMerge('px-4 py-2 border-b w-full flex flex-row items-center justify-between')}>
|
||||
<div
|
||||
class={twMerge('flex flex-row items-center gap-4 text-sm font-semibold')}
|
||||
style={depth > 0 ? `padding-left: ${depth * 16}px;` : ''}
|
||||
>
|
||||
<div
|
||||
class=" rounded-md p-1 flex justify-center items-center border bg-gray-50 border-gray-200 dark:bg-transparent dark:border-gray-900"
|
||||
>
|
||||
{#if depth === 0}
|
||||
<Folder size={20} color="#aaa" />
|
||||
{:else}
|
||||
<FolderTree size={20} color="#aaa" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{#if depth === 0}f/{/if}{item.folderName}
|
||||
<div class="text-2xs font-normal text-secondary">
|
||||
({pluralize(item.items.length, ' item')})
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button on:click={() => (opened = !opened)}>
|
||||
{#if opened}
|
||||
<ChevronUp size={20} />
|
||||
{:else}
|
||||
<ChevronDown size={20} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#if opened}
|
||||
<div transition:slide>
|
||||
{#each item.items as subItem}
|
||||
<svelte:self
|
||||
item={subItem}
|
||||
on:scriptChanged
|
||||
on:flowChanged
|
||||
on:appChanged
|
||||
on:rawAppChanged
|
||||
on:reload
|
||||
depth={depth + 1}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if isUser(item)}
|
||||
<div>
|
||||
<div class={twMerge('px-4 py-2 border-b w-full flex flex-row items-center justify-between')}>
|
||||
<div
|
||||
class={twMerge('flex flex-row items-center gap-4 text-sm font-semibold')}
|
||||
style={depth > 0 ? `padding-left: ${depth * 16}px;` : ''}
|
||||
>
|
||||
<div
|
||||
class=" rounded-md p-1 flex justify-center items-center border bg-gray-50 border-gray-200 dark:bg-transparent dark:border-gray-900"
|
||||
>
|
||||
<User size={20} color="#aaa" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
u/{item.username}
|
||||
<div class="text-2xs font-normal text-secondary"
|
||||
>({pluralize(item.items.length, ' item')})</div
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<button on:click={() => (opened = !opened)}>
|
||||
{#if opened}
|
||||
<ChevronUp size={20} />
|
||||
{:else}
|
||||
<ChevronDown size={20} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{#if opened}
|
||||
<div transition:slide>
|
||||
{#each item.items as subItem}
|
||||
<svelte:self
|
||||
item={subItem}
|
||||
on:scriptChanged
|
||||
on:flowChanged
|
||||
on:appChanged
|
||||
on:rawAppChanged
|
||||
on:reload
|
||||
depth={depth + 1}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<Item {item} on:scriptChanged on:flowChanged on:appChanged on:rawAppChanged on:reload {depth} />
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -0,0 +1,130 @@
|
||||
import type { ListableApp, Script, Flow, ListableRawApp } from '$lib/gen'
|
||||
type TableItem<T, U extends 'script' | 'flow' | 'app' | 'raw_app'> = T & {
|
||||
canWrite: boolean
|
||||
marked?: string
|
||||
type?: U
|
||||
time?: number
|
||||
starred?: boolean
|
||||
has_draft?: boolean
|
||||
}
|
||||
|
||||
type TableScript = TableItem<Script, 'script'>
|
||||
type TableFlow = TableItem<Flow, 'flow'>
|
||||
type TableApp = TableItem<ListableApp, 'app'>
|
||||
type TableRawApp = TableItem<ListableRawApp, 'raw_app'>
|
||||
|
||||
export type ItemType = TableScript | TableFlow | TableApp | TableRawApp
|
||||
|
||||
export interface FolderItem {
|
||||
folderName: string
|
||||
items: (ItemType | FolderItem)[]
|
||||
}
|
||||
|
||||
export type UserItem = {
|
||||
username: string
|
||||
items: (ItemType | FolderItem)[]
|
||||
}
|
||||
|
||||
function insertItemInFolder(
|
||||
root: (ItemType | FolderItem | UserItem)[],
|
||||
item: ItemType,
|
||||
path: string[]
|
||||
) {
|
||||
let currentLevel = root
|
||||
|
||||
path.forEach((folderName, index) => {
|
||||
if (index === path.length - 1) {
|
||||
currentLevel.push(item)
|
||||
} else {
|
||||
let folder = currentLevel.find((f) => 'folderName' in f && f.folderName === folderName) as
|
||||
| FolderItem
|
||||
| undefined
|
||||
|
||||
if (!folder) {
|
||||
folder = { folderName: folderName, items: [] }
|
||||
currentLevel.push(folder)
|
||||
}
|
||||
currentLevel = folder.items
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function groupItems(items: ItemType[] | undefined): (ItemType | FolderItem | UserItem)[] {
|
||||
if (!items) {
|
||||
return []
|
||||
}
|
||||
|
||||
const root: (ItemType | FolderItem | UserItem)[] = []
|
||||
|
||||
items.forEach((item) => {
|
||||
const pathSplit = item.path.split('/')
|
||||
if (pathSplit[0] === 'u') {
|
||||
const username = pathSplit[1]
|
||||
let userItem = root.find((f): f is UserItem => 'username' in f && f.username === username) as
|
||||
| UserItem
|
||||
| undefined
|
||||
|
||||
if (!userItem) {
|
||||
userItem = { username, items: [] }
|
||||
root.push(userItem)
|
||||
}
|
||||
|
||||
if (pathSplit.length > 2) {
|
||||
insertItemInFolder(userItem.items, item, pathSplit.slice(2))
|
||||
} else {
|
||||
userItem.items.push(item)
|
||||
}
|
||||
} else if (pathSplit[0] === 'f') {
|
||||
insertItemInFolder(root, item, pathSplit.slice(1))
|
||||
}
|
||||
})
|
||||
|
||||
root.sort((a, b) => {
|
||||
if ('username' in a && 'folderName' in b) {
|
||||
return -1
|
||||
}
|
||||
if ('folderName' in a && 'username' in b) {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
sortGroup(root)
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
function sortGroup(group: (ItemType | FolderItem | UserItem)[]) {
|
||||
group.forEach((item) => {
|
||||
if ('items' in item) {
|
||||
item.items.sort((a, b) => {
|
||||
if (isItemType(a) && isItemType(b)) {
|
||||
if (a.starred && !b.starred) return -1
|
||||
if (!a.starred && b.starred) return 1
|
||||
return getModifiedAt(b) - getModifiedAt(a)
|
||||
}
|
||||
return 0
|
||||
})
|
||||
|
||||
sortGroup(item.items)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function isItemType(item: ItemType | FolderItem | UserItem): item is ItemType {
|
||||
return 'type' in item
|
||||
}
|
||||
|
||||
function getModifiedAt(item: ItemType): number {
|
||||
if (item.type === 'app') {
|
||||
return new Date(item.edited_at).getTime() || 0
|
||||
} else if (item.type === 'script') {
|
||||
return new Date(item.created_at).getTime() || 0
|
||||
} else if (item.type === 'flow') {
|
||||
return new Date(item.edited_at).getTime() || 0
|
||||
} else if (item.type === 'raw_app') {
|
||||
return new Date(item.edited_at).getTime() || 0
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
Reference in New Issue
Block a user