Files
windmill/frontend/src/lib/components/apps/editor/settingsPanel/CSSMigrationModal.svelte
Ruben Fiszel b11d6ed794 fix(frontend): revert CloseButton refactor that broke tag removal in MultiSelect (#7909)
The recent refactor of CloseButton (from on:close component events to
onClick prop) broke tag removal in MultiSelect/TagsToListenTo. The
refactor changed on:pointerdown (component event) to onPointerdown
(native DOM event), which stopped native pointerdown propagation and
broke the drag tracking in DraggableTags, causing the dropdown to open
on every close button click.

Reverts CloseButton and all callers back to using createEventDispatcher
and on:close.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 14:27:50 +00:00

305 lines
9.2 KiB
Svelte

<script lang="ts">
import { getContext } from 'svelte'
import type { AppViewerContext, ComponentCssProperty } from '../../types'
import { ccomponents, type AppComponent } from '../component'
import Badge from '$lib/components/common/badge/Badge.svelte'
import Button from '$lib/components/common/button/Button.svelte'
import { twMerge } from 'tailwind-merge'
import { fade } from 'svelte/transition'
import css from 'svelte-highlight/languages/css'
import { Highlight } from 'svelte-highlight'
import { MoveRight } from 'lucide-svelte'
import { customisationByComponent } from '../componentsPanel/cssUtils'
import { sendUserToast } from '$lib/toast'
import CloseButton from '$lib/components/common/CloseButton.svelte'
import HighlightTheme from '$lib/components/HighlightTheme.svelte'
import Portal from '$lib/components/Portal.svelte'
interface Props {
component: AppComponent | undefined
}
let { component = $bindable() }: Props = $props()
const { app } = getContext<AppViewerContext>('AppViewerContext')
function fadeFast(node: HTMLElement) {
return fade(node, { duration: 100 })
}
let migrationModalOpen: boolean = $state(false)
export function open() {
migrationModalOpen = true
}
function generateCodeFromMigrations(migrations: Map<string, string[]>) {
let code = ''
for (const [key, value] of migrations) {
code += `${key} {\n\t${value.join(';\n\t')}\n}\n\n`
}
return code
}
function getSelector(key: string) {
return customisationByComponent
.find((c) => c.components.includes(component?.type ?? ''))
?.selectors.find((s) => {
return s.customCssKey === key
})?.selector
}
function setOrUpdateMigration(key: string, value: string) {
const selector = getSelector(key)
if (!selector) {
return
}
if (migrations.has(selector)) {
const arr = migrations.get(selector)
if (arr) {
arr.push(value)
migrations.set(selector, arr)
}
} else {
migrations.set(selector, [value])
}
migrations = migrations
}
function appendMigrationsToCss(migrations: Map<string, string[]>) {
const theme = $app.theme
if (theme?.type === 'path') {
sendUserToast(
'Cannot migrate to CSS editor when using a theme by path. Fork a global theme to edit CSS.',
true
)
return
} else if (theme?.type === 'inlined') {
let cssString = theme.css
if (!cssString) {
cssString = ''
}
for (const [key, value] of migrations) {
if (cssString.includes(`${key} {` || `${key}{`)) {
// append value to existing value
const regex = new RegExp(`\\${key}\\s*{\\s*([\\s\\S]*?)\\s*}`, 'g')
const match = regex.exec(cssString)
if (match) {
const existingValue = match[1]
cssString = cssString.replace(
regex,
`${key} {\n\t${existingValue}\n\t${value.join('\n\t')}\n}`
)
}
} else {
const firstBreakline = cssString === '' ? '' : '\n\n'
// append key and value
cssString += `${firstBreakline}${key} {\n\t${value.join('\n\t')}\n}\n\n`
}
}
theme.css = cssString
$app.theme = theme
}
}
function hasStyles(customCss: Record<string, ComponentCssProperty> | undefined) {
if (!customCss) {
return false
}
return Object.keys(customCss ?? {})
.map((key) => customCss[key])
.some((c) => c.style !== '')
}
let type: string | undefined = component?.type
let migrations = $derived(new Map<string, string[]>())
let generatedCode = $derived(generateCodeFromMigrations(migrations))
</script>
<HighlightTheme />
{#if migrationModalOpen}
<Portal name="css-global-migration-modal" class="z-[5000] absolute">
<div transition:fadeFast|local class={' top-0 bottom-0 left-0 right-0 z-[5000]'} role="dialog">
<div
class={twMerge(
'fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity',
migrationModalOpen
? 'ease-out duration-300 opacity-100'
: 'ease-in duration-200 opacity-0'
)}
></div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-center justify-center p-4">
<div
class={twMerge(
'relative transform overflow-hidden rounded-lg bg-surface px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-5xl sm:p-6 ',
migrationModalOpen
? 'ease-out duration-300 opacity-100 translate-y-0 sm:scale-100'
: 'ease-in duration-200 opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95'
)}
>
<div class="leading-6 font-semibold text-sm w-full flex justify-between">
<div>Migrate to CSS editor</div><CloseButton
on:close={() => (migrationModalOpen = false)}
/>
</div>
<div class="">
<div class="">
{#if hasStyles(component?.customCss)}
<div class="leading-6 text-xs font-semibold">
ID <Badge color="indigo" size="xs">
{component?.id}
</Badge>
</div>
{/if}
{#if component?.type && $app.css}
{#each Object.keys(component.customCss ?? {}) as cssKey}
{#if component.customCss?.[cssKey].style != undefined && component.customCss[cssKey].style !== ''}
<div class="grid grid-cols-2 gap-2">
<div>
<div class="flex flex-row justify-between items-center py-0.5">
<div class="leading-6 text-xs font-semibold">
<Badge>
{cssKey}
</Badge>
</div>
<Button
variant="accent"
size="xs"
on:click={() => {
if (component?.customCss?.[cssKey]?.style != undefined) {
setOrUpdateMigration(
cssKey,
component.customCss[cssKey].style ?? ''
)
component.customCss[cssKey].style = ''
}
}}
endIcon={{ icon: MoveRight }}
>
Migrate
</Button>
</div>
<div class="border p-2 rounded-md">
<Highlight code={component.customCss[cssKey].style} language={css} />
</div>
</div>
<div class="">
<div class="leading-6 text-xs font-semibold my-1">Preview</div>
<div class="border rounded-md p-2">
<Highlight
code={`${getSelector(cssKey)} {\n\t${
component.customCss[cssKey].style
}\n}`}
language={css}
/>
</div>
</div>
</div>
{/if}
{/each}
{/if}
{#if hasStyles(component?.type ? $app.css?.[component?.type] : undefined)}
<div class="leading-6 text-xs font-semibold">
Global: {component?.type ? ccomponents[component.type]?.name : ''}
</div>
{/if}
{#if component?.type && $app.css}
{#each Object.keys($app.css[component?.type] ?? {}) as cssKey}
{#if type && $app.css?.[type]?.[cssKey].style != undefined && $app.css[type]?.[cssKey].style !== ''}
<div class="grid grid-cols-2 gap-2">
<div>
<div class="flex flex-row justify-between items-center py-0.5">
<div class="leading-6 text-xs font-semibold">
<Badge>
{cssKey}
</Badge>
</div>
<Button
variant="accent"
size="xs"
on:click={() => {
if (type && $app.css?.[type]) {
setOrUpdateMigration(cssKey, $app.css[type][cssKey].style)
$app.css[type][cssKey].style = ''
}
}}
endIcon={{ icon: MoveRight }}
>
Migrate
</Button>
</div>
<div class="border p-2 rounded-md">
<Highlight code={$app.css[type][cssKey].style} language={css} />
</div>
</div>
<div class="">
<div class="leading-6 text-xs font-semibold my-1">Preview</div>
<div class="border rounded-md p-2">
<Highlight
code={`${getSelector(cssKey)} {\n\t${$app.css[type][cssKey].style}\n}`}
language={css}
/>
</div>
</div>
</div>
{/if}
{/each}
{/if}
</div>
</div>
<div>
<div class="leading-6 text-xs font-semibold my-1">Current migrations</div>
{#if migrations.size > 0}
<div class="border rounded-md p-2">
<Highlight code={generatedCode} language={css} />
</div>
{:else}
<div class="text-gray-500 text-xs">No migrations</div>
{/if}
</div>
<div class="mt-2 flex flex-row justify-end items-center gap-2">
<div class="text-xs">
If the class is already present in the CSS editor, the migration will append the new
values to the existing ones.
</div>
<Button
size="xs"
variant="accent"
on:click={() => {
appendMigrationsToCss(migrations)
migrationModalOpen = false
}}
disabled={migrations.size === 0}
endIcon={{ icon: MoveRight }}
>
Apply migration
</Button>
</div>
</div>
</div>
</div>
</div>
</Portal>
{/if}