mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-21 00:02:23 +00:00
7968d8cfc4
* all * all * all * all
305 lines
9.2 KiB
Svelte
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
|
|
color="dark"
|
|
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
|
|
color="dark"
|
|
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"
|
|
color="dark"
|
|
on:click={() => {
|
|
appendMigrationsToCss(migrations)
|
|
migrationModalOpen = false
|
|
}}
|
|
disabled={migrations.size === 0}
|
|
endIcon={{ icon: MoveRight }}
|
|
>
|
|
Apply migration
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Portal>
|
|
{/if}
|