fix(frontend): fix home (#981)

This commit is contained in:
Faton Ramadani
2022-12-03 14:24:20 +01:00
committed by GitHub
parent 90ce431418
commit fa64e83f7e
2 changed files with 437 additions and 368 deletions

View File

@@ -0,0 +1,430 @@
<script lang="ts">
import CenteredPage from '$lib/components/CenteredPage.svelte'
import {
AppService,
FlowService,
Job,
JobService,
ListableApp,
Script,
ScriptService,
type Flow,
type OpenFlow
} from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import VirtualList from '@sveltejs/svelte-virtual-list'
import {
Button,
Drawer,
DrawerContent,
Skeleton,
ToggleButton,
ToggleButtonGroup
} from '$lib/components/common'
import { canWrite, classNames, getScriptByPath, pluralize } from '$lib/utils'
import type { HubItem } from '$lib/components/flows/pickers/model'
import ShareModal from '$lib/components/ShareModal.svelte'
import { faCodeFork, faGlobe } from '@fortawesome/free-solid-svg-icons'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import SearchItems from '$lib/components/SearchItems.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import type uFuzzy from '@leeoniya/ufuzzy'
import { Code2, LayoutDashboard, Wind } from 'svelte-lucide'
import ScriptRow from '$lib/components/common/table/ScriptRow.svelte'
import FlowRow from '$lib/components/common/table/FlowRow.svelte'
import AppRow from '$lib/components/common/table/AppRow.svelte'
import { fade } from 'svelte/transition'
import { flip } from 'svelte/animate'
let jobs: Job[] = []
type TableItem<T, U extends 'script' | 'flow' | 'app'> = T & {
canWrite: boolean
marked?: string
type?: U
time?: number
starred?: boolean
}
type TableScript = TableItem<Script, 'script'>
type TableFlow = TableItem<Flow, 'flow'>
type TableApp = TableItem<ListableApp, 'app'>
let scripts: TableScript[] | undefined
let flows: TableFlow[] | undefined
let apps: TableApp[] | undefined
let filteredItems: (TableScript | TableFlow | TableApp)[] = []
let itemKind: 'script' | 'flow' | 'app' | 'all' = 'all'
let filter: string = ''
let shareModalScripts: ShareModal
let shareModalFlows: ShareModal
let loading = true
let flowViewer: Drawer
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined
let codeViewer: Drawer
let codeViewerContent: string = ''
let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno'
let codeViewerObj: HubItem | undefined = undefined
async function loadScripts(): Promise<void> {
const loadedScripts = await ScriptService.listScripts({
workspace: $workspaceStore!,
perPage: 300
})
scripts = loadedScripts.map((script: Script) => {
return {
canWrite:
canWrite(script.path, script.extra_perms, $userStore) &&
script.workspace_id == $workspaceStore,
...script
}
})
loading = false
}
async function loadFlows(): Promise<void> {
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
return {
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
...x
}
})
loading = false
}
async function loadApps(): Promise<void> {
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((app: ListableApp) => {
return {
canWrite:
canWrite(app.path!, app.extra_perms!, $userStore) && app.workspace_id == $workspaceStore,
...app
}
})
loading = false
}
$: owners = Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
let combinedItems: (TableScript | TableFlow | TableApp)[] | undefined = undefined
$: combinedItems =
flows == undefined || scripts == undefined || apps == undefined
? undefined
: [
...flows.map((x) => ({
...x,
type: 'flow' as 'flow',
time: new Date(x.edited_at).getTime()
})),
...scripts.map((x) => ({
...x,
type: 'script' as 'script',
time: new Date(x.created_at).getTime()
})),
...apps.map((x) => ({
...x,
type: 'app' as 'app',
time: new Date(x.edited_at).getTime()
}))
].sort((a, b) =>
a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1
)
$: preFilteredItems =
ownerFilter != undefined
? combinedItems?.filter(
(x) => x.path.startsWith(ownerFilter ?? '') && (x.type == itemKind || itemKind == 'all')
)
: combinedItems?.filter((x) => x.type == itemKind || itemKind == 'all')
let ownerFilter: string | undefined = undefined
async function loadJobs() {
jobs = await JobService.listJobs({
workspace: $workspaceStore!,
success: true,
createdBy: $userStore?.username,
jobKinds: 'flow,script'
})
loading = false
}
$: {
if (($userStore || $superadmin) && $workspaceStore) {
loadScripts()
loadFlows()
loadApps()
loadJobs()
}
}
const cmp = new Intl.Collator('en').compare
const opts: uFuzzy.Options = {
sort: (info, haystack, needle) => {
let {
idx,
chars,
terms,
interLft2,
interLft1,
// interRgt2,
// interRgt1,
start,
intraIns,
interIns
} = info
const sortResult = idx
.map((v, i) => i)
.sort(
(ia, ib) =>
// most contig chars matched
chars[ib] - chars[ia] ||
// least char intra-fuzz (most contiguous)
intraIns[ia] - intraIns[ib] ||
// most prefix bounds, boosted by full term matches
terms[ib] +
interLft2[ib] +
0.5 * interLft1[ib] -
(terms[ia] + interLft2[ia] + 0.5 * interLft1[ia]) ||
// highest density of match (least span)
// span[ia] - span[ib] ||
// highest density of match (least term inter-fuzz)
interIns[ia] - interIns[ib] ||
// earliest start of match
start[ia] - start[ib] ||
// alphabetic
cmp(haystack[idx[ia]], haystack[idx[ib]]) +
(preFilteredItems?.[idx[ib]]?.starred ? 100 : 0) -
(preFilteredItems?.[idx[ia]]?.starred ? 100 : 0)
)
return sortResult
}
}
$: items = filter !== '' ? filteredItems : preFilteredItems
let start: number
let end: number
export let clientHeight: number
$: height = (100 * clientHeight) / window.innerHeight
function resetScroll() {
const element = document.getElementsByTagName('svelte-virtual-list-viewport')
const firstElement = element.item(0)
if (firstElement) {
firstElement.scrollTop = 0
}
}
$: items && resetScroll()
</script>
<SearchItems
{filter}
items={preFilteredItems}
bind:filteredItems
f={(x) => (x.summary ? x.summary + ' (' + x.path + ')' : x.path)}
{opts}
/>
<ShareModal
bind:this={shareModalScripts}
kind="script"
on:change={() => {
loadScripts()
}}
/>
<ShareModal
bind:this={shareModalFlows}
kind="flow"
on:change={() => {
loadFlows()
}}
/>
<Drawer bind:this={codeViewer} size="900px">
<DrawerContent title={codeViewerObj?.summary ?? ''} on:close={codeViewer.closeDrawer}>
<div slot="submission" class="flex flex-row gap-2 pr-2">
<Button
href="https://hub.windmill.dev/scripts/{codeViewerObj?.app ?? ''}/{codeViewerObj?.ask_id ??
0}"
startIcon={{ icon: faGlobe }}
variant="border"
>
View on the Hub
</Button>
<Button
href="/scripts/add?hub={encodeURIComponent(codeViewerObj?.path ?? '')}"
startIcon={{ icon: faCodeFork }}
>
Fork
</Button>
</div>
<HighlightCode language={codeViewerLanguage} code={codeViewerContent} />
</DrawerContent>
</Drawer>
<Drawer bind:this={flowViewer} size="900px">
<DrawerContent title="Hub flow" on:close={flowViewer.closeDrawer}>
<div slot="submission" class="flex flex-row gap-2 pr-2">
<Button
href="https://hub.windmill.dev/flows/{flowViewerFlow?.flow?.id}"
startIcon={{ icon: faGlobe }}
variant="border"
>
View on the Hub
</Button>
<Button href="/flows/add?hub={flowViewerFlow?.flow?.id}" startIcon={{ icon: faCodeFork }}>
Fork
</Button>
</div>
{#if flowViewerFlow?.flow}
<FlowViewer flow={flowViewerFlow.flow} />
{/if}
</DrawerContent>
</Drawer>
<CenteredPage>
<div class="flex flex-col md:flex-row gap-2 items-center sm:justify-between w-full">
<div>
<ToggleButtonGroup bind:selected={itemKind}>
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
<ToggleButton light position="center" value="script" size="sm">
<div class="flex gap-1 items-center">
<Code2 size="16px" />
Scripts
</div>
</ToggleButton>
<ToggleButton light position="center" value="flow" size="sm">
<div class="flex gap-1 items-center">
<Wind size="16px" />
Flows
</div>
</ToggleButton>
<ToggleButton light position="right" value="app" size="sm">
<div class="flex gap-1 items-center">
<LayoutDashboard size="16px" />
Apps
</div>
</ToggleButton>
</ToggleButtonGroup>
</div>
<div class="relative text-gray-600 w-full">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
placeholder="Search Scripts, Flows & Apps"
bind:value={filter}
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
/>
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
<svg
class="h-4 w-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 56.966 56.966"
style="enable-background:new 0 0 56.966 56.966;"
xml:space="preserve"
width="512px"
height="512px"
>
<path
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
/>
</svg>
</button>
</div>
</div>
{#if owners.length > 0}
<div class="gap-2 w-full flex flex-wrap my-4">
{#each owners as owner (owner)}
<div in:fade animate:flip={{ duration: 200 }}>
<Badge
class="cursor-pointer hover:bg-gray-200"
on:click={() => {
ownerFilter = ownerFilter == owner ? undefined : owner
}}
color={owner === ownerFilter ? 'blue' : 'gray'}
baseClass={owner === ownerFilter ? 'border border-blue-500' : 'border'}
>
{owner}
{#if owner === ownerFilter}&cross;{/if}
</Badge>
</div>
{/each}
</div>
{/if}
<div>
{#if filteredItems == undefined}
<div class="mt-4" />
<Skeleton layout={[[2], 1]} />
{#each new Array(6) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{:else if filteredItems.length === 0}
<div class="flex justify-center items-center h-48">
<div class="text-gray-500 text-center">
<div class="text-2xl font-bold">No items found</div>
<div class="text-sm">Try changing your search or filters</div>
</div>
</div>
{:else}
<div
class={classNames('border rounded-md')}
style={`height: calc(100vh - 10em - ${height}vh);`}
>
<VirtualList {items} let:item bind:start bind:end>
{#if item.type == 'script'}
<ScriptRow
starred={item.starred ?? false}
marked={item.marked}
on:change={loadScripts}
script={item}
shareModal={shareModalScripts}
/>
{:else if item.type == 'flow'}
<FlowRow
starred={item.starred ?? false}
marked={item.marked}
on:change={loadFlows}
flow={item}
shareModal={shareModalFlows}
/>
{:else if item.type == 'app'}
<AppRow
starred={item.starred ?? false}
marked={item.marked}
on:change={loadApps}
app={item}
/>
{/if}
</VirtualList>
</div>
<span class="text-xs">{pluralize(items?.length ?? 0, 'item')}</span>
<span class="text-xs">{`(${start} - ${end})`}</span>
{/if}
</div>
</CenteredPage>

View File

@@ -1,83 +1,28 @@
<script lang="ts">
import CenteredPage from '$lib/components/CenteredPage.svelte'
import {
AppService,
FlowService,
Job,
JobService,
ListableApp,
Script,
ScriptService,
type Flow,
type OpenFlow
} from '$lib/gen'
import { superadmin, userStore, workspaceStore } from '$lib/stores'
import VirtualList from '@sveltejs/svelte-virtual-list'
import {
Alert,
Button,
Drawer,
DrawerContent,
Skeleton,
Tab,
Tabs,
ToggleButton,
ToggleButtonGroup
} from '$lib/components/common'
import { FlowService, type OpenFlow } from '$lib/gen'
import { workspaceStore } from '$lib/stores'
import { Alert, Button, Drawer, DrawerContent, Tab, Tabs } from '$lib/components/common'
import PageHeader from '$lib/components/PageHeader.svelte'
import CreateActionsFlow from '$lib/components/flows/CreateActionsFlow.svelte'
import CreateActionsScript from '$lib/components/scripts/CreateActionsScript.svelte'
import { canWrite, classNames, getScriptByPath, pluralize } from '$lib/utils'
import { getScriptByPath } from '$lib/utils'
import type { HubItem } from '$lib/components/flows/pickers/model'
import ShareModal from '$lib/components/ShareModal.svelte'
import { faCodeFork, faGlobe } from '@fortawesome/free-solid-svg-icons'
import PickHubScript from '$lib/components/flows/pickers/PickHubScript.svelte'
import PickHubFlow from './PickHubFlow.svelte'
import FlowViewer from '$lib/components/FlowViewer.svelte'
import HighlightCode from '$lib/components/HighlightCode.svelte'
import SearchItems from '$lib/components/SearchItems.svelte'
import Badge from '$lib/components/common/badge/Badge.svelte'
import type uFuzzy from '@leeoniya/ufuzzy'
import { Building, Code2, Globe2, LayoutDashboard, Wind } from 'svelte-lucide'
import { Building, Globe2 } from 'svelte-lucide'
import ScriptRow from '$lib/components/common/table/ScriptRow.svelte'
import FlowRow from '$lib/components/common/table/FlowRow.svelte'
import AppRow from '$lib/components/common/table/AppRow.svelte'
import { fade } from 'svelte/transition'
import { flip } from 'svelte/animate'
import CreateApp from '$lib/components/apps/CreateApp.svelte'
let jobs: Job[] = []
import ItemsList from '$lib/components/home/ItemsList.svelte'
type Tab = 'hubscripts' | 'hubflows' | 'workspace'
type TableItem<T, U extends 'script' | 'flow' | 'app'> = T & {
canWrite: boolean
marked?: string
type?: U
time?: number
starred?: boolean
}
type TableScript = TableItem<Script, 'script'>
type TableFlow = TableItem<Flow, 'flow'>
type TableApp = TableItem<ListableApp, 'app'>
let scripts: TableScript[] | undefined
let flows: TableFlow[] | undefined
let apps: TableApp[] | undefined
let filteredItems: (TableScript | TableFlow | TableApp)[] = []
let itemKind: 'script' | 'flow' | 'app' | 'all' = 'all'
let tab: Tab = 'workspace'
let filter: string = ''
let shareModalScripts: ShareModal
let shareModalFlows: ShareModal
let loading = true
let flowViewer: Drawer
let flowViewerFlow: { flow?: OpenFlow & { id?: number } } | undefined
@@ -86,24 +31,6 @@
let codeViewerLanguage: 'deno' | 'python3' | 'go' | 'bash' = 'deno'
let codeViewerObj: HubItem | undefined = undefined
async function loadScripts(): Promise<void> {
const loadedScripts = await ScriptService.listScripts({
workspace: $workspaceStore!,
perPage: 300
})
scripts = loadedScripts.map((script: Script) => {
return {
canWrite:
canWrite(script.path, script.extra_perms, $userStore) &&
script.workspace_id == $workspaceStore,
...script
}
})
loading = false
}
async function viewCode(obj: HubItem) {
const { content, language } = await getScriptByPath(obj.path)
codeViewerContent = content
@@ -112,178 +39,15 @@
codeViewer.openDrawer()
}
async function loadFlows(): Promise<void> {
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
return {
canWrite: canWrite(x.path, x.extra_perms, $userStore) && x.workspace_id == $workspaceStore,
...x
}
})
loading = false
}
async function loadApps(): Promise<void> {
apps = (await AppService.listApps({ workspace: $workspaceStore! })).map((app: ListableApp) => {
return {
canWrite:
canWrite(app.path!, app.extra_perms!, $userStore) && app.workspace_id == $workspaceStore,
...app
}
})
loading = false
}
async function viewFlow(obj: { flow_id: number }): Promise<void> {
const hub = await FlowService.getHubFlowById({ id: obj.flow_id })
flowViewerFlow = hub
flowViewer.openDrawer()
}
$: owners = Array.from(
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
).sort()
let combinedItems: (TableScript | TableFlow | TableApp)[] | undefined = undefined
$: combinedItems =
flows == undefined || scripts == undefined || apps == undefined
? undefined
: [
...flows.map((x) => ({
...x,
type: 'flow' as 'flow',
time: new Date(x.edited_at).getTime()
})),
...scripts.map((x) => ({
...x,
type: 'script' as 'script',
time: new Date(x.created_at).getTime()
})),
...apps.map((x) => ({
...x,
type: 'app' as 'app',
time: new Date(x.edited_at).getTime()
}))
].sort((a, b) =>
a.starred != b.starred ? (a.starred ? -1 : 1) : a.time - b.time > 0 ? -1 : 1
)
$: preFilteredItems =
ownerFilter != undefined
? combinedItems?.filter(
(x) => x.path.startsWith(ownerFilter ?? '') && (x.type == itemKind || itemKind == 'all')
)
: combinedItems?.filter((x) => x.type == itemKind || itemKind == 'all')
let ownerFilter: string | undefined = undefined
async function loadJobs() {
jobs = await JobService.listJobs({
workspace: $workspaceStore!,
success: true,
createdBy: $userStore?.username,
jobKinds: 'flow,script'
})
loading = false
}
$: {
if (($userStore || $superadmin) && $workspaceStore) {
loadScripts()
loadFlows()
loadApps()
loadJobs()
}
}
const cmp = new Intl.Collator('en').compare
const opts: uFuzzy.Options = {
sort: (info, haystack, needle) => {
let {
idx,
chars,
terms,
interLft2,
interLft1,
// interRgt2,
// interRgt1,
start,
intraIns,
interIns
} = info
const sortResult = idx
.map((v, i) => i)
.sort(
(ia, ib) =>
// most contig chars matched
chars[ib] - chars[ia] ||
// least char intra-fuzz (most contiguous)
intraIns[ia] - intraIns[ib] ||
// most prefix bounds, boosted by full term matches
terms[ib] +
interLft2[ib] +
0.5 * interLft1[ib] -
(terms[ia] + interLft2[ia] + 0.5 * interLft1[ia]) ||
// highest density of match (least span)
// span[ia] - span[ib] ||
// highest density of match (least term inter-fuzz)
interIns[ia] - interIns[ib] ||
// earliest start of match
start[ia] - start[ib] ||
// alphabetic
cmp(haystack[idx[ia]], haystack[idx[ib]]) +
(preFilteredItems?.[idx[ib]]?.starred ? 100 : 0) -
(preFilteredItems?.[idx[ia]]?.starred ? 100 : 0)
)
return sortResult
}
}
$: items = filter !== '' ? filteredItems : preFilteredItems
let start: number
let end: number
$: clientHeight = 0
$: height = (100 * clientHeight) / window.innerHeight
function resetScroll() {
const element = document.getElementsByTagName('svelte-virtual-list-viewport')
const firstElement = element.item(0)
if (firstElement) {
firstElement.scrollTop = 0
}
}
$: items && resetScroll()
</script>
<SearchItems
{filter}
items={preFilteredItems}
bind:filteredItems
f={(x) => (x.summary ? x.summary + ' (' + x.path + ')' : x.path)}
{opts}
/>
<ShareModal
bind:this={shareModalScripts}
kind="script"
on:change={() => {
loadScripts()
}}
/>
<ShareModal
bind:this={shareModalFlows}
kind="flow"
on:change={() => {
loadFlows()
}}
/>
<Drawer bind:this={codeViewer} size="900px">
<DrawerContent title={codeViewerObj?.summary ?? ''} on:close={codeViewer.closeDrawer}>
<div slot="submission" class="flex flex-row gap-2 pr-2">
@@ -386,130 +150,5 @@
</div>
{#if tab == 'workspace'}
<CenteredPage>
<div class="flex flex-col md:flex-row gap-2 items-center sm:justify-between w-full">
<div>
<ToggleButtonGroup bind:selected={itemKind}>
<ToggleButton light position="left" value="all" size="sm">All</ToggleButton>
<ToggleButton light position="center" value="script" size="sm">
<div class="flex gap-1 items-center">
<Code2 size="16px" />
Scripts
</div>
</ToggleButton>
<ToggleButton light position="center" value="flow" size="sm">
<div class="flex gap-1 items-center">
<Wind size="16px" />
Flows
</div>
</ToggleButton>
<ToggleButton light position="right" value="app" size="sm">
<div class="flex gap-1 items-center">
<LayoutDashboard size="16px" />
Apps
</div>
</ToggleButton>
</ToggleButtonGroup>
</div>
<div class="relative text-gray-600 w-full">
<!-- svelte-ignore a11y-autofocus -->
<input
autofocus
placeholder="Search Scripts, Flows & Apps"
bind:value={filter}
class="bg-white !h-10 !px-4 !pr-10 !rounded-lg text-sm focus:outline-none"
/>
<button type="submit" class="absolute right-0 top-0 mt-3 mr-4">
<svg
class="h-4 w-4 fill-current"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 56.966 56.966"
style="enable-background:new 0 0 56.966 56.966;"
xml:space="preserve"
width="512px"
height="512px"
>
<path
d="M55.146,51.887L41.588,37.786c3.486-4.144,5.396-9.358,5.396-14.786c0-12.682-10.318-23-23-23s-23,10.318-23,23 s10.318,23,23,23c4.761,0,9.298-1.436,13.177-4.162l13.661,14.208c0.571,0.593,1.339,0.92,2.162,0.92 c0.779,0,1.518-0.297,2.079-0.837C56.255,54.982,56.293,53.08,55.146,51.887z M23.984,6c9.374,0,17,7.626,17,17s-7.626,17-17,17 s-17-7.626-17-17S14.61,6,23.984,6z"
/>
</svg>
</button>
</div>
</div>
{#if owners.length > 0}
<div class="gap-2 w-full flex flex-wrap my-4">
{#each owners as owner (owner)}
<div in:fade animate:flip={{ duration: 200 }}>
<Badge
class="cursor-pointer hover:bg-gray-200"
on:click={() => {
ownerFilter = ownerFilter == owner ? undefined : owner
}}
color={owner === ownerFilter ? 'blue' : 'gray'}
baseClass={owner === ownerFilter ? 'border border-blue-500' : 'border'}
>
{owner}
{#if owner === ownerFilter}&cross;{/if}
</Badge>
</div>
{/each}
</div>
{/if}
<div>
{#if filteredItems == undefined}
<div class="mt-4" />
<Skeleton layout={[[2], 1]} />
{#each new Array(6) as _}
<Skeleton layout={[[4], 0.5]} />
{/each}
{:else if filteredItems.length === 0}
<div class="flex justify-center items-center h-48">
<div class="text-gray-500 text-center">
<div class="text-2xl font-bold">No items found</div>
<div class="text-sm">Try changing your search or filters</div>
</div>
</div>
{:else}
<div
class={classNames('border rounded-md')}
style={`height: calc(100vh - 10em - ${height}vh);`}
>
<VirtualList {items} let:item bind:start bind:end>
{#if item.type == 'script'}
<ScriptRow
starred={item.starred ?? false}
marked={item.marked}
on:change={loadScripts}
script={item}
shareModal={shareModalScripts}
/>
{:else if item.type == 'flow'}
<FlowRow
starred={item.starred ?? false}
marked={item.marked}
on:change={loadFlows}
flow={item}
shareModal={shareModalFlows}
/>
{:else if item.type == 'app'}
<AppRow
starred={item.starred ?? false}
marked={item.marked}
on:change={loadApps}
app={item}
/>
{/if}
</VirtualList>
</div>
<span class="text-xs">{pluralize(items?.length ?? 0, 'item')}</span>
<span class="text-xs">{`(${start} - ${end})`}</span>
{/if}
</div>
</CenteredPage>
<ItemsList {clientHeight} />
{/if}