mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 00:00:46 +00:00
Merge branch 'main' into app-preview-scroll
This commit is contained in:
@@ -123,6 +123,7 @@ async fn list_scripts(
|
||||
AND workspace_id = ?)"
|
||||
.bind(&w_id),
|
||||
);
|
||||
sqlb.and_where_eq("archived", true);
|
||||
} else {
|
||||
sqlb.and_where_eq("archived", false);
|
||||
}
|
||||
|
||||
Generated
+1460
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,8 @@
|
||||
"@sveltejs/package": "^1.0.2",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"@tailwindcss/typography": "^0.5.8",
|
||||
"@types/d3": "^7.4.0",
|
||||
"@types/d3-zoom": "^3.0.2",
|
||||
"@types/node": "^18.11.18",
|
||||
"@types/vscode": "~1.74.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.49.0",
|
||||
@@ -36,6 +38,7 @@
|
||||
"ol": "^7.2.2",
|
||||
"openapi-typescript-codegen": "^0.23.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
"pdfjs-dist": "^3.4.120",
|
||||
"postcss": "^8.4.18",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"prettier": "^2.8.3",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { setContext } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import { Button } from './common'
|
||||
import { Button, ButtonPopup, ButtonPopupItem } from './common'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { OFFSET } from './CronInput.svelte'
|
||||
@@ -26,8 +26,6 @@
|
||||
export let initialArgs: Record<string, any> = {}
|
||||
export let loading = false
|
||||
|
||||
let pathError = ''
|
||||
|
||||
async function createSchedule(path: string) {
|
||||
const { cron, args, enabled } = $scheduleStore
|
||||
|
||||
@@ -49,7 +47,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFlow(): Promise<void> {
|
||||
let loadingSave = false
|
||||
|
||||
async function saveFlow(leave: boolean): Promise<void> {
|
||||
loadingSave = true
|
||||
const flow = cleanInputs($flowStore)
|
||||
const { cron, args, enabled } = $scheduleStore
|
||||
$dirtyStore = false
|
||||
@@ -111,7 +112,12 @@
|
||||
await createSchedule(flow.path)
|
||||
}
|
||||
}
|
||||
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
|
||||
loadingSave = false
|
||||
if (leave) {
|
||||
goto(`/flows/get/${$flowStore.path}?workspace_id=${$workspaceStore}`)
|
||||
} else if (initialPath !== $flowStore.path) {
|
||||
goto(`/flows/edit/${$flowStore.path}?workspace_id=${$workspaceStore}`)
|
||||
}
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
@@ -139,7 +145,7 @@
|
||||
}, 500)
|
||||
}
|
||||
|
||||
const selectedIdStore = writable<string>(selectedId)
|
||||
const selectedIdStore = writable<string>(selectedId ?? 'settings-metadata')
|
||||
|
||||
const scheduleStore = writable<Schedule>({ args: {}, cron: '', enabled: false })
|
||||
const previewArgsStore = writable<Record<string, any>>(initialArgs)
|
||||
@@ -237,12 +243,22 @@
|
||||
<div class="flex flex-row space-x-2">
|
||||
<FlowPreviewButtons />
|
||||
<div class="center-center">
|
||||
<Button
|
||||
disabled={pathError != ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
<ButtonPopup
|
||||
loading={loadingSave}
|
||||
size="sm"
|
||||
on:click={saveFlow}>Save</Button
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={() => saveFlow(false)}
|
||||
>
|
||||
<svelte:fragment slot="main">Save</svelte:fragment>
|
||||
<ButtonPopupItem on:click={() => saveFlow(true)}>Save and exit</ButtonPopupItem>
|
||||
{#if initialPath != ''}
|
||||
<ButtonPopupItem
|
||||
on:click={() => {
|
||||
window.open(`/flows/add?template=${initialPath}`)
|
||||
}}>Fork</ButtonPopupItem
|
||||
>
|
||||
{/if}
|
||||
</ButtonPopup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
{#if showOptions}
|
||||
<ul
|
||||
class="options"
|
||||
transition:fly={{ duration: 200, y: 5 }}
|
||||
transition:fly|local={{ duration: 200, y: 5 }}
|
||||
on:mousedown|preventDefault={handleOptionMousedown}
|
||||
>
|
||||
{#each filtered as option}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
import CenteredPage from './CenteredPage.svelte'
|
||||
import UnsavedConfirmationModal from './common/confirmationModal/UnsavedConfirmationModal.svelte'
|
||||
import { dirtyStore } from './common/confirmationModal/dirtyStore'
|
||||
import { Button, Kbd } from './common'
|
||||
import { Button, ButtonPopup, ButtonPopupItem, Kbd } from './common'
|
||||
import { faChevronDown, faChevronUp, faPen, faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
import Breadcrumb from './common/breadcrumb/Breadcrumb.svelte'
|
||||
import LanguageIcon from './common/languageIcons/LanguageIcon.svelte'
|
||||
@@ -54,7 +54,9 @@
|
||||
script.content = initialCode(language, kind, template)
|
||||
}
|
||||
|
||||
async function editScript(): Promise<void> {
|
||||
let loadingSave = false
|
||||
async function editScript(leave: boolean): Promise<void> {
|
||||
loadingSave = true
|
||||
try {
|
||||
$dirtyStore = false
|
||||
localStorage.removeItem(script.path)
|
||||
@@ -83,11 +85,17 @@
|
||||
kind: script.kind
|
||||
}
|
||||
})
|
||||
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
|
||||
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
|
||||
if (leave) {
|
||||
history.replaceState(history.state, '', `/scripts/edit/${newHash}?step=2`)
|
||||
goto(`/scripts/get/${newHash}?workspace_id=${$workspaceStore}`)
|
||||
} else {
|
||||
await goto(`/scripts/edit/${newHash}?step=2`)
|
||||
script.hash = newHash
|
||||
}
|
||||
} catch (error) {
|
||||
sendUserToast(`Impossible to save the script: ${error.body}`, true)
|
||||
sendUserToast(`Impossible to save the script: ${error.body || error.message}`, true)
|
||||
}
|
||||
loadingSave = false
|
||||
}
|
||||
|
||||
async function changeStep(step: number) {
|
||||
@@ -182,14 +190,24 @@
|
||||
>
|
||||
Next {#if step == 1}<Kbd>Enter</Kbd>{/if}
|
||||
</Button>
|
||||
<Button
|
||||
<ButtonPopup
|
||||
loading={loadingSave}
|
||||
size="sm"
|
||||
variant={step == 1 ? 'border' : 'contained'}
|
||||
disabled={step === 1 && pathError !== ''}
|
||||
btnClasses={step == 1 && initialPath == '' ? 'invisible' : ''}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={editScript}>Save</Button
|
||||
on:click={() => editScript(false)}
|
||||
>
|
||||
<svelte:fragment slot="main">Save</svelte:fragment>
|
||||
<ButtonPopupItem on:click={() => editScript(true)}>Save and exit</ButtonPopupItem>
|
||||
{#if initialPath != ''}
|
||||
<ButtonPopupItem
|
||||
on:click={() => {
|
||||
window.open(`/scripts/add?template=${initialPath}`)
|
||||
}}>Fork</ButtonPopupItem
|
||||
>
|
||||
{/if}
|
||||
</ButtonPopup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
{#if !view}<ChevronDown />{:else}<ChevronUp />{/if}</Button
|
||||
>
|
||||
{#if view}
|
||||
<div class="my-4 px-2" transition:slide><slot /></div>
|
||||
<div class="my-4 px-2" transition:slide|local><slot /></div>
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { getDocument, type PDFDocumentProxy, type PDFPageProxy } from 'pdfjs-dist'
|
||||
import 'pdfjs-dist/build/pdf.worker.entry'
|
||||
import type { AppInput } from '../../inputType'
|
||||
import type { AppEditorContext, ComponentCustomCSS } from '../../types'
|
||||
import { concatCustomCss } from '../../utils'
|
||||
import InputValue from '../helpers/InputValue.svelte'
|
||||
import { throttle } from '../../../../utils'
|
||||
import { Button } from '../../../common'
|
||||
import { Download, Loader2, MoveHorizontal, ZoomIn, ZoomOut } from 'lucide-svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { findGridItem } from '../../editor/appUtils'
|
||||
|
||||
export let id: string
|
||||
export let configuration: Record<string, AppInput>
|
||||
export const staticOutputs: string[] = ['loading']
|
||||
export let customCss: ComponentCustomCSS<'container'> | undefined = undefined
|
||||
|
||||
const { app, mode, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
let source: string | ArrayBuffer | undefined = undefined
|
||||
let wrapper: HTMLDivElement | undefined = undefined
|
||||
let error: string | undefined = undefined
|
||||
let doc: PDFDocumentProxy | undefined = undefined
|
||||
let pages: PDFPageProxy[] = []
|
||||
let zoom: number | undefined = undefined
|
||||
let controlsWidth: number | undefined = undefined
|
||||
let controlsHeight: number | undefined = undefined
|
||||
let pageNumber = 1
|
||||
|
||||
$: if (source == '') {
|
||||
resetDoc()
|
||||
error = 'Set the "Source" attribute of the PDF component'
|
||||
}
|
||||
$: zoom && handleZoom()
|
||||
$: wrapper && loadDocument(source)
|
||||
$: wideView = controlsWidth && controlsWidth > 450
|
||||
|
||||
async function resetDoc() {
|
||||
await doc?.destroy()
|
||||
doc = undefined
|
||||
}
|
||||
|
||||
function handleZoom() {
|
||||
if (zoom && wrapper) {
|
||||
try {
|
||||
renderPdf(false)
|
||||
} catch (err) {
|
||||
error = err?.message ?? (typeof err === 'string' ? err : 'Error loading PDF')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDocument(src: string | ArrayBuffer | undefined) {
|
||||
if (!src) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await resetDoc()
|
||||
doc = await getDocument(src).promise
|
||||
pageNumber = 1
|
||||
await renderPdf(false, false)
|
||||
error = undefined
|
||||
} catch (err) {
|
||||
await resetDoc()
|
||||
error = err?.message ?? (typeof err === 'string' ? err : 'Error loading PDF')
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
|
||||
async function renderPdf(scaleToViewport = true, resizing = false) {
|
||||
if (!(doc && wrapper && zoom)) {
|
||||
return
|
||||
}
|
||||
const scrollPosition = wrapper.scrollTop / wrapper.scrollHeight
|
||||
if (!resizing) {
|
||||
pages = []
|
||||
}
|
||||
const nextPages: typeof pages = []
|
||||
const nextChildren: HTMLCanvasElement[] = []
|
||||
const { width } = wrapper.getBoundingClientRect()
|
||||
let scale = zoom / 100
|
||||
if (scaleToViewport) {
|
||||
const firstViewport = (await doc.getPage(1)).getViewport({ scale: 1 })
|
||||
// Rounded to the first integer that is a multiple of 10 and is less than the viewport width
|
||||
zoom = Math.floor((width / firstViewport.width) * 10) * 10
|
||||
scale = zoom / 100
|
||||
}
|
||||
|
||||
for (let i = 0; i < doc.numPages; i++) {
|
||||
const canvas = document.createElement('canvas')
|
||||
const canvasContext = canvas.getContext('2d')
|
||||
if (!canvasContext) {
|
||||
console.warn('Could not get canvas context for PDF page ' + i)
|
||||
continue
|
||||
}
|
||||
const page = await doc.getPage(i + 1)
|
||||
nextPages.push(page)
|
||||
const viewport = page.getViewport({ scale })
|
||||
canvas.height = viewport.height
|
||||
canvas.width = viewport.width
|
||||
canvas.classList.add('mx-auto', 'my-4', 'shadow-sm')
|
||||
await page.render({ canvasContext, viewport }).promise
|
||||
nextChildren.push(canvas)
|
||||
}
|
||||
while (wrapper.firstChild) {
|
||||
wrapper.removeChild(wrapper.firstChild)
|
||||
}
|
||||
pages = [...nextPages]
|
||||
wrapper.append(...nextChildren)
|
||||
wrapper.scrollTo({
|
||||
top: scrollPosition * wrapper.scrollHeight
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToPage(page: number) {
|
||||
page = pageNumber = minMax(page, 1, pages.length)
|
||||
const offset = (wrapper?.children.item(page - 1) as HTMLCanvasElement | null)?.offsetTop
|
||||
// debugger
|
||||
if (!offset) {
|
||||
return
|
||||
}
|
||||
// controlsHeight + 2px border + half of the top margin
|
||||
const padding = (controlsHeight ? controlsHeight + 2 : 0) + 8
|
||||
wrapper?.scrollTo({
|
||||
top: offset - padding
|
||||
})
|
||||
}
|
||||
|
||||
const throttledScroll = throttle(onScroll, 400)
|
||||
function onScroll() {
|
||||
if (!wrapper) {
|
||||
return
|
||||
}
|
||||
const THRESHOLD = 50
|
||||
let scrollPosition = wrapper.scrollTop + THRESHOLD + (controlsHeight ?? 0)
|
||||
let page = 1
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const canvas = wrapper.children.item(i) as HTMLCanvasElement | null
|
||||
if (scrollPosition < (canvas?.offsetTop ?? wrapper.scrollHeight)) {
|
||||
break
|
||||
}
|
||||
page = i + 1
|
||||
}
|
||||
pageNumber = page
|
||||
}
|
||||
|
||||
function syncZoomValue() {
|
||||
const gridItem = findGridItem($app, id)
|
||||
|
||||
if (gridItem && gridItem.data.configuration.zoom.value !== zoom) {
|
||||
gridItem.data.configuration.zoom.value = zoom
|
||||
}
|
||||
|
||||
$app = $app
|
||||
}
|
||||
|
||||
async function downloadPdf() {
|
||||
if (!doc) {
|
||||
return
|
||||
}
|
||||
const data = await doc.saveDocument()
|
||||
const url = URL.createObjectURL(new Blob([data.buffer]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'document.pdf'
|
||||
link.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
function minMax(value: number, min: number, max: number) {
|
||||
if (value < min) {
|
||||
return min
|
||||
} else if (value > max) {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
$: css = concatCustomCss($app.css?.pdfcomponent, customCss)
|
||||
</script>
|
||||
|
||||
<InputValue {id} input={configuration.source} bind:value={source} />
|
||||
<InputValue {id} input={configuration.zoom} bind:value={zoom} />
|
||||
|
||||
<div class="relative w-full h-full bg-gray-100">
|
||||
{#if source && zoom}
|
||||
{#if pages?.length}
|
||||
<div
|
||||
bind:clientWidth={controlsWidth}
|
||||
bind:clientHeight={controlsHeight}
|
||||
class="fixed flex {$mode !== 'preview'
|
||||
? 'w-[calc(100%-2px)] top-[1px]'
|
||||
: 'w-full top-0'} {wideView
|
||||
? 'justify-center gap-14'
|
||||
: '!justify-between'} overflow-x-auto bg-white border mx-auto py-1"
|
||||
>
|
||||
<div class="flex justify-start items-center px-2 text-gray-600 text-sm">
|
||||
<Button
|
||||
on:click={() => zoom && (zoom -= 10)}
|
||||
disabled={!doc}
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
title="Zoom out"
|
||||
aria-label="Zoom out"
|
||||
btnClasses="!rounded-r-none !px-2"
|
||||
>
|
||||
<ZoomOut size={16} />
|
||||
</Button>
|
||||
{#if wideView}
|
||||
<Button
|
||||
on:click={() => (zoom = 100)}
|
||||
disabled={!doc}
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
title="Reset zoom"
|
||||
aria-label="Reset zoom"
|
||||
btnClasses="!w-[50px] !font-medium !rounded-none !border-l-0 !px-1"
|
||||
>
|
||||
{zoom.toFixed(0)}%
|
||||
</Button>
|
||||
{/if}
|
||||
<Button
|
||||
on:click={() => renderPdf(true, true)}
|
||||
disabled={!doc}
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
title="Scale to viewport"
|
||||
aria-label="Scale to viewport"
|
||||
btnClasses="!rounded-none !border-l-0 !px-2"
|
||||
>
|
||||
<MoveHorizontal size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
on:click={() => zoom && (zoom += 10)}
|
||||
disabled={!doc}
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
title="Zoom in"
|
||||
aria-label="Zoom in"
|
||||
btnClasses="!rounded-l-none !px-2 !border-l-0"
|
||||
>
|
||||
<ZoomIn size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="center-center px-2 text-gray-600 text-sm">
|
||||
<input
|
||||
on:input={({ currentTarget }) => {
|
||||
scrollToPage(currentTarget.valueAsNumber)
|
||||
}}
|
||||
min="1"
|
||||
max={pages.length}
|
||||
value={pageNumber}
|
||||
disabled={!doc}
|
||||
type="number"
|
||||
class="!w-[45px] !px-1 !py-0"
|
||||
/>
|
||||
<span class="whitespace-nowrap pl-1">
|
||||
/ {pages.length}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex justify-end items-center px-2 text-gray-600 text-sm">
|
||||
<Button
|
||||
on:click={downloadPdf}
|
||||
disabled={!doc}
|
||||
size="xs"
|
||||
color="light"
|
||||
variant="border"
|
||||
title="Download PDF"
|
||||
aria-label="Download PDF"
|
||||
btnClasses="!font-medium !px-2"
|
||||
>
|
||||
{#if wideView}
|
||||
<span class="mr-1"> Download </span>
|
||||
{/if}
|
||||
<Download size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
out:fade={{ duration: 200 }}
|
||||
class="absolute inset-0 center-center flex-col text-center text-sm bg-white text-gray-600"
|
||||
>
|
||||
<Loader2 class="animate-spin mb-2" />
|
||||
Loading PDF
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
bind:this={wrapper}
|
||||
on:scroll={throttledScroll}
|
||||
class={twMerge('w-full h-full overflow-auto', css?.container?.class ?? '', 'bg-gray-100')}
|
||||
style="padding-top: {controlsHeight ?? 0}px; {css?.container?.style ?? ''}"
|
||||
/>
|
||||
{/if}
|
||||
{#if $mode !== 'preview' && $selectedComponent === id}
|
||||
<button
|
||||
class="fixed z-10 bottom-0 left-0 px-2 py-0.5 bg-indigo-500/90
|
||||
hover:bg-indigo-500 focus:bg-indigo-500 duration-200 text-white text-2xs"
|
||||
on:click={() => syncZoomValue()}
|
||||
>
|
||||
Sync zoom value
|
||||
</button>
|
||||
{/if}
|
||||
{#if error}
|
||||
<div
|
||||
class="absolute inset-0 z-20 center-center
|
||||
bg-gray-100 text-center text-gray-600 text-sm"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -6,6 +6,7 @@ export { default as AppHtml } from './AppHtml.svelte'
|
||||
export { default as AppIcon } from './AppIcon.svelte'
|
||||
export { default as AppImage } from './AppImage.svelte'
|
||||
export { default as AppMap } from './AppMap.svelte'
|
||||
export { default as AppPdf } from './AppPdf.svelte'
|
||||
export { default as AppPieChart } from './AppPieChart.svelte'
|
||||
export { default as AppScatterChart } from './AppScatterChart.svelte'
|
||||
export { default as AppText } from './AppText.svelte'
|
||||
|
||||
@@ -17,6 +17,10 @@
|
||||
|
||||
$: if (input && !deepEqual(input, lastInput)) {
|
||||
lastInput = JSON.parse(JSON.stringify(input))
|
||||
// Needed because of file uploads
|
||||
if (input?.['value'] instanceof ArrayBuffer) {
|
||||
lastInput.value = input?.['value']
|
||||
}
|
||||
}
|
||||
|
||||
const { worldStore } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
@@ -54,7 +54,12 @@
|
||||
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
|
||||
{+min}
|
||||
</span>
|
||||
<div class="grow" on:pointerdown|stopPropagation>
|
||||
<div
|
||||
class="grow"
|
||||
style="--range-handle-focus: {'#7e9abd'}; --range-handle: {'#7e9abd'}; {css?.bar?.style ??
|
||||
''}"
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<RangeSlider
|
||||
bind:slider
|
||||
bind:values
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
export let configuration: Record<string, AppInput>
|
||||
export let verticalAlignment: 'top' | 'center' | 'bottom' | undefined = undefined
|
||||
export const staticOutputs: string[] = ['result']
|
||||
export let customCss: ComponentCustomCSS<'handle' | 'limits' | 'value'> | undefined = undefined
|
||||
export let customCss: ComponentCustomCSS<'handle' | 'limits' | 'value' | 'bar'> | undefined =
|
||||
undefined
|
||||
|
||||
const { app, worldStore, selectedComponent } = getContext<AppEditorContext>('AppEditorContext')
|
||||
let min = 0
|
||||
@@ -60,7 +61,12 @@
|
||||
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
|
||||
{+min}
|
||||
</span>
|
||||
<div class="grow" on:pointerdown|stopPropagation={() => ($selectedComponent = id)}>
|
||||
<div
|
||||
class="grow"
|
||||
style="--range-handle-focus: {'#7e9abd'}; --range-handle: {'#7e9abd'}; {css?.bar?.style ??
|
||||
''}"
|
||||
on:pointerdown|stopPropagation={() => ($selectedComponent = id)}
|
||||
>
|
||||
<RangeSlider bind:slider bind:values {step} min={+min} max={+max} />
|
||||
</div>
|
||||
<span class={css?.limits?.class ?? ''} style={css?.limits?.style ?? ''}>
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { page } from '$app/stores'
|
||||
import { Alert, Badge, Drawer, DrawerContent } from '$lib/components/common'
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
ButtonPopup,
|
||||
ButtonPopupItem,
|
||||
Drawer,
|
||||
DrawerContent
|
||||
} from '$lib/components/common'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
@@ -537,14 +544,31 @@
|
||||
>
|
||||
<span class="hidden md:inline">Publish</span>
|
||||
</Button>
|
||||
<Button
|
||||
loading={loading.save}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={save}
|
||||
color="dark"
|
||||
size="xs"
|
||||
>
|
||||
<span class="hidden md:inline">Save</span>
|
||||
</Button>
|
||||
{#if appPath == ''}
|
||||
<Button
|
||||
loading={loading.save}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={save}
|
||||
color="dark"
|
||||
size="xs"
|
||||
>
|
||||
<span class="hidden md:inline">Save</span>
|
||||
</Button>
|
||||
{:else}
|
||||
<ButtonPopup
|
||||
loading={loading.save}
|
||||
startIcon={{ icon: faSave }}
|
||||
on:click={save}
|
||||
color="dark"
|
||||
size="xs"
|
||||
>
|
||||
<svelte:fragment slot="main">Save</svelte:fragment>
|
||||
<ButtonPopupItem
|
||||
on:click={() => {
|
||||
window.open(`/apps/add?template=${appPath}`)
|
||||
}}>Fork</ButtonPopupItem
|
||||
>
|
||||
</ButtonPopup>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -37,7 +37,8 @@
|
||||
AppAggridTable,
|
||||
AppDrawer,
|
||||
AppMap,
|
||||
AppSplitpanes
|
||||
AppSplitpanes,
|
||||
AppPdf
|
||||
} from '../../components'
|
||||
import AppMultiSelect from '../../components/inputs/AppMultiSelect.svelte'
|
||||
|
||||
@@ -386,7 +387,19 @@
|
||||
customCss={component.customCss}
|
||||
/>
|
||||
{:else if component.type === 'mapcomponent'}
|
||||
<AppMap {...component} bind:staticOutputs={$staticOutputs[component.id]} />
|
||||
<AppMap
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{:else if component.type === 'pdfcomponent'}
|
||||
<AppPdf
|
||||
configuration={component.configuration}
|
||||
id={component.id}
|
||||
customCss={component.customCss}
|
||||
bind:staticOutputs={$staticOutputs[component.id]}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,7 +31,8 @@ import {
|
||||
SidebarClose,
|
||||
MapPin,
|
||||
FlipHorizontal,
|
||||
FlipVertical
|
||||
FlipVertical,
|
||||
FileText
|
||||
} from 'lucide-svelte'
|
||||
import type { BaseAppComponent } from '../../types'
|
||||
|
||||
@@ -91,6 +92,7 @@ export type VerticalSplitPanesComponent = BaseComponent<'verticalsplitpanescompo
|
||||
export type HorizontalSplitPanesComponent = BaseComponent<'horizontalsplitpanescomponent'> & {
|
||||
panes: number[]
|
||||
}
|
||||
export type PdfComponent = BaseComponent<'pdfcomponent'>
|
||||
|
||||
export type AppComponent = BaseAppComponent &
|
||||
(
|
||||
@@ -129,6 +131,7 @@ export type AppComponent = BaseAppComponent &
|
||||
| MapComponent
|
||||
| VerticalSplitPanesComponent
|
||||
| HorizontalSplitPanesComponent
|
||||
| PdfComponent
|
||||
)
|
||||
|
||||
export type AppComponentDimensions = `${IntRange<
|
||||
@@ -1055,6 +1058,7 @@ Hello \${ctx.username}
|
||||
}
|
||||
},
|
||||
customCss: {
|
||||
bar: { style: '' },
|
||||
handle: { style: '' },
|
||||
limits: { class: '', style: '' },
|
||||
value: { class: '', style: '' }
|
||||
@@ -1101,6 +1105,7 @@ Hello \${ctx.username}
|
||||
},
|
||||
customCss: {
|
||||
handles: { style: '' },
|
||||
bar: { style: '' },
|
||||
limits: { class: '', style: '' },
|
||||
values: { class: '', style: '' }
|
||||
} as const,
|
||||
@@ -1532,5 +1537,35 @@ Hello \${ctx.username}
|
||||
panes: [50, 50],
|
||||
numberOfSubgrids: 2
|
||||
}
|
||||
},
|
||||
pdfcomponent: {
|
||||
name: 'PDF',
|
||||
icon: FileText,
|
||||
dims: '3:8-8:12',
|
||||
data: {
|
||||
id: '',
|
||||
type: 'pdfcomponent',
|
||||
componentInput: undefined,
|
||||
configuration: {
|
||||
source: {
|
||||
type: 'static',
|
||||
value: '/dummy.pdf',
|
||||
fieldType: 'text',
|
||||
fileUpload: {
|
||||
accept: 'application/pdf',
|
||||
convertTo: 'buffer'
|
||||
}
|
||||
},
|
||||
zoom: {
|
||||
fieldType: 'number',
|
||||
type: 'static',
|
||||
value: 100
|
||||
}
|
||||
},
|
||||
customCss: {
|
||||
container: { class: '', style: '' }
|
||||
} as const,
|
||||
card: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ const display: ComponentSet = {
|
||||
'plotlycomponent',
|
||||
'scatterchartcomponent',
|
||||
'timeseriescomponent',
|
||||
'pdfcomponent',
|
||||
'displaycomponent'
|
||||
]
|
||||
} as const
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<input type="text" class="!pr-7" bind:value={value.style} on:focus />
|
||||
{#if value?.style}
|
||||
<button
|
||||
transition:fade={{ duration: 100 }}
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="absolute z-10 top-1.5 right-1 rounded-full p-1 duration-200 hover:bg-gray-200"
|
||||
aria-label="Remove styles"
|
||||
on:click|preventDefault|stopPropagation={() => reset('style')}
|
||||
@@ -44,7 +44,7 @@
|
||||
<input type="text" class="!pr-7" bind:value={value.class} on:focus />
|
||||
{#if value?.class}
|
||||
<button
|
||||
transition:fade={{ duration: 100 }}
|
||||
transition:fade|local={{ duration: 100 }}
|
||||
class="absolute z-10 top-1.5 right-1 rounded-full p-1 duration-200 hover:bg-gray-200"
|
||||
aria-label="Remove classes"
|
||||
on:click|preventDefault|stopPropagation={() => reset('class')}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let startIcon: ButtonType.Icon | undefined = undefined
|
||||
export let endIcon: ButtonType.Icon | undefined = undefined
|
||||
export let spacingSize: ButtonType.Size = size
|
||||
export let loading = false
|
||||
|
||||
let ref: ButtonType.Element
|
||||
|
||||
@@ -33,6 +34,7 @@
|
||||
<div class="flex justy-start items-center">
|
||||
{#if $$slots.main}
|
||||
<Button
|
||||
{loading}
|
||||
{...commonProps}
|
||||
{href}
|
||||
{target}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative z-10" bind:this={menu}>
|
||||
<div class="relative" bind:this={menu}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
on:click|stopPropagation={() => {
|
||||
@@ -63,7 +63,7 @@
|
||||
}
|
||||
show = !show
|
||||
}}
|
||||
class="cursor-pointer hover:bg-gray-100/30"
|
||||
class="relative cursor-pointer hover:bg-gray-100/30"
|
||||
>
|
||||
<slot class="triggerable" name="trigger" />
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { MoreVertical } from 'lucide-svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Badge from '../badge/Badge.svelte'
|
||||
import Button from '../button/Button.svelte'
|
||||
import Row from './Row.svelte'
|
||||
|
||||
@@ -31,7 +32,7 @@
|
||||
export let shareModal: ShareModal
|
||||
export let moveDrawer: MoveDrawer
|
||||
|
||||
let { summary, path, extra_perms, canWrite, workspace_id } = flow
|
||||
let { summary, path, extra_perms, canWrite, workspace_id, archived } = flow
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
@@ -70,11 +71,15 @@
|
||||
>
|
||||
<svelte:fragment slot="badges">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
|
||||
{#if archived}
|
||||
<Badge color="red" baseClass="border">archived</Badge>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if !$userStore?.operator}
|
||||
{#if canWrite}
|
||||
{#if canWrite && !archived}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
@@ -135,7 +140,7 @@
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
href: `/flows/edit/${path}?nodraft=true`,
|
||||
disabled: !canWrite
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
displayName: 'Use as template/Fork',
|
||||
@@ -153,14 +158,15 @@
|
||||
action: () => {
|
||||
moveDrawer.openDrawer(path, summary, 'flow')
|
||||
},
|
||||
disabled: !canWrite
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
displayName: 'Schedule',
|
||||
icon: faCalendarAlt,
|
||||
action: () => {
|
||||
scheduleEditor.openNew(true, path)
|
||||
}
|
||||
},
|
||||
disabled: archived
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
@@ -176,7 +182,7 @@
|
||||
path ? archiveFlow(path) : null
|
||||
},
|
||||
type: 'delete',
|
||||
disabled: !canWrite
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
|
||||
@@ -42,7 +42,8 @@
|
||||
canWrite,
|
||||
lock_error_logs,
|
||||
kind,
|
||||
workspace_id
|
||||
workspace_id,
|
||||
archived
|
||||
} = script
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -76,13 +77,17 @@
|
||||
<Badge color="gray" baseClass="border">{capitalize(kind)}</Badge>
|
||||
{/if}
|
||||
|
||||
{#if archived}
|
||||
<Badge color="red" baseClass="border">archived</Badge>
|
||||
{/if}
|
||||
|
||||
<LanguageBadge {language} />
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="actions">
|
||||
<span class="hidden md:inline-flex gap-x-1">
|
||||
{#if !$userStore?.operator}
|
||||
{#if canWrite}
|
||||
{#if canWrite && !archived}
|
||||
<div>
|
||||
<Button
|
||||
color="light"
|
||||
@@ -143,13 +148,13 @@
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
href: `/scripts/edit/${hash}`,
|
||||
disabled: !canWrite
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
displayName: 'Edit code',
|
||||
icon: faEdit,
|
||||
href: `/scripts/edit/${hash}?step=2`,
|
||||
disabled: !canWrite
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
displayName: 'Use as template',
|
||||
@@ -162,7 +167,7 @@
|
||||
action: () => {
|
||||
moveDrawer.openDrawer(path, summary, 'script')
|
||||
},
|
||||
disabled: !canWrite
|
||||
disabled: !canWrite || archived
|
||||
},
|
||||
{
|
||||
displayName: 'View runs',
|
||||
@@ -174,14 +179,16 @@
|
||||
icon: faCalendarAlt,
|
||||
action: () => {
|
||||
scheduleEditor.openNew(false, path)
|
||||
}
|
||||
},
|
||||
disabled: archived
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: faShare,
|
||||
action: () => {
|
||||
shareModal.openDrawer && shareModal.openDrawer(path, 'script')
|
||||
}
|
||||
},
|
||||
disabled: archived
|
||||
},
|
||||
{
|
||||
displayName: 'Archive',
|
||||
@@ -190,7 +197,7 @@
|
||||
path ? archiveScript(path) : null
|
||||
},
|
||||
type: 'delete',
|
||||
disabled: !canWrite
|
||||
disabled: !canWrite || archived
|
||||
}
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{#if retry}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center rounded border bg-white border-gray-400 text-gray-700 px-1 py-0.5"
|
||||
>
|
||||
<Repeat size={14} />
|
||||
@@ -47,7 +47,7 @@
|
||||
{#if earlyStop}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-white rounded border border-gray-400 text-gray-700 px-1 py-0.5"
|
||||
>
|
||||
<Square size={14} />
|
||||
@@ -58,7 +58,7 @@
|
||||
{#if suspend}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-white rounded border border-gray-400 text-gray-700 px-1 py-0.5"
|
||||
>
|
||||
<PhoneIncoming size={14} />
|
||||
@@ -69,7 +69,7 @@
|
||||
{#if sleep}
|
||||
<Popover notClickable>
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
transition:fade|local={{ duration: 200 }}
|
||||
class="center-center bg-white rounded border border-gray-400 text-gray-700 px-1 py-0.5"
|
||||
>
|
||||
<Bed size={14} />
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
<div class="flex-auto grow" bind:clientHeight={minHeight}>
|
||||
<FlowGraph
|
||||
insertable
|
||||
scroll
|
||||
{minHeight}
|
||||
moving={$moving?.module.id}
|
||||
rebuildOnChange={$flowStore}
|
||||
@@ -157,12 +158,13 @@
|
||||
let indexToRemove = $moving.modules.findIndex((m) => $moving?.module?.id == m.id)
|
||||
$moving.modules.splice(indexToRemove, 1)
|
||||
detail.modules.splice(detail.index, 0, $moving.module)
|
||||
$selectedId = $moving.module.id
|
||||
$moving = undefined
|
||||
} else {
|
||||
await insertNewModuleAtIndex(detail.modules, detail.index ?? 0, detail.detail)
|
||||
$selectedId = detail.modules[detail.index ?? 0].id
|
||||
}
|
||||
$flowStore = $flowStore
|
||||
$selectedId = detail.modules[detail.index ?? 0].id
|
||||
}
|
||||
}}
|
||||
on:newBranch={async ({ detail }) => {
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
|
||||
export let insertable = false
|
||||
export let moving: string | undefined = undefined
|
||||
export let scroll = false
|
||||
|
||||
setContext<{ selectedId: Writable<string | undefined> }>('FlowGraphContext', { selectedId })
|
||||
|
||||
@@ -480,7 +481,7 @@
|
||||
id: des.data.id,
|
||||
position: {
|
||||
x: des.x
|
||||
? des.data.loopDepth * 50 + des.x + width - boxSize.width / 2 - NODE.width / 2
|
||||
? des.data.loopDepth * 50 + des.x + width / 2 - boxSize.width / 2 - NODE.width / 2
|
||||
: 0,
|
||||
y: des.y || 0
|
||||
}
|
||||
@@ -646,19 +647,17 @@
|
||||
|
||||
<div bind:clientWidth={width} class="w-full h-full overflow-hidden relative">
|
||||
{#if width && height}
|
||||
{#key width}
|
||||
<Svelvet
|
||||
highlightEdges={false}
|
||||
locked
|
||||
initialLocation={{ x: width * (initialZoom ?? 1), y: height / 2 }}
|
||||
{initialZoom}
|
||||
{nodes}
|
||||
{width}
|
||||
{edges}
|
||||
{height}
|
||||
background={false}
|
||||
bgColor="rgb(249 250 251)"
|
||||
/>
|
||||
{/key}
|
||||
<Svelvet
|
||||
highlightEdges={false}
|
||||
locked
|
||||
{initialZoom}
|
||||
{nodes}
|
||||
{width}
|
||||
{edges}
|
||||
{height}
|
||||
{scroll}
|
||||
background={false}
|
||||
bgColor="rgb(249 250 251)"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
import TemporaryEdge from '../../interactiveNodes/views/TemporaryEdge.svelte'
|
||||
import { determineD3Instance, zoomInit } from '../..//d3/controllers/d3'
|
||||
|
||||
import EditEdge from '../../editEdges/views/EditEdge.svelte'
|
||||
import { filterByCollapsible } from '../../collapsible/controllers/util'
|
||||
import type { AnchorType } from '../../edges/types/types'
|
||||
import { onDestroy } from 'svelte'
|
||||
|
||||
//these are typscripted as any, however they have been transformed inside of store.ts
|
||||
export let canvasId: string
|
||||
@@ -26,6 +26,7 @@
|
||||
export let initialLocation
|
||||
export let boundary = false
|
||||
export let minimap = false
|
||||
export let scroll = false
|
||||
// here we lookup the store using the unique key
|
||||
const store = findStore(canvasId)
|
||||
const {
|
||||
@@ -41,7 +42,6 @@
|
||||
widthStore,
|
||||
heightStore,
|
||||
d3Scale,
|
||||
edgeEditModal,
|
||||
collapsibleStore
|
||||
} = store
|
||||
$: nodes = Object.values($nodesStore)
|
||||
@@ -101,119 +101,72 @@
|
||||
let d3Translate = { x: 0, y: 0, k: 1 }
|
||||
onMount(() => {
|
||||
// actualizes the d3 instance
|
||||
let edgesd3 = d3
|
||||
.select(`.Edges-${canvasId}`)
|
||||
.call(d3Zoom)
|
||||
.on('wheel.zoom', null)
|
||||
.on('mousewheel.zoom', null)
|
||||
.on('mousemove.zoom', null)
|
||||
.on('DOMMouseScroll.zoom', null)
|
||||
.on('dblclick.zoom', null)
|
||||
let nodesd3 = d3
|
||||
.select(`.Nodes-${canvasId}`)
|
||||
.call(d3Zoom)
|
||||
.on('wheel.zoom', null)
|
||||
.on('mousewheel.zoom', null)
|
||||
.on('mousemove.zoom', null)
|
||||
.on('DOMMouseScroll.zoom', null)
|
||||
.on('dblclick.zoom', null)
|
||||
d3.selectAll('#dot')
|
||||
.call(d3Zoom)
|
||||
.on('wheel.zoom', null)
|
||||
.on('mousewheel.zoom', null)
|
||||
.on('mousemove.zoom', null)
|
||||
.on('DOMMouseScroll.zoom', null)
|
||||
.on('dblclick.zoom', null)
|
||||
|
||||
const nodes = d3.select(`.zoomable`).call(d3Zoom).on('dblclick.zoom', null)
|
||||
|
||||
if (!scroll) {
|
||||
;[nodes].forEach((d3Instance) => {
|
||||
d3Instance
|
||||
.on('wheel.zoom', null)
|
||||
.on('mousewheel.zoom', null)
|
||||
.on('mousemove.zoom', null)
|
||||
.on('DOMMouseScroll.zoom', null)
|
||||
})
|
||||
}
|
||||
|
||||
d3.select('#zoom_in').on('click', function () {
|
||||
try {
|
||||
d3Zoom
|
||||
.scaleBy(edgesd3.transition().duration(250), 1.4)
|
||||
.translateTo(d3Translate.x, d3Translate.y)
|
||||
d3Zoom.scaleBy(nodes.transition().duration(250), 1.4)
|
||||
|
||||
d3Zoom
|
||||
.scaleBy(nodesd3.transition().duration(250), 1.4)
|
||||
.translateTo(d3Translate.x, d3Translate.y)
|
||||
} catch {}
|
||||
// d3Zoom.translateTo(edgesd3, d3Translate.x, d3Translate.y)
|
||||
// d3Zoom.translateTo(nodesd3, d3Translate.x, d3Translate.y)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
})
|
||||
d3.select('#zoom_out').on('click', function () {
|
||||
try {
|
||||
d3Zoom
|
||||
.scaleBy(edgesd3.transition().duration(250), 0.714)
|
||||
.translateTo(d3Translate.x, d3Translate.y)
|
||||
d3Zoom.scaleBy(nodes.transition().duration(250), 0.714)
|
||||
|
||||
d3Zoom
|
||||
.scaleBy(nodesd3.transition().duration(250), 0.714)
|
||||
.translateTo(d3Translate.x, d3Translate.y)
|
||||
} catch {}
|
||||
// d3Zoom.translateTo(edgesd3.transition().duration(0), d3Translate.x, d3Translate.y)
|
||||
// d3Zoom.translateTo(nodesd3.transition().duration(0), d3Translate.x, d3Translate.y)
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
}
|
||||
})
|
||||
d3Translate = zoomInit(d3, canvasId, d3Zoom, d3Translate, initialLocation, initialZoom, d3Scale)
|
||||
// })
|
||||
// d3Translate = zoomInit(d3, canvasId, d3Zoom, d3Translate, initialLocation, initialZoom, d3Scale)
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
d3.select('svg').remove()
|
||||
})
|
||||
|
||||
// This is necessary to make Graphview reactive to changes in initialZoom
|
||||
// When initialZoom changes, then zoomInit will set the zoom/position
|
||||
let prevZoom = initialZoom
|
||||
let prevInitialLocationX = initialLocation.x
|
||||
let prevInitialLocationY = initialLocation.y
|
||||
$: if (
|
||||
initialZoom !== prevZoom ||
|
||||
prevInitialLocationX !== initialLocation.x ||
|
||||
prevInitialLocationY !== initialLocation.y
|
||||
) {
|
||||
prevZoom = initialZoom
|
||||
prevInitialLocationX = initialLocation.x
|
||||
prevInitialLocationY = initialLocation.y
|
||||
d3Translate = zoomInit(d3, canvasId, d3Zoom, d3Translate, initialLocation, initialZoom, d3Scale)
|
||||
}
|
||||
// let prevZoom = initialZoom
|
||||
// let prevInitialLocationX = initialLocation.x
|
||||
// let prevInitialLocationY = initialLocation.y
|
||||
// $: if (
|
||||
// initialZoom !== prevZoom ||
|
||||
// prevInitialLocationX !== initialLocation.x ||
|
||||
// prevInitialLocationY !== initialLocation.y
|
||||
// ) {
|
||||
// prevZoom = initialZoom
|
||||
// prevInitialLocationX = initialLocation.x
|
||||
// prevInitialLocationY = initialLocation.y
|
||||
// d3Translate = zoomInit(d3, canvasId, d3Zoom, d3Translate, initialLocation, initialZoom, d3Scale)
|
||||
// }
|
||||
|
||||
// moves canvas when you click on the minimap
|
||||
// handles case for when minimap sends message back to initiate translation event (click to traverse minimap)
|
||||
// moves camera to the clicked node
|
||||
function miniMapClick(event) {
|
||||
// onclick in case of boundless minimap
|
||||
if (!boundary) {
|
||||
// For edges
|
||||
d3.select(`.Edges-${key}`)
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(d3Zoom.translateTo, event.detail.x, event.detail.y)
|
||||
// For nodes
|
||||
d3.select(`.Nodes-${key}`)
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(d3Zoom.translateTo, event.detail.x, event.detail.y)
|
||||
}
|
||||
// handles case for when minimap has a boundary
|
||||
else {
|
||||
// For edges
|
||||
d3.select(`.Edges-${key}`)
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(d3Zoom.translateTo, event.detail.x, event.detail.y)
|
||||
// For nodes
|
||||
d3.select(`.Nodes-${key}`)
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(d3Zoom.translateTo, event.detail.x, event.detail.y)
|
||||
}
|
||||
}
|
||||
|
||||
const key = canvasId
|
||||
function handleZoom(e) {
|
||||
if (!$movementStore) return
|
||||
//add a store that contains the current value of the d3-zoom's scale to be used in onMouseMove function
|
||||
d3Scale.set(e.transform.k)
|
||||
// should not run d3.select below if backgroundStore is false
|
||||
if (backgroundStore) {
|
||||
d3.select(`#background-${canvasId}`)
|
||||
.attr('x', e.transform.x)
|
||||
.attr('y', e.transform.y)
|
||||
.attr('width', gridSize * e.transform.k)
|
||||
.attr('height', gridSize * e.transform.k)
|
||||
.selectAll('#dot')
|
||||
.attr('x', (gridSize * e.transform.k) / 2 - dotSize / 2)
|
||||
.attr('y', (gridSize * e.transform.k) / 2 - dotSize / 2)
|
||||
.attr('opacity', Math.min(e.transform.k, 1))
|
||||
}
|
||||
// transform 'g' SVG elements (edge, edge text, edge anchor)
|
||||
d3.select(`.Edges-${canvasId} g`).attr('transform', e.transform)
|
||||
// transform div elements (nodes)
|
||||
@@ -231,103 +184,84 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $edgeEditModal}
|
||||
<EditEdge edgeId={`${$edgeEditModal}`} {canvasId} />
|
||||
{/if}
|
||||
<!-- This is the container that holds GraphView and we have disabled right click functionality to prevent a sticking behavior -->
|
||||
<div id="graphview-container">
|
||||
<div class={`Nodes Nodes-${canvasId}`} on:contextmenu|preventDefault>
|
||||
<!-- This container is transformed by d3zoom -->
|
||||
<div class={`Node Node-${canvasId}`}>
|
||||
{#each filteredNodes as node}
|
||||
{#if node.data.html}
|
||||
<Node {node} {canvasId} nodeId={node.id}>{@html node.data.html}</Node>
|
||||
{:else if node.data.custom}
|
||||
<Node isCustom {node} {canvasId} nodeId={node.id}>
|
||||
<svelte:component
|
||||
this={node.data.custom.component}
|
||||
on:new={(e) => node?.data?.custom?.cb?.('new', e.detail)}
|
||||
on:delete={(e) => node?.data?.custom?.cb?.('delete', e.detail)}
|
||||
on:select={(e) => node?.data?.custom?.cb?.('select', e.detail)}
|
||||
on:insert={(e) => node?.data?.custom?.cb?.('insert', e.detail)}
|
||||
on:newBranch={(e) => node?.data?.custom?.cb?.('newBranch', e.detail)}
|
||||
on:deleteBranch={(e) => node?.data?.custom?.cb?.('deleteBranch', e.detail)}
|
||||
on:move={(e) => node?.data?.custom?.cb?.('move', e.detail)}
|
||||
{...node.data.custom.props}
|
||||
/>
|
||||
</Node>
|
||||
<div class="zoomable">
|
||||
<!-- This is the container that holds GraphView and we have disabled right click functionality to prevent a sticking behavior -->
|
||||
<div id="graphview-container">
|
||||
<div class={`Nodes Nodes-${canvasId}`} on:contextmenu|preventDefault>
|
||||
<!-- This container is transformed by d3zoom -->
|
||||
<div class={`Node Node-${canvasId}`}>
|
||||
{#each filteredNodes as node}
|
||||
{#if node.data.html}
|
||||
<Node {node} {canvasId} nodeId={node.id}>{@html node.data.html}</Node>
|
||||
{:else if node.data.custom}
|
||||
<Node isCustom {node} {canvasId} nodeId={node.id}>
|
||||
<svelte:component
|
||||
this={node.data.custom.component}
|
||||
on:new={(e) => node?.data?.custom?.cb?.('new', e.detail)}
|
||||
on:delete={(e) => node?.data?.custom?.cb?.('delete', e.detail)}
|
||||
on:select={(e) => node?.data?.custom?.cb?.('select', e.detail)}
|
||||
on:insert={(e) => node?.data?.custom?.cb?.('insert', e.detail)}
|
||||
on:newBranch={(e) => node?.data?.custom?.cb?.('newBranch', e.detail)}
|
||||
on:deleteBranch={(e) => node?.data?.custom?.cb?.('deleteBranch', e.detail)}
|
||||
on:move={(e) => node?.data?.custom?.cb?.('move', e.detail)}
|
||||
{...node.data.custom.props}
|
||||
/>
|
||||
</Node>
|
||||
{:else}
|
||||
<Node {node} {canvasId} nodeId={node.id}>{node.data.label}</Node>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- rendering dots on the background depending on the zoom level -->
|
||||
<svg
|
||||
class={`Edges Edges-${canvasId}`}
|
||||
viewBox="0 0 {$widthStore} {$heightStore}"
|
||||
on:contextmenu|preventDefault
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={`background-${canvasId}`}
|
||||
x="0"
|
||||
y="0"
|
||||
width={gridSize}
|
||||
height={gridSize}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<circle
|
||||
id="dot"
|
||||
cx={gridSize / 2 - dotSize / 2}
|
||||
cy={gridSize / 2 - dotSize / 2}
|
||||
r="0.5"
|
||||
style="fill: gray"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
<!-- <g> tag defines which edge type to render depending on properties of edge object -->
|
||||
<g>
|
||||
{#each filteredEdges as edge}
|
||||
{#if edge.type === 'smoothstep'}
|
||||
<SmoothStepEdge {edge} {canvasId} />
|
||||
{:else if edge.type === 'step'}
|
||||
<StepEdge {edge} {canvasId} />
|
||||
{:else}
|
||||
<Node {node} {canvasId} nodeId={node.id}>{node.data.label}</Node>
|
||||
<SimpleBezierEdge edgeId={edge.id} {canvasId} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<!-- {#each filteredResizeNodes as res}
|
||||
<ResizeNode resizeId={res.id} {canvasId} />
|
||||
{/each} -->
|
||||
|
||||
{#each potentialAnchors as potentialAnchor}
|
||||
<PotentialAnchor
|
||||
{canvasId}
|
||||
x={potentialAnchor.positionX}
|
||||
y={potentialAnchor.positionY}
|
||||
potentialAnchorId={potentialAnchor.id}
|
||||
/>
|
||||
{#each tempEdges as temporaryEdge}
|
||||
<TemporaryEdge {temporaryEdge} />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- {#each filteredAnchors as anchor} -->
|
||||
<!-- note that these are SVG -->
|
||||
<!-- <EdgeAnchor x={anchor.positionX} y={anchor.positionY} /> -->
|
||||
<!-- {/each} -->
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- rendering dots on the background depending on the zoom level -->
|
||||
<svg
|
||||
class={`Edges Edges-${canvasId}`}
|
||||
viewBox="0 0 {$widthStore} {$heightStore}"
|
||||
on:contextmenu|preventDefault
|
||||
>
|
||||
<defs>
|
||||
<pattern
|
||||
id={`background-${canvasId}`}
|
||||
x="0"
|
||||
y="0"
|
||||
width={gridSize}
|
||||
height={gridSize}
|
||||
patternUnits="userSpaceOnUse"
|
||||
>
|
||||
<circle
|
||||
id="dot"
|
||||
cx={gridSize / 2 - dotSize / 2}
|
||||
cy={gridSize / 2 - dotSize / 2}
|
||||
r="0.5"
|
||||
style="fill: gray"
|
||||
/>
|
||||
</pattern>
|
||||
</defs>
|
||||
|
||||
{#if $backgroundStore}
|
||||
<rect width="100%" height="100%" style="fill: url(#background-{canvasId});" />
|
||||
{/if}
|
||||
|
||||
<!-- <g> tag defines which edge type to render depending on properties of edge object -->
|
||||
<g>
|
||||
{#each filteredEdges as edge}
|
||||
{#if edge.type === 'smoothstep'}
|
||||
<SmoothStepEdge {edge} {canvasId} />
|
||||
{:else if edge.type === 'step'}
|
||||
<StepEdge {edge} {canvasId} />
|
||||
{:else}
|
||||
<SimpleBezierEdge edgeId={edge.id} {canvasId} />
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
{#each tempEdges as temporaryEdge}
|
||||
<TemporaryEdge {temporaryEdge} />
|
||||
{/each}
|
||||
|
||||
<!-- {#each filteredAnchors as anchor} -->
|
||||
<!-- note that these are SVG -->
|
||||
<!-- <EdgeAnchor x={anchor.positionX} y={anchor.positionY} /> -->
|
||||
<!-- {/each} -->
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
<div id="buttons">
|
||||
<button id="zoom_in">+</button>
|
||||
<button id="zoom_out">-</button>
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
export let editable: boolean = false
|
||||
export let resizable: boolean = true
|
||||
export let highlightEdges: boolean = true
|
||||
export let scroll: boolean = false
|
||||
|
||||
// generates a unique string for each svelvet component's unique store instance
|
||||
// creates a store that uses the unique sting as the key to create and look up the corresponding store
|
||||
@@ -103,7 +104,16 @@
|
||||
class="Svelvet"
|
||||
style={`width: ${width}px; height: ${height}px; background-color: ${bgColor};`}
|
||||
>
|
||||
<GraphView {canvasId} {width} {height} {initialLocation} {initialZoom} {boundary} {minimap} />
|
||||
<GraphView
|
||||
{scroll}
|
||||
{canvasId}
|
||||
{width}
|
||||
{height}
|
||||
{initialLocation}
|
||||
{initialZoom}
|
||||
{boundary}
|
||||
{minimap}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// import { zoom, zoomTransform } from 'd3-zoom';
|
||||
// import { select, selectAll } from 'd3-selection';
|
||||
import type { ZoomBehavior } from 'd3-zoom';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export function zoomInit(
|
||||
@@ -54,7 +55,7 @@ export function zoomInit(
|
||||
// create d3 instance conditionally based on boundary prop
|
||||
export function determineD3Instance(
|
||||
boundary,
|
||||
d3,
|
||||
d3: { zoom: () => ZoomBehavior<any, any> },
|
||||
nodeSelected,
|
||||
width,
|
||||
height,
|
||||
@@ -65,7 +66,7 @@ export function determineD3Instance(
|
||||
canvasId,
|
||||
d3Scale,
|
||||
handleZoom
|
||||
) {
|
||||
): ZoomBehavior<any, any> {
|
||||
if (boundary) {
|
||||
return d3
|
||||
.zoom()
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import type uFuzzy from '@leeoniya/ufuzzy'
|
||||
import { Code2, LayoutDashboard } from 'lucide-svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
|
||||
export let filter = ''
|
||||
|
||||
@@ -51,6 +52,7 @@
|
||||
async function loadScripts(): Promise<void> {
|
||||
const loadedScripts = await ScriptService.listScripts({
|
||||
workspace: $workspaceStore!,
|
||||
showArchived: archived ? true : undefined,
|
||||
perPage: 300
|
||||
})
|
||||
|
||||
@@ -68,7 +70,12 @@
|
||||
}
|
||||
|
||||
async function loadFlows(): Promise<void> {
|
||||
flows = (await FlowService.listFlows({ workspace: $workspaceStore! })).map((x: Flow) => {
|
||||
flows = (
|
||||
await FlowService.listFlows({
|
||||
workspace: $workspaceStore!,
|
||||
showArchived: archived ? true : undefined
|
||||
})
|
||||
).map((x: Flow) => {
|
||||
return {
|
||||
canWrite:
|
||||
canWrite(x.path, x.extra_perms, $userStore) &&
|
||||
@@ -140,7 +147,11 @@
|
||||
if ($userStore && $workspaceStore) {
|
||||
loadScripts()
|
||||
loadFlows()
|
||||
loadApps()
|
||||
if (!archived) {
|
||||
loadApps()
|
||||
} else {
|
||||
apps = []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +211,8 @@
|
||||
}
|
||||
|
||||
$: items && resetScroll()
|
||||
|
||||
let archived = false
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
@@ -284,8 +297,12 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
|
||||
<div class="relative">
|
||||
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
|
||||
<div class="absolute -bottom-2 right-0">
|
||||
<Toggle size="xs" bind:checked={archived} options={{ right: 'Show archived' }} /></div
|
||||
>
|
||||
</div>
|
||||
<div>
|
||||
{#if filteredItems == undefined}
|
||||
<div class="mt-4" />
|
||||
|
||||
@@ -184,7 +184,7 @@ export function removeItemAll<T>(arr: T[], value: T) {
|
||||
}
|
||||
|
||||
export async function isOwner(path: string, user: UserExt, workspace: string): Promise<boolean> {
|
||||
if (user.is_admin && ((workspace == 'starter' || workspace == 'admin') && user.is_super_admin)) {
|
||||
if (user.is_admin && (workspace == 'starter' || workspace == 'admin') && user.is_super_admin) {
|
||||
return true
|
||||
} else if (workspace == 'starter' || workspace == 'admin') {
|
||||
return false
|
||||
@@ -255,8 +255,8 @@ export function allTrue(dict: { [id: string]: boolean }): boolean {
|
||||
}
|
||||
|
||||
function subtractSeconds(date: Date, seconds: number): Date {
|
||||
date.setSeconds(date.getSeconds() - seconds);
|
||||
return date;
|
||||
date.setSeconds(date.getSeconds() - seconds)
|
||||
return date
|
||||
}
|
||||
|
||||
export function forLater(scheduledString: string): boolean {
|
||||
@@ -484,7 +484,7 @@ export function scriptPathToHref(path: string): string {
|
||||
export async function getScriptByPath(path: string): Promise<{
|
||||
content: string
|
||||
language: SupportedLanguage
|
||||
schema: any,
|
||||
schema: any
|
||||
description: string
|
||||
}> {
|
||||
if (path.startsWith('hub/')) {
|
||||
@@ -549,7 +549,6 @@ export async function loadHubFlows() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export async function loadHubApps() {
|
||||
try {
|
||||
const apps = (await AppService.listHubApps()).apps ?? []
|
||||
@@ -583,7 +582,6 @@ export function flowToHubUrl(flow: Flow): URL {
|
||||
return url
|
||||
}
|
||||
|
||||
|
||||
export function appToHubUrl(staticApp: any): URL {
|
||||
const url = new URL('https://hub.windmill.dev/apps/add')
|
||||
url.searchParams.append('app', encodeState(staticApp))
|
||||
@@ -631,7 +629,9 @@ export function scriptLangToEditorLang(
|
||||
}
|
||||
|
||||
export async function copyToClipboard(value?: string, sendToast = true): Promise<boolean> {
|
||||
if (!value) { return false }
|
||||
if (!value) {
|
||||
return false
|
||||
}
|
||||
|
||||
let success = false
|
||||
if (navigator?.clipboard) {
|
||||
@@ -660,14 +660,39 @@ export function capitalize(word: string): string {
|
||||
}
|
||||
|
||||
export function addWhitespaceBeforeCapitals(word?: string): string {
|
||||
if (!word) { return '' }
|
||||
if (!word) {
|
||||
return ''
|
||||
}
|
||||
return word.replace(/([A-Z])/g, ' $1').trim()
|
||||
}
|
||||
|
||||
export function isCloudHosted(): boolean {
|
||||
return (get(page)?.url?.hostname == 'app.windmill.dev')
|
||||
return get(page)?.url?.hostname == 'app.windmill.dev'
|
||||
}
|
||||
|
||||
export function isObject(obj: any) {
|
||||
return typeof obj === 'object'
|
||||
}
|
||||
|
||||
export function debounce(func: (...args: any[]) => any, wait: number) {
|
||||
let timeout: any
|
||||
return function (...args: any[]) {
|
||||
// @ts-ignore
|
||||
const context = this
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func.apply(context, args), wait)
|
||||
}
|
||||
}
|
||||
|
||||
export function throttle<T>(func: (...args: any[]) => T, wait: number) {
|
||||
let timeout: any
|
||||
return function (...args: any[]) {
|
||||
if (!timeout) {
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null
|
||||
// @ts-ignore
|
||||
func.apply(this, args)
|
||||
}, wait)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,9 @@
|
||||
return (
|
||||
pathname.startsWith('/apps') ||
|
||||
pathname.startsWith('/flows/add') ||
|
||||
pathname.startsWith('/flows/edit')
|
||||
pathname.startsWith('/flows/edit') ||
|
||||
pathname.startsWith('/scripts/add') ||
|
||||
pathname.startsWith('/scripts/edit')
|
||||
)
|
||||
}
|
||||
afterNavigate((n) => {
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
})
|
||||
Object.assign(flow, template)
|
||||
const oldPath = flow.path.split('/')
|
||||
flow.path = `u/${$userStore?.username}/${oldPath[oldPath.length - 1]}`
|
||||
flow.path = `u/${$userStore?.username.split('@')[0]}/${oldPath[oldPath.length - 1]}_fork`
|
||||
flow = flow
|
||||
goto('?', { replaceState: true })
|
||||
selectedId = 'settings-metadata'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { page } from '$app/stores'
|
||||
import FlowBuilder from '$lib/components/FlowBuilder.svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { decodeArgs, decodeState, emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { decodeArgs, decodeState, sendUserToast } from '$lib/utils'
|
||||
import { initFlow } from '$lib/components/flows/flowStore'
|
||||
import { dirtyStore } from '$lib/components/common/confirmationModal/dirtyStore'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user