Files
windmill/frontend/src/lib/components/copilot/chat/LinkRenderer.svelte
T
6186a0645d feat: edit variables, resources and triggers in their own session tab (#11206)
* feat: edit variables, resources and triggers in their own session tab

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: take a trigger's flow kind from its config and name the written trigger

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: one operating-workspace context for editors acting on a session's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: reload page item tabs only in sessions acting on the tool's workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: resolve open in workspace for every preview tab kind in one place

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: guard every component under a session editor against the navigation workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: show a page item tab whose item is gone instead of mounting its editor

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: follow a saved page item in the tab's own workspace and restore in-frame rows

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* feat: close a page item tab from its editor header

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge trigger permissions by the user acting in the session workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: judge every session-editor permission by the user acting in that workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: keep session permissions reactive to the acting role and restore tabs as last seen

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: default Path's owner controls to the user acting in its workspace

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ruben Fiszel <ruben@windmill.dev>
2026-09-19 23:29:14 +02:00

133 lines
4.5 KiB
Svelte

<script lang="ts">
import type { Snippet } from 'svelte'
import { ExternalLink, PanelRight } from 'lucide-svelte'
import { Button } from '$lib/components/common'
import RowIcon from '$lib/components/common/table/RowIcon.svelte'
import { newTabModifier } from '$lib/attachments/newTabModifier.svelte'
import {
hasToolDisplayActionHandler,
runToolDisplayAction
} from './createdResourceActions.svelte'
import {
workspaceItemAction,
type WindmillItemKind,
type WorkspaceItemTargetKind
} from './workspaceItems.svelte'
import { safeHref } from './safeHref'
type Props = {
href?: string
children?: Snippet
'data-wm-kind'?: WindmillItemKind
'data-wm-path'?: string
'data-wm-target-kind'?: WorkspaceItemTargetKind
'data-wm-raw-app'?: string
title?: string
}
let {
href,
children,
'data-wm-kind': wmKind,
'data-wm-path': wmPath,
'data-wm-target-kind': wmTargetKind,
'data-wm-raw-app': wmRawApp,
title
}: Props = $props()
// The drawers ride with the docked chat and the session tabs with the sessions page, so a
// surface can render this pill with nothing able to open one.
const available = $derived.by(() => {
const action = workspaceItemAction(wmKind, wmPath, wmTargetKind, wmRawApp === 'true')
return action && hasToolDisplayActionHandler(action.type) ? action : undefined
})
// Only the preview panel takes the plain click. A drawer keeps its own button beside an
// outbound link: the docked chat mounts drawer handlers on nearly every page, so claiming
// that click would redirect these pills far outside the sessions page.
const previewAction = $derived(available?.type === 'open_item_preview' ? available : undefined)
const drawerAction = $derived(available?.type === 'open_created_resource' ? available : undefined)
const allowedHref = $derived(safeHref(href, window.location.href))
const modifier = newTabModifier()
const hint = $derived(
previewAction ? `Open ${wmPath} in the preview panel` : `Open ${wmPath} in a new tab`
)
async function openDrawer(event?: Event) {
event?.preventDefault()
event?.stopPropagation()
if (drawerAction) {
await runToolDisplayAction(drawerAction)
}
}
async function onclick(event: MouseEvent) {
// Modifier clicks are the only remaining route to the tab once the plain click is
// spoken for, so leave them to the browser.
if (!previewAction || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
event.preventDefault()
await runToolDisplayAction(previewAction)
}
</script>
{#if allowedHref}
{#if wmKind}
<!-- Only a preview pill can change icon, so only it is worth tracking the modifier for. -->
<span
class="group inline-flex items-baseline"
{@attach previewAction ? modifier.attach : undefined}
>
<a
href={allowedHref}
target={previewAction ? undefined : '_blank'}
rel={previewAction ? undefined : 'noopener noreferrer'}
title={title || hint}
{onclick}
class="inline-flex items-baseline gap-1 px-1 rounded hover:bg-surface-hover text-primary no-underline font-mono text-[0.9em] align-baseline"
>
<!-- Kind icon and action icon share one fixed 12px box, so the pill is the same
width at rest and on hover and the surrounding sentence never reflows. -->
<span class="relative inline-flex self-center shrink-0 w-3 h-3">
<span class="absolute inset-0 transition-opacity group-hover:opacity-0">
<RowIcon kind={wmKind} size={12} />
</span>
<span
class="absolute inset-0 flex items-center justify-center text-tertiary opacity-0 transition-opacity group-hover:opacity-100"
>
<!-- Narrower than the modifier list `onclick` bails on: only the modifier that
really opens a tab may show the icon for one. -->
{#if previewAction && !modifier.held}
<PanelRight size={12} />
{:else}
<ExternalLink size={11} />
{/if}
</span>
</span>
{@render children?.()}
</a>
{#if drawerAction}
<Button
type="button"
size="xs3"
variant="subtle"
iconOnly
startIcon={{ icon: PanelRight }}
title="Open in editor"
aria-label="Open {wmPath} in editor"
wrapperClasses="ml-0.5 inline-flex self-center shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
btnClasses="!w-auto !rounded !p-0.5 !text-tertiary"
onClick={openDrawer}
/>
{/if}
</span>
{:else}
<a href={allowedHref} target="_blank" rel="noopener noreferrer" {title}>
{@render children?.()}
</a>
{/if}
{:else}
<!-- An empty or unsafe href still has text; drop only the link. -->
{@render children?.()}
{/if}