mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-19 16:02:14 +00:00
dev(frontend) move component options into editor menu (#4221)
* dev(frontend) move component options into editor menu * dev(frontend) move component options into editor menu * dev(frontend) add editor menu popovers --------- Co-authored-by: Guilhem Le Mouel <guilhem.le-mouel.ext@altran.com>
This commit is contained in:
@@ -350,7 +350,7 @@ pub async fn renew_license_key() -> Result<String> {
|
||||
pub async fn renew_license_key(Extension(db): Extension<DB>, authed: ApiAuthed) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
windmill_common::stats_ee::send_stats(&"manual".to_string(), &HTTP_CLIENT, &db).await?;
|
||||
let result = windmill_common::ee::renew_license_key(&HTTP_CLIENT, &db).await;
|
||||
let result = windmill_common::ee::renew_license_key(&HTTP_CLIENT, &db, None).await;
|
||||
|
||||
if result != "success" {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
@@ -371,7 +371,7 @@ pub async fn create_customer_portal_session() -> Result<String> {
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn create_customer_portal_session() -> Result<String> {
|
||||
let url = windmill_common::ee::create_customer_portal_session(&HTTP_CLIENT).await?;
|
||||
let url = windmill_common::ee::create_customer_portal_session(&HTTP_CLIENT, None).await?;
|
||||
|
||||
return Ok(url);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { Anchor, ArrowDownFromLine, Bug, Expand, Move, Network, Pen, Plug2 } from 'lucide-svelte'
|
||||
import { Anchor, ArrowDownFromLine, Bug, 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'
|
||||
@@ -22,12 +22,40 @@
|
||||
export let inlineEditorOpened: boolean = false
|
||||
export let errorHandledByComponent: boolean = false
|
||||
export let fullHeight: boolean = false
|
||||
export let componentContainerWidth: number
|
||||
//export let willNotDisplay: boolean = false
|
||||
|
||||
const DECISION_TREE_THRESHOLD = 300
|
||||
const STEPPER_THRESHOLD = 180
|
||||
const CONDITIONAL_WRAPPER_THRESHOLD = 200
|
||||
const MINIMUM_WIDTH = 20
|
||||
|
||||
let maxWidth = 10
|
||||
let isManuallySelected = false
|
||||
let componentIsDebugging = false
|
||||
|
||||
$: maxWidth = Math.max(Math.round(0.2 * componentContainerWidth), MINIMUM_WIDTH)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
const { errorByComponent, openDebugRun, connectingInput } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
|
||||
// Function to check if any condition is met
|
||||
function checkComponentOptions(): boolean {
|
||||
const componentOptions = {
|
||||
isEditable: hasInlineEditor,
|
||||
isConditionalWrapper: component.type === 'conditionalwrapper',
|
||||
hasTabs:
|
||||
component.type === 'steppercomponent' ||
|
||||
(component.type === 'tabscomponent' &&
|
||||
component.configuration.tabsKind.type === 'static' &&
|
||||
component.configuration.tabsKind.value === 'invisibleOnView'),
|
||||
isDecisionTree: component.type === 'decisiontreecomponent'
|
||||
}
|
||||
|
||||
return Object.values(componentOptions).some((value) => value)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if connecting}
|
||||
@@ -54,142 +82,153 @@
|
||||
{#if selected || hover}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
||||
<span
|
||||
on:mouseover|stopPropagation={() => {
|
||||
dispatch('mouseover')
|
||||
}}
|
||||
on:mousedown|stopPropagation|capture
|
||||
draggable="false"
|
||||
title={`Id: ${component.id}`}
|
||||
class={twMerge(
|
||||
'px-2 text-2xs font-semibold w-fit absolute shadow -top-[9px] -left-[8px] border rounded-sm z-50 cursor-move',
|
||||
selected
|
||||
? 'bg-indigo-500/90 border-indigo-600 text-white'
|
||||
: $connectingInput.opened
|
||||
? 'bg-red-500/90 border-red-600 text-white'
|
||||
: 'bg-blue-500/90 border-blue-600 text-white'
|
||||
)}
|
||||
>
|
||||
{component.id}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if selected && !connecting}
|
||||
<div class="top-[-9px] -right-[8px] flex flex-row absolute gap-1.5 z-50">
|
||||
{#if hasInlineEditor}
|
||||
<button
|
||||
title="Edit"
|
||||
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('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if inlineEditorOpened}
|
||||
<Pen aria-label="Unlock position" size={14} class="text-orange-500" />
|
||||
{:else}
|
||||
<Pen aria-label="Lock position" size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if component.type === 'conditionalwrapper'}
|
||||
<TabsDebug id={component.id} tabs={component.conditions ?? []} isConditionalDebugMode />
|
||||
{:else if component.type === 'steppercomponent' || (component.type === 'tabscomponent' && component.configuration.tabsKind.type === 'static' && component.configuration.tabsKind.value === 'invisibleOnView')}
|
||||
<TabsDebug id={component.id} tabs={component.tabs ?? []} />
|
||||
{:else if component.type === 'decisiontreecomponent'}
|
||||
<button
|
||||
title={'Open Decision Tree Editor'}
|
||||
class={classNames(
|
||||
'text-2xs py-0.5 px-1 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={() => {
|
||||
const element = document.getElementById(`decision-tree-graph-editor`)
|
||||
if (element) {
|
||||
element.click()
|
||||
}
|
||||
}}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<Network size={14} />
|
||||
</button>
|
||||
<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(
|
||||
'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('expand')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<Expand aria-label="Expand position" size={14} />
|
||||
</button>
|
||||
<button
|
||||
title="Lock Position"
|
||||
class={classNames(
|
||||
'px-1 text-2xs py-0.5 font-bold w-fit border rounded-sm cursor-pointer',
|
||||
'bg-indigo-100 text-indigo-600 border-indigo-500 hover:bg-indigo-200 hover:text-indigo-800'
|
||||
)}
|
||||
on:click={() => dispatch('lock')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if locked}
|
||||
<Anchor aria-label="Unlock position" size={14} class="text-orange-500" />
|
||||
{:else}
|
||||
<Anchor aria-label="Lock position" size={14} />
|
||||
{/if}
|
||||
</button>
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div class="-top-[18px] -left-[8px] flex flex-row flex-nowrap w-fit h-fit absolute gap-0.5">
|
||||
<div
|
||||
draggable="false"
|
||||
title="Move"
|
||||
on:mouseover|stopPropagation={() => {
|
||||
dispatch('mouseover')
|
||||
}}
|
||||
on:mousedown|stopPropagation|capture
|
||||
class={classNames(
|
||||
'px-1 text-2xs py-0.5 font-bold w-fit border cursor-move rounded-sm',
|
||||
'bg-indigo-100 text-indigo-600 border-indigo-500 hover:bg-indigo-200 hover:text-indigo-800',
|
||||
'flex items-center justify-center'
|
||||
draggable="false"
|
||||
title={`Id: ${component.id}`}
|
||||
class={twMerge(
|
||||
'py-0.5 text-2xs w-fit h-full min-h-5 border rounded z-50 cursor-move flex flex-row flex-nowrap font-semibold items-center shadow',
|
||||
selected
|
||||
? 'bg-indigo-500/90 border-indigo-500 text-white'
|
||||
: $connectingInput.opened
|
||||
? 'bg-red-500/90 border-red-600 text-white'
|
||||
: 'bg-blue-500/90 border-blue-600 text-white'
|
||||
)}
|
||||
>
|
||||
<Move size={14} />
|
||||
<div class={`px-1 text-2xs w-full min-w-4 h-full truncate`} style="max-width: {maxWidth}px;">
|
||||
{component.id}
|
||||
</div>
|
||||
{#if !connecting}
|
||||
<div class="flex flex-row px-1 py-0 gap-0.5">
|
||||
<button
|
||||
title="Fill height"
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
fullHeight
|
||||
? ' bg-indigo-800 text-indigo-200'
|
||||
: 'text-white hover:bg-indigo-700 hover:text-indigo-200'
|
||||
)}
|
||||
on:click={() => dispatch('fillHeight')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<ArrowDownFromLine aria-label="Expand position" size={11} />
|
||||
</button>
|
||||
|
||||
<button
|
||||
title="Lock Position"
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
locked
|
||||
? ' bg-indigo-800 text-indigo-200'
|
||||
: 'text-white hover:bg-indigo-700 hover:text-indigo-200'
|
||||
)}
|
||||
on:click={() => dispatch('lock')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if locked}
|
||||
<Anchor aria-label="Unlock position" size={11} />
|
||||
{:else}
|
||||
<Anchor aria-label="Lock position" size={11} />
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if selected && !connecting && checkComponentOptions()}
|
||||
<div
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-semibold w-fit min-h-5 border shadow rounded z-50 flex flex-row items-center flex-nowrap',
|
||||
isManuallySelected || componentIsDebugging
|
||||
? 'bg-red-100 text-red-600 border-red-500'
|
||||
: 'bg-indigo-100/90 border-indigo-200 text-indigo-600'
|
||||
)}
|
||||
>
|
||||
{#if hasInlineEditor}
|
||||
<button
|
||||
title="Edit"
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
inlineEditorOpened
|
||||
? 'bg-indigo-300 text-indigo-800'
|
||||
: 'text-indigo-600 hover:bg-indigo-300 hover:text-indigo-800'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
{#if inlineEditorOpened}
|
||||
<Pen aria-label="Unlock position" size={11} />
|
||||
{:else}
|
||||
<Pen aria-label="Lock position" size={11} />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
{#if component.type === 'conditionalwrapper'}
|
||||
<TabsDebug
|
||||
id={component.id}
|
||||
tabs={component.conditions ?? []}
|
||||
isConditionalDebugMode
|
||||
isSmall={componentContainerWidth < CONDITIONAL_WRAPPER_THRESHOLD}
|
||||
bind:isManuallySelected
|
||||
/>
|
||||
{:else if component.type === 'steppercomponent' || (component.type === 'tabscomponent' && component.configuration.tabsKind.type === 'static' && component.configuration.tabsKind.value === 'invisibleOnView')}
|
||||
<TabsDebug
|
||||
id={component.id}
|
||||
tabs={component.tabs ?? []}
|
||||
isSmall={componentContainerWidth < STEPPER_THRESHOLD}
|
||||
bind:isManuallySelected
|
||||
/>
|
||||
{:else if component.type === 'decisiontreecomponent'}
|
||||
<button
|
||||
title={'Open Decision Tree Editor'}
|
||||
class={twMerge(
|
||||
'px-1 py-0.5 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
componentIsDebugging
|
||||
? 'text-red-600 hover:bg-red-300 hover:text-red-800'
|
||||
: 'text-indigo-600 hover:bg-indigo-300 hover:text-indigo-800'
|
||||
)}
|
||||
on:click={() => {
|
||||
const element = document.getElementById(`decision-tree-graph-editor`)
|
||||
if (element) {
|
||||
element.click()
|
||||
}
|
||||
}}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<Network size={11} />
|
||||
</button>
|
||||
<DecisionTreeDebug
|
||||
id={component.id}
|
||||
nodes={component.nodes ?? []}
|
||||
isSmall={componentContainerWidth < DECISION_TREE_THRESHOLD}
|
||||
bind:componentIsDebugging
|
||||
/>
|
||||
{/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} -->
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -6,10 +6,14 @@
|
||||
import type { AppViewerContext } from '../types'
|
||||
import type { DecisionTreeNode } from './component'
|
||||
import { isDebugging } from './settingsPanel/decisionTree/utils'
|
||||
import { X } from 'lucide-svelte'
|
||||
import { X, Bug } from 'lucide-svelte'
|
||||
|
||||
export let nodes: DecisionTreeNode[] = []
|
||||
export let id: string
|
||||
export let isSmall = false
|
||||
export let componentIsDebugging = false
|
||||
|
||||
$: componentIsDebugging = isDebugging($debuggingComponents, id)
|
||||
|
||||
const { componentControl, debuggingComponents, worldStore } =
|
||||
getContext<AppViewerContext>('AppViewerContext')
|
||||
@@ -71,20 +75,20 @@
|
||||
<button
|
||||
title={'Debug tabs'}
|
||||
class={classNames(
|
||||
'text-2xs py-0.5 font-bold w-fit border cursor-pointer rounded-sm',
|
||||
isDebugging($debuggingComponents, id)
|
||||
? 'bg-red-100 text-red-600 border-red-500 hover:bg-red-200 hover:text-red-800'
|
||||
: 'bg-indigo-100 text-indigo-600 border-indigo-500 hover:bg-indigo-200 hover:text-indigo-800'
|
||||
'px-1 text-2xs font-bold rounded cursor-pointer w-fit h-full',
|
||||
componentIsDebugging
|
||||
? ' hover:bg-red-300 hover:text-red-800'
|
||||
: ' hover:bg-indigo-300 hover:text-indigo-800'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
>
|
||||
<ButtonDropdown hasPadding={false}>
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<div class="px-1">
|
||||
{#if isDebugging($debuggingComponents, id)}
|
||||
<div class="px-1 w-fit">
|
||||
{#if componentIsDebugging}
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{`Debugging node ${nodes[$debuggingComponents[id] ?? 0]?.id}`}
|
||||
{`${isSmall ? '' : 'Debugging node'} ${nodes[$debuggingComponents[id] ?? 0]?.id}`}
|
||||
<button
|
||||
on:click={() => {
|
||||
$componentControl?.[id]?.setTab?.(0)
|
||||
@@ -94,12 +98,14 @@
|
||||
)
|
||||
}}
|
||||
>
|
||||
<X size={14} />
|
||||
<X size={11} />
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
{`Debug nodes (current node: ${currentNodeId})`}
|
||||
{/if}
|
||||
{:else if isSmall}
|
||||
<div class="flex h-full w-fit items-center"><Bug size={11} /></div>
|
||||
{:else}<div class="whitespace-nowrap h-full"
|
||||
>{`Debug nodes (current node: ${currentNodeId})`}</div
|
||||
>{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="items">
|
||||
|
||||
@@ -45,6 +45,23 @@
|
||||
.flatMap((id) => dfs($app.grid, id, $app.subgrids ?? {}))
|
||||
.filter((x) => x != undefined) as string[]
|
||||
}
|
||||
|
||||
function handleLock(id: string) {
|
||||
const gridItem = findGridItem($app, id)
|
||||
if (gridItem) {
|
||||
toggleFixed(gridItem)
|
||||
}
|
||||
$app = $app
|
||||
}
|
||||
|
||||
function handleFillHeight(id: string) {
|
||||
const gridItem = findGridItem($app, id)
|
||||
const b = $breakpoint === 'sm' ? 3 : 12
|
||||
if (gridItem?.[b]) {
|
||||
gridItem[b].fullHeight = !gridItem[b].fullHeight
|
||||
}
|
||||
$app = $app
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="w-full z-[1000] overflow-visible h-full">
|
||||
@@ -137,7 +154,23 @@
|
||||
Boolean($selectedComponent?.includes(dataItem.id)) ? 'active-grid-item' : ''
|
||||
)}
|
||||
>
|
||||
<GridEditorMenu id={dataItem.id}>
|
||||
<GridEditorMenu
|
||||
id={dataItem.id}
|
||||
on:expand={() => {
|
||||
push(history, $app)
|
||||
$selectedComponent = [dataItem.id]
|
||||
expandGriditem($app.grid, dataItem.id, $breakpoint)
|
||||
$app = $app
|
||||
}}
|
||||
on:lock={() => {
|
||||
handleLock(dataItem.id)
|
||||
}}
|
||||
on:fillHeight={() => {
|
||||
handleFillHeight(dataItem.id)
|
||||
}}
|
||||
locked={isFixed(dataItem)}
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
>
|
||||
<Component
|
||||
{hidden}
|
||||
render={true}
|
||||
@@ -146,25 +179,10 @@
|
||||
locked={isFixed(dataItem)}
|
||||
fullHeight={dataItem?.[$breakpoint === 'sm' ? 3 : 12]?.fullHeight}
|
||||
on:lock={() => {
|
||||
const gridItem = findGridItem($app, dataItem.id)
|
||||
if (gridItem) {
|
||||
toggleFixed(gridItem)
|
||||
}
|
||||
$app = $app
|
||||
}}
|
||||
on:expand={() => {
|
||||
push(history, $app)
|
||||
$selectedComponent = [dataItem.id]
|
||||
expandGriditem($app.grid, dataItem.id, $breakpoint)
|
||||
$app = $app
|
||||
handleLock(dataItem.id)
|
||||
}}
|
||||
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
|
||||
handleFillHeight(dataItem.id)
|
||||
}}
|
||||
/>
|
||||
</GridEditorMenu>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script context="module" lang="ts">
|
||||
import { writable } from 'svelte/store'
|
||||
import Popover from '../../Popover.svelte'
|
||||
|
||||
interface ContextMenuRegistry {
|
||||
id: string
|
||||
@@ -13,9 +14,18 @@
|
||||
<script lang="ts">
|
||||
import { getModifierKey } from '$lib/utils'
|
||||
|
||||
import { Copy, Paintbrush2, Scissors, Trash } from 'lucide-svelte'
|
||||
import {
|
||||
Anchor,
|
||||
ArrowDownFromLine,
|
||||
Copy,
|
||||
Expand,
|
||||
ExternalLink,
|
||||
Paintbrush2,
|
||||
Scissors,
|
||||
Trash
|
||||
} from 'lucide-svelte'
|
||||
import ComponentCallbacks from './component/ComponentCallbacks.svelte'
|
||||
import { getContext } from 'svelte'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { AppEditorContext, AppViewerContext } from '../types'
|
||||
import DeleteComponent from './settingsPanel/DeleteComponent.svelte'
|
||||
import { secondaryMenuLeft } from './settingsPanel/secondaryMenu'
|
||||
@@ -27,6 +37,9 @@
|
||||
let menuX = 0
|
||||
let menuY = 0
|
||||
|
||||
export let locked: boolean = false
|
||||
export let fullHeight: boolean = false
|
||||
|
||||
function handleRightClick(event: MouseEvent) {
|
||||
event.preventDefault()
|
||||
contextMenuVisible = true
|
||||
@@ -61,11 +74,13 @@
|
||||
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { movingcomponents, stylePanel } = getContext<AppEditorContext>('AppEditorContext')
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let deleteComponent: DeleteComponent | undefined = undefined
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: 'Cut',
|
||||
label: () => 'Cut',
|
||||
onClick: () => {
|
||||
componentCallbacks?.handleCut(new KeyboardEvent('keydown'))
|
||||
},
|
||||
@@ -74,7 +89,7 @@
|
||||
disabled: $movingcomponents?.includes($selectedComponent?.[0] ?? '')
|
||||
},
|
||||
{
|
||||
label: 'Copy',
|
||||
label: () => 'Copy',
|
||||
onClick: () => {
|
||||
componentCallbacks?.handleCopy(new KeyboardEvent('keydown'))
|
||||
},
|
||||
@@ -82,16 +97,53 @@
|
||||
shortcut: `${getModifierKey()}C`
|
||||
},
|
||||
{
|
||||
label: 'Show style panel',
|
||||
label: () => (fullHeight ? 'Undo fill height' : 'Fill height'),
|
||||
onClick: () => {
|
||||
dispatch('fillHeight')
|
||||
},
|
||||
icon: ArrowDownFromLine,
|
||||
tooltip: {
|
||||
text: 'When set to full height, a component will extend its height to fill the entire parent container (or canvas).',
|
||||
link: 'https://www.windmill.dev/docs/apps/app_configuration_settings/app_styling#full-height'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: () => 'Expand',
|
||||
onClick: () => {
|
||||
dispatch('expand')
|
||||
},
|
||||
icon: Expand,
|
||||
tooltip: {
|
||||
text: "Clicking the expand button maximizes the component's width and height, respecting other components' position.",
|
||||
link: 'https://www.windmill.dev/docs/apps/canvas#expand-a-component'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: () => (locked ? 'Unlock' : 'Lock'),
|
||||
onClick: () => {
|
||||
dispatch('lock')
|
||||
},
|
||||
icon: Anchor,
|
||||
tooltip: {
|
||||
text: 'Lock the component to prevent it from being repositioned by other components.',
|
||||
link: 'https://www.windmill.dev/docs/apps/canvas#lock-the-position-of-a-component'
|
||||
}
|
||||
},
|
||||
{
|
||||
label: () => 'Show style panel',
|
||||
onClick: () => {
|
||||
secondaryMenuLeft?.toggle(stylePanel(), { type: 'style' })
|
||||
},
|
||||
icon: Paintbrush2,
|
||||
disabled: $secondaryMenuLeft.isOpen
|
||||
disabled: $secondaryMenuLeft.isOpen,
|
||||
tooltip: {
|
||||
text: 'Use style panel to define custom CSS and Tailwind classes for the components.',
|
||||
link: 'https://www.windmill.dev/docs/apps/app_configuration_settings/app_styling'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
label: 'Delete',
|
||||
label: () => 'Delete',
|
||||
onClick: () => {
|
||||
deleteComponent?.removeGridElement()
|
||||
},
|
||||
@@ -122,30 +174,51 @@
|
||||
{#each menuItems as item}
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<button
|
||||
class={twMerge(
|
||||
'flex items-center p-2 hover:bg-surface-hover cursor-pointer transition-all rounded-md w-full',
|
||||
item.color === 'red' && 'text-red-500',
|
||||
item.color === 'green' && 'text-green-500',
|
||||
item.color === 'blue' && 'text-blue-500',
|
||||
item.disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
on:click={() => {
|
||||
item.onClick()
|
||||
closeContextMenu()
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<svelte:component this={item.icon} class="w-4 h-4" />
|
||||
|
||||
<span class="ml-2 text-xs">{item.label}</span>
|
||||
{#if item.shortcut}
|
||||
<span class="ml-auto text-xs text-gray-400">
|
||||
{item.shortcut}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
<Popover
|
||||
notClickable
|
||||
placement="right"
|
||||
popupClass="z-[7000]"
|
||||
disablePopup={!item.tooltip}
|
||||
appearTimeout={800}
|
||||
>
|
||||
<svelte:fragment slot="text"
|
||||
>{item.tooltip?.text}
|
||||
{#if item.tooltip?.link}
|
||||
<a href={item.tooltip.link} target="_blank" class="text-blue-300 text-xs">
|
||||
<div class="flex flex-row gap-2 mt-4">
|
||||
See documentation
|
||||
<ExternalLink size="16" />
|
||||
</div>
|
||||
</a>
|
||||
{/if}</svelte:fragment
|
||||
>
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'flex items-center p-2 hover:bg-surface-hover cursor-pointer transition-all rounded-md w-full',
|
||||
item.color === 'red' && 'text-red-500',
|
||||
item.color === 'green' && 'text-green-500',
|
||||
item.color === 'blue' && 'text-blue-500',
|
||||
item.disabled && 'opacity-50 cursor-not-allowed'
|
||||
)}
|
||||
on:click={() => {
|
||||
item.onClick()
|
||||
closeContextMenu()
|
||||
}}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
<!-- svelte-ignore missing-declaration -->
|
||||
<svelte:component this={item.icon} class="w-4 h-4" />
|
||||
|
||||
<span class="ml-2 text-xs">{item.label()}</span>
|
||||
{#if item.shortcut}
|
||||
<span class="ml-auto text-xs text-gray-400">
|
||||
{item.shortcut}
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</Popover>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,26 +4,28 @@
|
||||
import { MenuItem } from '@rgossiaux/svelte-headlessui'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { AppViewerContext } from '../types'
|
||||
import { Bug } from 'lucide-svelte'
|
||||
|
||||
export let tabs: any[] = []
|
||||
export let id: string
|
||||
|
||||
export let isConditionalDebugMode: boolean = false
|
||||
export let isSmall = false
|
||||
|
||||
const { componentControl } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let isManuallySelected: boolean = false
|
||||
export let isManuallySelected: boolean = false
|
||||
let selected: number | null = null
|
||||
</script>
|
||||
|
||||
<button
|
||||
title={isConditionalDebugMode ? 'Debug conditions' : 'Debug tabs'}
|
||||
class={classNames(
|
||||
'text-2xs py-0.5 font-bold w-fit border cursor-pointer rounded-sm',
|
||||
'text-2xs font-bold w-fit h-full cursor-pointer rounded',
|
||||
isManuallySelected
|
||||
? 'bg-red-100 text-red-600 border-red-500 hover:bg-red-200 hover:text-red-800'
|
||||
: 'bg-indigo-100 text-indigo-600 border-indigo-500 hover:bg-indigo-200 hover:text-indigo-800'
|
||||
? 'hover:bg-red-200 hover:text-red-800'
|
||||
: ' hover:text-indigo-800 hover:bg-indigo-300'
|
||||
)}
|
||||
on:click={() => dispatch('triggerInlineEditor')}
|
||||
on:pointerdown|stopPropagation
|
||||
@@ -32,14 +34,21 @@
|
||||
<svelte:fragment slot="buttonReplacement">
|
||||
<div class="px-1">
|
||||
{#if isManuallySelected}
|
||||
<div>
|
||||
<div class="whitespace-nowrap">
|
||||
{#if selected === tabs.length - 1}
|
||||
{isConditionalDebugMode ? `Debug default condition` : `Debug tab ${selected + 1}`}
|
||||
{#if isSmall}
|
||||
{isConditionalDebugMode ? `df` : `t ${selected + 1}`}
|
||||
{:else}
|
||||
{isConditionalDebugMode ? `Debug default condition` : `Debug tab ${selected + 1}`}
|
||||
{/if}
|
||||
{:else if isSmall}
|
||||
{`${isConditionalDebugMode ? 'c' : 't'} ${(selected ?? 0) + 1}`}
|
||||
{:else}
|
||||
{`Debugging ${isConditionalDebugMode ? 'condition' : 'tab'} ${(selected ?? 0) + 1}`}
|
||||
{/if}
|
||||
{`Debugging ${isConditionalDebugMode ? 'condition' : 'tab'} ${
|
||||
(selected ?? 0) + 1
|
||||
}`}{/if}
|
||||
</div>
|
||||
{:else}
|
||||
{:else if isSmall}<Bug size={11} />{:else}
|
||||
{isConditionalDebugMode ? `Debug conditions` : `Debug tabs`}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -99,6 +99,7 @@
|
||||
let initializing: boolean | undefined = undefined
|
||||
let errorHandledByComponent: boolean = false
|
||||
let componentContainerHeight: number = 0
|
||||
let componentContainerWidth: number = 0
|
||||
|
||||
let inlineEditorOpened: boolean = false
|
||||
|
||||
@@ -158,6 +159,7 @@
|
||||
inlineEditorOpened = !inlineEditorOpened
|
||||
}}
|
||||
{errorHandledByComponent}
|
||||
{componentContainerWidth}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -191,6 +193,7 @@
|
||||
)}
|
||||
style={$app.css?.['app']?.['component']?.style}
|
||||
bind:clientHeight={componentContainerHeight}
|
||||
bind:clientWidth={componentContainerWidth}
|
||||
>
|
||||
{#if component.type === 'displaycomponent'}
|
||||
<AppDisplayComponent
|
||||
|
||||
Reference in New Issue
Block a user