feat(frontend): full height component (#3676)

* feat(frontend): wip

* feat(frontend): Full Height component

* feat(frontend): wip

* feat(frontend): add fullHeight indicator

* feat(frontend): add indicator for component that won't render

* feat(frontend): update collision algo

* fix(frontend): Full height component done

* fix(frontend): Remove the hidden check from preview

* feat(frontend): code cleanup

* feat(frontend): code cleanup

* feat(frontend): code cleanup

* feat(frontend): code cleanup

* feat(frontend): Fix height issue + fix toggle display

* feat(frontend): Fix insertNewGridITem

* feat(frontend): code cleanup

* feat(frontend): mobile fullHeigt

* feat(frontend): fix wording

* feat(frontend): add missing constants

* feat(frontend): fix typo

* feat(frontend): remove dead code

* feat(frontend): improve the structure

* feat(frontend): done

---------

Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
This commit is contained in:
Faton Ramadani
2024-05-15 14:52:05 +02:00
committed by GitHub
parent 001c010974
commit 6ff6a603f7
15 changed files with 277 additions and 59 deletions
@@ -20,7 +20,7 @@
import { columnConfiguration } from '../gridUtils'
import { HiddenComponent } from '../components'
import { deepEqual } from 'fast-equals'
import { dfs } from './appUtils'
import { dfs, maxHeight } from './appUtils'
import { BG_PREFIX, migrateApp } from '../utils'
import { workspaceStore, enterpriseLicense } from '$lib/stores'
import DarkModeObserver from '$lib/components/DarkModeObserver.svelte'
@@ -161,13 +161,17 @@
}
}
}
let appHeight: number = 0
$: maxRow = maxHeight($appStore.grid, appHeight, $breakpoint)
</script>
<DarkModeObserver on:change={onThemeChange} />
<svelte:window on:hashchange={hashchange} on:resize={resizeWindow} />
<div class="relative h-full">
<div class="relative h-full" bind:clientHeight={appHeight}>
<div id="app-editor-top-level-drawer" />
<div id="app-editor-select" />
@@ -201,7 +205,7 @@
class={twMerge(
'p-2 overflow-visible',
app.css?.['app']?.['grid']?.class ?? '',
'wm-app-grid subgrid'
'wm-app-grid subgrid'
)}
bind:clientWidth={$parentWidth}
>
@@ -210,16 +214,24 @@
allIdsInPath={$allIdsInPath}
items={app.grid}
let:dataItem
rowHeight={36}
let:hidden
cols={columnConfiguration}
gap={[4, 2]}
{maxRow}
breakpoint={$breakpoint}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class={'h-full w-full center-center'}
on:pointerdown={() => ($selectedComponent = [dataItem.id])}
>
<Component render={true} component={dataItem.data} selected={false} locked={true} />
<Component
render={true}
component={dataItem.data}
selected={false}
locked={true}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
{hidden}
/>
</div>
</GridViewer>
</div>
@@ -228,6 +240,7 @@
{#if isLocked}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:click={() => (isLocked = false)}
class="absolute inset-0 center-center bg-black/20 z-50 backdrop-blur-[1px] cursor-pointer"
@@ -1,7 +1,7 @@
<script lang="ts">
import { classNames } from '$lib/utils'
import type { AppViewerContext } from '../types'
import { Anchor, Bug, Expand, Move, Network, Pen, Plug2 } from 'lucide-svelte'
import { Anchor, ArrowDownFromLine, Bug, Expand, Move, Network, Pen, Plug2 } from 'lucide-svelte'
import { createEventDispatcher, getContext } from 'svelte'
import Popover from '$lib/components/Popover.svelte'
import { Button, Popup } from '$lib/components/common'
@@ -21,6 +21,8 @@
export let hasInlineEditor: boolean = false
export let inlineEditorOpened: boolean = false
export let errorHandledByComponent: boolean = false
export let fullHeight: boolean = false
//export let willNotDisplay: boolean = false
const dispatch = createEventDispatcher()
@@ -115,6 +117,40 @@
<DecisionTreeDebug id={component.id} nodes={component.nodes ?? []} />
{/if}
<!-- {#if willNotDisplay}
<Popover>
<svelte:fragment slot="text">
This component won't render, because an other component above it is set to fill the
height.
</svelte:fragment>
<div
title="Fill height"
class={classNames(
'px-1 text-2xs py-0.5 font-bold w-fit border cursor-pointer rounded-sm',
'bg-red-100 text-red-600 border-red-500 hover:bg-red-200 hover:text-red-800'
)}
>
<EyeOff aria-label="Expand position" size={14} />
</div>
</Popover>
{/if} -->
<button
title="Fill height"
class={classNames(
'px-1 text-2xs py-0.5 font-bold w-fit border cursor-pointer rounded-sm',
'bg-indigo-100 text-indigo-600 border-indigo-500 hover:bg-indigo-200 hover:text-indigo-800'
)}
on:click={() => dispatch('fillHeight')}
on:pointerdown|stopPropagation
>
<ArrowDownFromLine
aria-label="Expand position"
size={14}
class={fullHeight ? 'text-orange-500' : ''}
/>
</button>
<button
title="Expand"
class={classNames(
@@ -86,10 +86,10 @@
</div>
<div>
{policy.on_behalf_of ? `Author ${policy.on_behalf_of_email}` : ''}
<Tooltip
>The scripts will be run on behalf of the author and a tight policy ensure security about
the possible inputs of the runnables.</Tooltip
>
<Tooltip>
The scripts will be run on behalf of the author and a tight policy ensure security about
the possible inputs of the runnables.
</Tooltip>
</div>
</div>
</div>
@@ -125,9 +125,8 @@
$app.grid = e.detail
}}
let:dataItem
rowHeight={36}
let:hidden
cols={columnConfiguration}
gap={[4, 2]}
>
<ComponentWrapper
id={dataItem.id}
@@ -139,10 +138,12 @@
>
<GridEditorMenu id={dataItem.id}>
<Component
{hidden}
render={true}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
on:lock={() => {
const gridItem = findGridItem($app, dataItem.id)
if (gridItem) {
@@ -156,6 +157,14 @@
expandGriditem($app.grid, dataItem.id, $breakpoint)
$app = $app
}}
on:fillHeight={() => {
const gridItem = findGridItem($app, dataItem.id)
const b = $breakpoint === 'sm' ? 3 : 12
if (gridItem?.[b]) {
gridItem[b].fullHeight = !gridItem[b].fullHeight
}
$app = $app
}}
/>
</GridEditorMenu>
</ComponentWrapper>
@@ -1,4 +1,8 @@
<script lang="ts">
import { ROW_GAP_X, ROW_GAP_Y, ROW_HEIGHT } from './appUtils'
import type { EditorBreakpoint } from '../types'
import { onMount, createEventDispatcher } from 'svelte'
import type { FilledItem } from '../svelte-grid/types'
@@ -11,10 +15,12 @@
type T = $$Generic
export let items: FilledItem<T>[]
export let rowHeight: number
export let rowHeight: number = ROW_HEIGHT
export let cols: [number, number][]
export let gap = [10, 10]
export let gap = [ROW_GAP_X, ROW_GAP_Y]
export let throttleUpdate = 100
export let maxRow: number
export let breakpoint: EditorBreakpoint
export let allIdsInPath: string[] | undefined = undefined
export let containerWidth: number | undefined = undefined
@@ -81,9 +87,15 @@
{@const width =
Math.min(getComputedCols, item[getComputedCols] && item[getComputedCols].w) * xPerPx -
gapX * 2}
{@const height = (item[getComputedCols] && item[getComputedCols].h) * yPerPx - gapY * 2}
{@const height =
(item?.[breakpoint === 'sm' ? 3 : 12]?.fullHeight
? maxRow - item[getComputedCols].y
: item[getComputedCols] && item[getComputedCols].h) *
yPerPx -
gapY * 2}
{@const top = (item[getComputedCols] && item[getComputedCols].y) * yPerPx + gapY}
{@const left = (item[getComputedCols] && item[getComputedCols].x) * xPerPx + gapX}
<div
class="svlt-grid-item"
style="width: {width}px; height:{height}px; {onTop
@@ -91,7 +103,7 @@
: ''} top: {top}px; left: {left}px;"
>
{#if item[getComputedCols]}
<slot dataItem={item} item={item[getComputedCols]} />
<slot dataItem={item} item={item[getComputedCols]} hidden={false} />
{/if}
</div>
{/each}
@@ -6,7 +6,7 @@
import { columnConfiguration, isFixed, toggleFixed } from '../gridUtils'
import Grid from '../svelte-grid/Grid.svelte'
import type { AppEditorContext, AppViewerContext, GridItem } from '../types'
import { expandGriditem, findGridItem, selectId } from './appUtils'
import { expandGriditem, findGridItem, maxHeight, selectId } from './appUtils'
import Component from './component/Component.svelte'
import ComponentWrapper from './component/ComponentWrapper.svelte'
import GridViewer from './GridViewer.svelte'
@@ -66,6 +66,8 @@
}
let container: HTMLElement | undefined = undefined
$: maxRow = maxHeight($app.subgrids?.[subGridId] ?? [], containerHeight ?? 0, $breakpoint)
</script>
<div
@@ -105,9 +107,8 @@
}}
selectedIds={$selectedComponent}
let:dataItem
rowHeight={36}
let:hidden
cols={columnConfiguration}
gap={[4, 2]}
scroller={container}
parentWidth={$parentWidth - 17}
{containerWidth}
@@ -123,6 +124,8 @@
>
<GridEditorMenu id={dataItem.id}>
<Component
{hidden}
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
@@ -146,6 +149,15 @@
)
$app = $app
}}
on:fillHeight={() => {
const gridItem = findGridItem($app, dataItem.id)
const b = $breakpoint === 'sm' ? 3 : 12
if (gridItem?.[b]) {
gridItem[b].fullHeight = !gridItem[b].fullHeight
}
$app = $app
}}
/>
</GridEditorMenu>
</ComponentWrapper>
@@ -156,11 +168,12 @@
allIdsInPath={$allIdsInPath}
items={$app.subgrids?.[subGridId] ?? []}
let:dataItem
rowHeight={36}
let:hidden
cols={columnConfiguration}
gap={[4, 2]}
breakpoint={$breakpoint}
parentWidth={$parentWidth - 17}
{containerWidth}
{maxRow}
>
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
@@ -168,10 +181,12 @@
class={classNames('h-full w-full center-center', 'top-0')}
>
<Component
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
render={visible}
component={dataItem.data}
selected={Boolean($selectedComponent?.includes(dataItem.id))}
locked={isFixed(dataItem)}
{hidden}
/>
</div>
</GridViewer>
@@ -246,7 +246,8 @@ export function createNewGridItem(
const newComponent = {
fixed: false,
x: 0,
y: 0
y: 0,
fullHeight: false
}
let newData: AppComponent = JSON.parse(JSON.stringify(data))
@@ -378,6 +379,7 @@ export function insertNewGridItem(
const id = keepId ?? getNextGridItemId(app)
const data = builddata(id)
if (data.type == 'aggridcomponentee' && !get(enterpriseLicense)) {
sendUserToast('AgGrid Enterprise Edition require Windmill Enterprise Edition', true)
throw Error('AgGrid Enterprise Edition require Windmill Enterprise Edition')
@@ -972,3 +974,29 @@ export function collectOneOfFields(fields: AppInputs, app: App) {
})
)
}
export const ROW_HEIGHT = 36
export const ROW_GAP_Y = 2
export const ROW_GAP_X = 4
export function maxHeight(
grid: GridItem[],
windowHeight: number,
breakpoint: EditorBreakpoint = 'lg'
) {
const totalRowHeight = ROW_HEIGHT + ROW_GAP_Y
let maxRows = Math.floor((windowHeight - ROW_GAP_Y) / totalRowHeight)
if (!grid.length) {
return maxRows
}
const breakpointKey = breakpoint === 'sm' ? 3 : 12
const maxRowPerGrid = grid.reduce((max, item) => {
const y = item[breakpointKey].y
const h = item[breakpointKey].h
return Math.max(max, y + h)
}, 0)
return Math.max(maxRowPerGrid, maxRows)
}
@@ -80,6 +80,8 @@
export let selected: boolean
export let locked: boolean = false
export let render: boolean
export let hidden: boolean
export let fullHeight: boolean
const { mode, app, hoverStore, connectingInput } =
getContext<AppViewerContext>('AppViewerContext')
@@ -115,7 +117,9 @@
}
}}
on:mouseout|stopPropagation={mouseOut}
class="h-full flex flex-col w-full component {initializing ? 'overflow-hidden h-0' : ''}"
class="h-full flex flex-col w-full component {initializing
? 'overflow-hidden h-0'
: ''} {hidden && $mode === 'preview' ? 'hidden' : ''} "
>
{#if $mode !== 'preview'}
<ComponentHeader
@@ -129,9 +133,11 @@
hover={$hoverStore === component.id}
{component}
{selected}
{fullHeight}
connecting={$connectingInput.opened}
on:lock
on:expand
on:fillHeight
{locked}
{inlineEditorOpened}
hasInlineEditor={component.type === 'textcomponent' &&
@@ -7,7 +7,7 @@
import TableActions from './TableActions.svelte'
import StaticInputEditor from './inputEditor/StaticInputEditor.svelte'
import ConnectedInputEditor from './inputEditor/ConnectedInputEditor.svelte'
import { classNames, getModifierKey } from '$lib/utils'
import { classNames, getModifierKey, isMac } from '$lib/utils'
import { buildExtraLib } from '../../utils'
import Recompute from './Recompute.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
@@ -38,6 +38,7 @@
import MenuItems from './MenuItems.svelte'
import DecisionTreeGraphEditor from './DecisionTreeGraphEditor.svelte'
import GridAgChartsLicenseKe from './GridAgChartsLicenseKe.svelte'
import Toggle from '$lib/components/Toggle.svelte'
export let componentSettings: { item: GridItem; parent: string | undefined } | undefined =
undefined
@@ -167,10 +168,6 @@
componentSettings.item.data.componentInput = appInput
}
}
function isMac() {
return navigator.platform.toUpperCase().indexOf('MAC') >= 0
}
</script>
<svelte:window on:keydown={keydown} />
@@ -442,6 +439,30 @@
Show
</Button>
</div>
{#if componentSettings?.item?.[12]?.fullHeight !== undefined}
<Toggle
bind:checked={componentSettings.item[12].fullHeight}
size="xs"
options={{
right: 'Desktop full height',
rightTooltip:
'When enabled, the component will take the full height of the parent container.'
}}
/>
{/if}
{#if componentSettings?.item?.[3]?.fullHeight !== undefined}
<Toggle
bind:checked={componentSettings.item[3].fullHeight}
size="xs"
options={{
right: 'Mobile full height',
rightTooltip:
'When enabled, the component will take the full height of the parent container.'
}}
/>
{/if}
<AlignmentEditor bind:component={componentSettings.item.data} />
{#if viewCssOptions}
<div transition:slide|local class="w-full">
@@ -5,22 +5,21 @@
import { getColumn, throttle } from './utils/other'
import MoveResize from './MoveResize.svelte'
import type { FilledItem } from './types'
import { sortGridItemsPosition } from '../editor/appUtils'
import { ROW_GAP_X, ROW_GAP_Y, ROW_HEIGHT, sortGridItemsPosition } from '../editor/appUtils'
const dispatch = createEventDispatcher()
type T = $$Generic
export let items: FilledItem<T>[]
export let rowHeight: number
export let rowHeight: number = ROW_HEIGHT
export let cols: [number, number][]
export let gap = [10, 10]
export let gap = [ROW_GAP_X, ROW_GAP_Y]
export let throttleUpdate = 100
export let throttleResize = 100
export let selectedIds: string[] | undefined
export let allIdsInPath: string[] | undefined
export let containerWidth: number | undefined = undefined
export let scroller: HTMLElement | undefined = undefined
export let sensor = 20
@@ -136,13 +135,25 @@
const throttleMatrix = throttle(updateMatrix, throttleResize)
//let hiddenComponents = writable({})
const handleRepaint = ({ detail }) => {
if (!detail.isPointerUp) {
throttleMatrix({ detail })
} else {
updateMatrix({ detail })
}
/**
setTimeout(() => {
$hiddenComponents = {
...$hiddenComponents,
[detail.id]: updateComponentVisibility(detail, sortedItems, getComputedCols)
}
}, 0)
*/
}
let moveResizes: Record<string, MoveResize> = {}
let shadows: Record<string, { x: number; y: number; w: number; h: number } | undefined> = {}
@@ -193,7 +204,7 @@
nativeContainer={container}
>
{#if item[getComputedCols]}
<slot dataItem={item} />
<slot dataItem={item} hidden={false} />
{/if}
</MoveResize>
{/if}
@@ -330,7 +330,6 @@
shadow.w = Math.round(Math.max((newSize.width + gapX * 2) / xPerPx, 1))
shadow.h = Math.round(Math.max((newSize.height + gapY * 2) / yPerPx, 1))
}
repaint(false, false)
}
}
@@ -10,6 +10,7 @@ export interface Positon {
interface ItemLayout extends Size, Positon {
fixed: boolean
fullHeight: boolean
}
export type FilledItem<T> = { [width: number]: Required<ItemLayout>; data: T; id: string }
@@ -6,6 +6,7 @@ function makeItem(item) {
const { min = { w: 1, h: 1 }, max } = item
return {
fixed: false,
fullHeight: false,
min: {
w: Math.max(1, min.w),
h: Math.max(1, min.h)
@@ -0,0 +1,32 @@
import type { FilledItem } from '../types'
export function segmentsOverlap(x1: number, length1: number, x2: number, length2: number): boolean {
const end1: number = x1 + length1
const end2: number = x2 + length2
return Math.max(x1, x2) < Math.min(end1, end2)
}
export function updateComponentVisibility<T>(
detail: FilledItem<T>,
items: FilledItem<T>[],
getComputedCols: number
): boolean {
const c = items.find((x) => x.id === detail.id)
if (!c) return false
return items.some((item) => {
if (item?.data?.['fullHeight'] !== true || item.id === detail.id) {
return false
}
if (item[getComputedCols]) {
let { x, y, w } = item[getComputedCols]
let { x: x1, y: y1, w: w1 } = c[getComputedCols]
return y < y1 && segmentsOverlap(x, w, x1, w1)
}
return false
})
}
+10
View File
@@ -11,6 +11,7 @@ import type {
HorizontalAlignment,
VerticalAlignment
} from './types'
import { gridColumns } from './gridUtils'
export const BG_PREFIX = 'bg_'
@@ -26,7 +27,16 @@ export function migrateApp(app: App) {
x.doNotRecomputeOnInputChanged = undefined
}
})
allItems(app.grid, app.subgrids).forEach((x) => {
gridColumns.forEach((column: number) => {
if (x?.[column]?.fullHeight === undefined) {
x[column].fullHeight = false
}
})
})
}
export function allItems(
grid: GridItem[],
subgrids: Record<string, GridItem[]> | undefined
@@ -7,14 +7,16 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 21
h: 21,
fullHeight: false
},
data: {
type: 'verticalsplitpanescomponent',
@@ -40,14 +42,16 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
x: 0,
y: 1,
w: 3,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 19
h: 19,
fullHeight: false
},
data: {
type: 'schemaformcomponent',
@@ -77,14 +81,16 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
x: 0,
y: 0,
w: 1,
h: 1
h: 1,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 19,
w: 12,
h: 1
h: 1,
fullHeight: false
},
data: {
type: 'buttoncomponent',
@@ -262,14 +268,16 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 20
h: 20,
fullHeight: false
},
data: {
type: 'tabscomponent',
@@ -306,14 +314,16 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 18
h: 18,
fullHeight: false
},
data: {
type: 'displaycomponent',
@@ -339,14 +349,16 @@ export function createAppFromScript(path: string, schema: Record<string, any> |
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 18
h: 18,
fullHeight: false
},
data: {
type: 'jobidlogcomponent',
@@ -410,14 +422,16 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 21
h: 21,
fullHeight: false
},
data: {
type: 'verticalsplitpanescomponent',
@@ -443,14 +457,16 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
x: 0,
y: 1,
w: 3,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 19
h: 19,
fullHeight: false
},
data: {
type: 'schemaformcomponent',
@@ -480,14 +496,16 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
x: 0,
y: 0,
w: 1,
h: 1
h: 1,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 19,
w: 12,
h: 1
h: 1,
fullHeight: false
},
data: {
type: 'buttoncomponent',
@@ -665,14 +683,16 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 20
h: 20,
fullHeight: false
},
data: {
type: 'tabscomponent',
@@ -709,14 +729,16 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 18
h: 18,
fullHeight: false
},
data: {
type: 'jobidflowstatuscomponent',
@@ -743,14 +765,16 @@ export function createAppFromFlow(path: string, schema: Record<string, any> | un
x: 0,
y: 0,
w: 2,
h: 8
h: 8,
fullHeight: false
},
'12': {
fixed: false,
x: 0,
y: 0,
w: 12,
h: 18
h: 18,
fullHeight: false
},
data: {
type: 'jobidlogcomponent',