mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-25 00:01:55 +00:00
fix(frontend): update raw app editor to brand guidelines (#7545)
* nit * Update sidebar to brand guidelines * use dropdown for file delete and rename * update runnable script styling * fix icon sizes * Polish app creation form * Add missing languages icons * Allow to delete runnable from the left pannel * update top bar to brand guidelines * improve svelte template * fix runnables binding * nit * Deselect frontend file when selecting background runnable * fix renaming wip * fix add ing files, update only after adding * Change filename when already existing * update deployment page to brand guidelines * fix folder open error * expand folder if adding file inside * nit * fix bad link * use alert for ai not configured message
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import EEOnly from '$lib/components/EEOnly.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
|
||||
let {
|
||||
policy,
|
||||
@@ -117,44 +118,46 @@
|
||||
</Alert>
|
||||
<div class="py-2"></div>
|
||||
{/if}
|
||||
<span class="text-secondary text-sm font-bold">Summary</span>
|
||||
<div class="w-full pt-2">
|
||||
<label for="summary" class="text-emphasis text-xs font-semibold">Summary</label>
|
||||
<div class="w-full pt-1">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
autofocus
|
||||
type="text"
|
||||
placeholder="App summary"
|
||||
class="text-sm w-full"
|
||||
bind:value={summary}
|
||||
onkeydown={(e) => {
|
||||
e.stopPropagation()
|
||||
}}
|
||||
onkeyup={() => {
|
||||
if (appPath == '' && summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
<TextInput
|
||||
inputProps={{
|
||||
id: 'summary',
|
||||
autofocus: true,
|
||||
placeholder: 'App summary',
|
||||
onkeydown: (e) => {
|
||||
e.stopPropagation()
|
||||
},
|
||||
onkeyup: () => {
|
||||
if (appPath == '' && summary?.length > 0 && !dirtyPath) {
|
||||
path?.setName(
|
||||
summary
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9_]/g, '_')
|
||||
.replace(/-+/g, '_')
|
||||
.replace(/^-|-$/g, '')
|
||||
)
|
||||
}
|
||||
}
|
||||
}}
|
||||
bind:value={summary}
|
||||
/>
|
||||
</div>
|
||||
<div class="py-4"></div>
|
||||
<span class="text-secondary text-sm font-bold">Deployment message</span>
|
||||
<div class="w-full pt-2">
|
||||
<div class="py-6"></div>
|
||||
<label for="deploymentMsg" class="text-emphasis text-xs font-semibold">Deployment message</label>
|
||||
<div class="w-full pt-1">
|
||||
<!-- svelte-ignore a11y_autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Optional deployment message"
|
||||
class="text-sm w-full"
|
||||
<TextInput
|
||||
inputProps={{
|
||||
id: 'deploymentMsg',
|
||||
placeholder: 'Optional deployment message'
|
||||
}}
|
||||
bind:value={deploymentMsg}
|
||||
/>
|
||||
</div>
|
||||
<div class="py-4"></div>
|
||||
<span class="text-secondary text-sm font-bold">Path</span>
|
||||
<div class="py-6"></div>
|
||||
<label for="path" class="text-emphasis text-xs font-semibold">Path</label>
|
||||
<Path
|
||||
bind:this={path}
|
||||
bind:dirty={dirtyPath}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
width?: number;
|
||||
height?: number;
|
||||
width?: number
|
||||
height?: number
|
||||
size?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
let { width = 16, height = 16, ...rest }: Props = $props();
|
||||
let { width = 16, height = 16, size = undefined, ...rest }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
{width}
|
||||
{height}
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
{...rest}
|
||||
viewBox="0 0 17 16"
|
||||
fill="none"
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
>
|
||||
{/if}
|
||||
<div class="flex">
|
||||
<div class="ml-4 text-left flex-1">
|
||||
<div class="text-left flex-1">
|
||||
<div class="flex flex-row items-center justify-between">
|
||||
<h3 class="text-emphasis text-lg font-semibold">{title}</h3>
|
||||
{@render settings?.()}
|
||||
|
||||
@@ -154,16 +154,18 @@
|
||||
|
||||
<div class="flex justify-end gap-3 pt-2">
|
||||
<Button
|
||||
color="light"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
variant="default"
|
||||
unifiedSize="md"
|
||||
onClick={() => {
|
||||
featurePreviewModalOpen = false
|
||||
appTypeModalOpen = true
|
||||
}}
|
||||
>
|
||||
Go back
|
||||
</Button>
|
||||
<Button color="blue" size="sm" on:click={confirmFullCode}>I understand, continue</Button>
|
||||
<Button unifiedSize="md" variant="accent" on:click={confirmFullCode}
|
||||
>I understand, continue</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M6 28L4 3H28L26 28L16 31L6 28Z" fill="#1172B8" />
|
||||
<path d="M26 5H16V29.5L24 27L26 5Z" fill="#33AADD" />
|
||||
<path
|
||||
d="M19.5 17.5H9.5L9 14L17 11.5H9L8.5 8.5H24L23.5 12L17 14.5H23L22 24L16 26L10 24L9.5 19H12.5L13 21.5L16 22.5L19 21.5L19.5 17.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M6 28L4 3H28L26 28L16 31L6 28Z" fill="#E44D26" />
|
||||
<path d="M26 5H16V29.5L24 27L26 5Z" fill="#F16529" />
|
||||
<path
|
||||
d="M9.5 17.5L8.5 8H24L23.5 11H11.5L12 14.5H23L22 24L16 26L10 24L9.5 19H12.5L13 21.5L16 22.5L19 21.5L19.5 17.5H9.5Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,13 +1,23 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string;
|
||||
width?: string;
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px' }: Props = $props();
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg {width} {height} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
>
|
||||
<path d="M0 0H32V32H0V0Z" fill="#F7DF1E" />
|
||||
<path
|
||||
d="M8.41397 26.7415L10.8628 25.2595C11.3353 26.0972 11.765 26.8059 12.7959 26.8059C13.784 26.8059 14.407 26.4193 14.407 24.9158V14.6911H17.4142V24.9584C17.4142 28.073 15.5885 29.4907 12.9248 29.4907C10.5192 29.4907 9.12274 28.2448 8.41392 26.7413"
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string;
|
||||
width?: string;
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px' }: Props = $props();
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg {width} {height} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 -960 960 960"
|
||||
><path
|
||||
fill="#f9a825"
|
||||
d="M560-160v-80h120q17 0 28.5-11.5T720-280v-80q0-38 22-69t58-44v-14q-36-13-58-44t-22-69v-80q0-17-11.5-28.5T680-720H560v-80h120q50 0 85 35t35 85v80q0 17 11.5 28.5T840-560h40v160h-40q-17 0-28.5 11.5T800-360v80q0 50-35 85t-85 35zm-280 0q-50 0-85-35t-35-85v-80q0-17-11.5-28.5T120-400H80v-160h40q17 0 28.5-11.5T160-600v-80q0-50 35-85t85-35h120v80H280q-17 0-28.5 11.5T240-680v80q0 38-22 69t-58 44v14q36 13 58 44t22 69v80q0 17 11.5 28.5T280-240h120v80z"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
fill="#000000"
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><path
|
||||
d="M21.142 10.843c0-.64.098-1.018.098-1.707 0-1.067-.393-1.411-1.263-1.411h-.641v.755h.196c.444 0 .542.147.542.689 0 .509-.049 1.018-.049 1.608 0 .755.246 1.05.739 1.165v.049c-.493.116-.739.411-.739 1.166 0 .591.049 1.067.049 1.608 0 .558-.114.705-.542.705v.017h-.196v.788h.641c.87 0 1.263-.344 1.263-1.411 0-.706-.098-1.067-.098-1.707 0-.345.213-.706.854-.739v-.853c-.642-.016-.854-.377-.854-.722zm-3.299 1.001c-.493-.196-.952-.312-.952-.64 0-.246.197-.395.558-.395.361 0 .689.148 1.05.411l.657-.87c-.409-.313-.968-.641-1.724-.641-1.115 0-1.871.641-1.871 1.544 0 .804.706 1.214 1.298 1.443.508.196 1.001.361 1.001.689 0 .246-.197.41-.641.41-.41 0-.821-.164-1.263-.509l-.657.952c.492.41 1.247.689 1.871.689 1.313 0 2.019-.689 2.019-1.592-.001-.9-.707-1.277-1.346-1.491zm-11.21 1.854c-.114 0-.262-.098-.262-.41V7.725H4.039c-.886 0-1.279.344-1.279 1.411 0 .706.099 1.101.099 1.707 0 .345-.213.706-.854.739v.853c.641.017.854.378.854.723 0 .606-.099.968-.099 1.674 0 1.067.394 1.411 1.264 1.411h.64v-.755h-.197c-.411 0-.542-.164-.542-.706 0-.541.049-1.001.049-1.607 0-.756-.245-1.05-.738-1.165v-.051c.493-.114.738-.409.738-1.165 0-.59-.049-1.066-.049-1.607 0-.542.114-.689.542-.689h.442v4.711c0 1.001.345 1.657 1.346 1.657.313 0 .559-.05.739-.115l-.165-1.066c-.098.013-.146.013-.196.013zm7.238-1.854c-.509-.196-.969-.312-.969-.64 0-.246.197-.395.558-.395.361 0 .689.148 1.051.411l.656-.87c-.41-.313-.968-.641-1.723-.641-1.116 0-1.872.641-1.872 1.544 0 .804.707 1.214 1.297 1.443.51.196 1.002.361 1.002.689 0 .246-.197.41-.641.41-.41 0-.82-.164-1.264-.509l-.64.952c.492.41 1.247.689 1.871.689 1.313 0 2.019-.689 2.019-1.592.001-.9-.705-1.277-1.345-1.491zm-4.842-2.15c-1.198 0-2.347 1.001-2.314 2.577 0 1.624 1.066 2.576 2.479 2.576.591 0 1.247-.214 1.756-.558l-.492-.87c-.36.213-.706.312-1.066.312-.657 0-1.165-.312-1.297-1.066h2.971c.017-.115.049-.345.049-.607.016-1.33-.707-2.364-2.086-2.364zm-.935 2.068c.099-.655.492-.969.951-.969.592 0 .821.411.821.969H8.094z"
|
||||
/></svg
|
||||
>
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0 8C0 5.79086 1.79086 4 4 4H20C22.2091 4 24 5.79086 24 8V16C24 18.2091 22.2091 20 20 20H4C1.79086 20 0 18.2091 0 16V8ZM4 6C2.89543 6 2 6.89543 2 8V16C2 17.1046 2.89543 18 4 18H20C21.1046 18 22 17.1046 22 16V8C22 6.89543 21.1046 6 20 6H4ZM5.68377 8.05132C6.09211 7.9152 6.54174 8.05566 6.8 8.4L9 11.3333L11.2 8.4C11.4583 8.05566 11.9079 7.9152 12.3162 8.05132C12.7246 8.18743 13 8.56957 13 9V15C13 15.5523 12.5523 16 12 16C11.4477 16 11 15.5523 11 15V12L9.8 13.6C9.61115 13.8518 9.31476 14 9 14C8.68524 14 8.38885 13.8518 8.2 13.6L7 12V15C7 15.5523 6.55228 16 6 16C5.44772 16 5 15.5523 5 15V9C5 8.56957 5.27543 8.18743 5.68377 8.05132ZM18 9C18 8.44772 17.5523 8 17 8C16.4477 8 16 8.44772 16 9V12.5858L15.7071 12.2929C15.3166 11.9024 14.6834 11.9024 14.2929 12.2929C13.9024 12.6834 13.9024 13.3166 14.2929 13.7071L16.2929 15.7071C16.6834 16.0976 17.3166 16.0976 17.7071 15.7071L19.7071 13.7071C20.0976 13.3166 20.0976 12.6834 19.7071 12.2929C19.3166 11.9024 18.6834 11.9024 18.2929 12.2929L18 12.5858V9Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
</svg>
|
||||
@@ -1,13 +1,23 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string;
|
||||
width?: string;
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px' }: Props = $props();
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg {width} {height} viewBox="0 0 600 600" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 600 600"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
>
|
||||
<g transform="translate(300, 300)">
|
||||
<!-- Central circle -->
|
||||
<circle fill="#61DAFB" r="50" />
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 410.87 410.87"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
><defs
|
||||
><style>
|
||||
.cls-1 {
|
||||
fill: #c69;
|
||||
fill-rule: evenodd;
|
||||
}
|
||||
.cls-2 {
|
||||
fill: #fff;
|
||||
}
|
||||
</style></defs
|
||||
><path
|
||||
class="cls-1"
|
||||
d="M205.44,0c113.46,0,205.44,91.98,205.44,205.44s-91.98,205.44-205.44,205.44S0,318.89,0,205.44,91.98,0,205.44,0h0Z"
|
||||
/><path
|
||||
class="cls-2"
|
||||
d="M334.3,87.93c-9.3-36.49-69.8-48.48-127.06-28.14-34.08,12.11-70.96,31.11-97.49,55.92-31.54,29.5-36.57,55.17-34.49,65.9,7.31,37.85,59.18,62.6,80.5,80.95v.11c-6.29,3.09-52.3,26.38-63.07,50.19-11.36,25.12,1.81,43.14,10.53,45.57,27.02,7.52,54.74-6,69.64-28.23,14.38-21.45,13.18-49.14,6.93-62.92,8.62-2.27,18.67-3.29,31.44-1.8,36.04,4.21,43.11,26.71,41.76,36.13-1.35,9.42-8.91,14.6-11.44,16.16-2.53,1.56-3.3,2.11-3.09,3.27.31,1.69,1.48,1.63,3.63,1.26,2.97-.5,18.92-7.66,19.61-25.04.87-22.07-20.28-46.76-57.73-46.11-15.42.27-25.12,1.73-32.13,4.34-.52-.59-1.04-1.18-1.59-1.76-23.15-24.7-65.95-42.17-64.14-75.38.66-12.07,4.86-43.86,82.24-82.42,63.39-31.59,114.14-22.89,122.91-3.63,12.53,27.52-27.12,78.67-92.95,86.05-25.08,2.81-38.29-6.91-41.57-10.53-3.46-3.81-3.97-3.98-5.27-3.27-2.1,1.17-.77,4.53,0,6.54,1.97,5.12,10.03,14.19,23.78,18.7,12.1,3.97,41.54,6.15,77.16-7.62,39.88-15.43,71.03-58.35,61.88-94.22ZM164.61,273.87c2.99,11.06,2.66,21.37-.43,30.7-.34,1.04-.72,2.06-1.13,3.08-.41,1.01-.85,2.01-1.33,3-2.38,4.94-5.58,9.56-9.49,13.83-11.94,13.02-28.61,17.95-35.76,13.8-7.72-4.48-3.85-22.84,9.98-37.46,14.89-15.74,36.31-25.86,36.31-25.86l-.03-.06c.62-.33,1.24-.67,1.88-1.02Z"
|
||||
/></svg
|
||||
>
|
||||
@@ -1,13 +1,23 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string;
|
||||
width?: string;
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px' }: Props = $props();
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg {width} {height} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 32 32"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
>
|
||||
<path
|
||||
d="M27.5342 4.35822C24.589 0.144416 18.7724 -1.10443 14.5671 1.5745L7.1816 6.28105C5.16609 7.54695 3.77642 9.60267 3.35293 11.9448C2.99976 13.8998 3.30868 15.9168 4.23081 17.6765C3.59882 18.6352 3.16817 19.7123 2.96497 20.8425C2.53883 23.2342 3.09525 25.6967 4.50833 27.6729C7.45346 31.8867 13.2701 33.1356 17.4754 30.4566L24.8609 25.7699C26.8739 24.5014 28.2628 22.4469 28.6896 20.1062C29.0414 18.1518 28.7315 16.1358 27.8089 14.3773C28.4405 13.4181 28.872 12.3413 29.0775 11.2113C29.5024 8.8196 28.9461 6.35758 27.5342 4.38088"
|
||||
fill="#FF3E00"
|
||||
|
||||
@@ -1,13 +1,23 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string;
|
||||
width?: string;
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px' }: Props = $props();
|
||||
let { height = '24px', width = '24px', size = 16 }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg {width} {height} viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg" version="1.1">
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
>
|
||||
<path
|
||||
d="M25.6 2.1875H32L16 29.7875L0 2.1875H6.32H12.24L16 8.5875L19.68 2.1875H25.6Z"
|
||||
fill="#41B883"
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
height?: string
|
||||
width?: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { height = '24px', width = '24px', size = undefined }: Props = $props()
|
||||
|
||||
const derivedWidth = $derived(size ? size : width)
|
||||
const derivedHeight = $derived(size ? size : height)
|
||||
</script>
|
||||
|
||||
<svg
|
||||
width={derivedWidth}
|
||||
height={derivedHeight}
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fill-rule="nonzero"
|
||||
clip-rule="nonzero"
|
||||
d="M0.828288 0.135541C1.17426 -0.103975 1.64888 -0.017679 1.8884 0.328288L3.58007 2.77182L5.48344 0.297431C5.74 -0.0360947 6.21836 -0.0984891 6.55189 0.158069C6.88541 0.414627 6.94781 0.892984 6.69125 1.22651L4.30958 4.32268V7.11115C4.30958 7.53194 3.96846 7.87305 3.54767 7.87305C3.12689 7.87305 2.78577 7.53194 2.78577 7.11115V4.30154L0.635541 1.19565C0.396025 0.849685 0.482321 0.375057 0.828288 0.135541ZM8.88099 6.89044e-05C9.18311 6.89044e-05 9.45669 0.178586 9.57837 0.455123L12.372 6.8043C12.5415 7.18945 12.3666 7.63906 11.9815 7.80853C11.5963 7.978 11.1467 7.80315 10.9772 7.418L10.2835 5.84131H7.47847L6.78473 7.418C6.61526 7.80315 6.16565 7.978 5.7805 7.80853C5.39535 7.63906 5.2205 7.18945 5.38997 6.8043L8.18361 0.455123C8.30528 0.178586 8.57886 6.89044e-05 8.88099 6.89044e-05ZM8.14894 4.31751H9.61303L8.88099 2.65377L8.14894 4.31751ZM9.60471 8.66615C9.92856 8.76179 10.1508 9.05918 10.1508 9.39685V15.2381C10.1508 15.6589 9.80971 16 9.38892 16C8.96813 16 8.62702 15.6589 8.62702 15.2381V11.9768L7.23495 14.1282C7.09962 14.3373 6.8706 14.4671 6.62164 14.4757C6.37268 14.4844 6.13523 14.3707 5.98576 14.1714L4.30958 11.9365V15.2381C4.30958 15.6589 3.96846 16 3.54767 16C3.12689 16 2.78577 15.6589 2.78577 15.2381V9.65082C2.78577 9.32288 2.99562 9.03172 3.30674 8.92802C3.61786 8.82431 3.96043 8.93132 4.1572 9.19368L6.5492 12.383L8.74925 8.98295C8.93269 8.69945 9.28086 8.57051 9.60471 8.66615ZM11.9286 8.63495C12.3494 8.63495 12.6905 8.97607 12.6905 9.39685V14.4762H14.7222C15.143 14.4762 15.4841 14.8173 15.4841 15.2381C15.4841 15.6589 15.143 16 14.7222 16H11.9286C11.5078 16 11.1667 15.6589 11.1667 15.2381V9.39685C11.1667 8.97607 11.5078 8.63495 11.9286 8.63495Z"
|
||||
fill="#000000"
|
||||
/>
|
||||
</svg>
|
||||
@@ -9,6 +9,7 @@
|
||||
toDatatableItems,
|
||||
toSchemaItems
|
||||
} from './datatableUtils.svelte'
|
||||
import { Button } from '../common'
|
||||
|
||||
interface Props {
|
||||
/** Currently selected datatable */
|
||||
@@ -48,15 +49,18 @@
|
||||
|
||||
<Popover>
|
||||
<svelte:fragment slot="trigger">
|
||||
<button
|
||||
class="pt-1.5 pb-0.5 px-1 hover:bg-surface-hover rounded transition-colors"
|
||||
<Button
|
||||
title="Configure default datatable & schema"
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
nonCaptureEvent
|
||||
btnClasses="px-1"
|
||||
>
|
||||
<Settings size={12} class="text-tertiary" />
|
||||
</button>
|
||||
<Settings size={12} />
|
||||
</Button>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="content">
|
||||
<div class="flex flex-col gap-3 p-2 min-w-64 max-w-80">
|
||||
<div class="flex flex-col gap-3 p-4 min-w-64 max-w-80">
|
||||
<div class="text-xs font-medium text-primary">Default Datatable & Schema</div>
|
||||
|
||||
<p class="text-2xs text-tertiary leading-relaxed">
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<script lang="ts" module>
|
||||
export const SUPPORTED_EXTENSIONS = ['tsx', 'json', 'ts', 'js', 'vue', 'css', 'svelte']
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import TypeScript from '../common/languageIcons/TypeScript.svelte'
|
||||
import JavaScriptIcon from '../icons/JavaScriptIcon.svelte'
|
||||
@@ -8,23 +12,27 @@
|
||||
|
||||
interface Props {
|
||||
file: string
|
||||
size?: number
|
||||
}
|
||||
|
||||
let { file }: Props = $props()
|
||||
let { file, size = 16 }: Props = $props()
|
||||
|
||||
const width = $derived(size.toString() + 'px')
|
||||
const height = $derived(size.toString() + 'px')
|
||||
</script>
|
||||
|
||||
{#if file.endsWith('.tsx')}
|
||||
<ReactIcon width="16px" height="16px" />
|
||||
<ReactIcon {width} {height} />
|
||||
{:else if file.endsWith('.json')}
|
||||
<JsonIcon width="16px" height="16px" />
|
||||
<JsonIcon {width} {height} />
|
||||
{:else if file.endsWith('.ts')}
|
||||
<TypeScript width={16} height={16} />
|
||||
<TypeScript width={size} height={size} />
|
||||
{:else if file.endsWith('.js')}
|
||||
<JavaScriptIcon width="16px" height="16px" />
|
||||
<JavaScriptIcon {width} {height} />
|
||||
{:else if file.endsWith('.vue')}
|
||||
<VueIcon width="16px" height="16px" />
|
||||
<VueIcon {width} {height} />
|
||||
{:else if file.endsWith('.css')}
|
||||
<span class="text-blue-600 ml-0.5" style="font-size: 16px;">#</span>
|
||||
{:else if file.endsWith('.svelte')}
|
||||
<SvelteIcon width="16px" height="16px" />
|
||||
<SvelteIcon {width} {height} />
|
||||
{/if}
|
||||
|
||||
@@ -4,15 +4,30 @@
|
||||
ChevronDown,
|
||||
File,
|
||||
Folder,
|
||||
FileJson,
|
||||
FileCode,
|
||||
ImageIcon,
|
||||
Palette,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Lock
|
||||
Lock,
|
||||
Ellipsis,
|
||||
ImageIcon
|
||||
} from 'lucide-svelte'
|
||||
import Self from './FileTreeNode.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import DropdownV2 from '../DropdownV2.svelte'
|
||||
import { Button } from '../common'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
import TypeScript from '../common/languageIcons/TypeScript.svelte'
|
||||
import JavaScriptIcon from '../icons/JavaScriptIcon.svelte'
|
||||
import JsonIcon from '../icons/JsonIcon.svelte'
|
||||
import ReactIcon from '../icons/ReactIcon.svelte'
|
||||
import SvelteIcon from '../icons/SvelteIcon.svelte'
|
||||
import VueIcon from '../icons/VueIcon.svelte'
|
||||
import CssIcon from '../icons/CssIcon.svelte'
|
||||
import SassIcon from '../icons/SassIcon.svelte'
|
||||
import LessIcon from '../icons/LessIcon.svelte'
|
||||
import HtmlIcon from '../icons/HtmlIcon.svelte'
|
||||
import MarkdownIcon from '../icons/MarkdownIcon.svelte'
|
||||
import YamlIcon from '../icons/YamlIcon.svelte'
|
||||
import { tick } from 'svelte'
|
||||
|
||||
interface TreeNode {
|
||||
name: string
|
||||
@@ -28,9 +43,10 @@
|
||||
onAddFolder?: (folderPath: string) => void
|
||||
onRename?: (oldPath: string, newName: string) => void
|
||||
onDelete?: (path: string) => void
|
||||
onRequestEdit?: (path: string) => void
|
||||
onCancelEdit?: () => void
|
||||
selectedPath?: string
|
||||
pathToRename?: string
|
||||
pathToExpand?: string
|
||||
pathToEdit?: string
|
||||
noEdit?: boolean
|
||||
level?: number
|
||||
}
|
||||
@@ -42,24 +58,34 @@
|
||||
onAddFolder,
|
||||
onRename,
|
||||
onDelete,
|
||||
onRequestEdit,
|
||||
onCancelEdit,
|
||||
selectedPath,
|
||||
pathToRename,
|
||||
pathToExpand,
|
||||
pathToEdit,
|
||||
noEdit = false,
|
||||
level = 0
|
||||
}: Props = $props()
|
||||
|
||||
let expanded = $state(level === 0) // Root folders start expanded
|
||||
let userExpanded = $state<boolean | null>(null) // null = not set by user
|
||||
let isHovered = $state(false)
|
||||
let isEditing = $state(false)
|
||||
let editValue = $state(node.name)
|
||||
let inputElement: HTMLInputElement | undefined = $state()
|
||||
let textInputElement: TextInput | undefined = $state()
|
||||
let dropdownOpen = $state(false)
|
||||
|
||||
const isSelected = $derived(selectedPath === node.path)
|
||||
const isEditing = $derived(pathToEdit === node.path)
|
||||
const expanded = $derived(
|
||||
// Auto-expand for editing nested paths takes priority
|
||||
pathToEdit && node.isFolder && pathToEdit.startsWith(node.path)
|
||||
? true
|
||||
: userExpanded !== null
|
||||
? userExpanded
|
||||
: level === 0 // Default: root expanded
|
||||
)
|
||||
|
||||
function toggleExpanded() {
|
||||
if (node.isFolder) {
|
||||
expanded = !expanded
|
||||
userExpanded = !expanded
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +101,7 @@
|
||||
|
||||
function handleEdit(e: MouseEvent) {
|
||||
e.stopPropagation()
|
||||
isEditing = true
|
||||
editValue = node.name
|
||||
onRequestEdit?.(node.path)
|
||||
}
|
||||
|
||||
function handleDelete(e: MouseEvent) {
|
||||
@@ -85,58 +110,37 @@
|
||||
}
|
||||
|
||||
function finishEdit() {
|
||||
if (isEditing && editValue.trim() && editValue !== node.name) {
|
||||
if (isEditing && editValue.trim()) {
|
||||
// Always call onRename - parent handles whether it's a new file or actual rename
|
||||
onRename?.(node.path, editValue.trim())
|
||||
} else {
|
||||
onCancelEdit?.()
|
||||
}
|
||||
isEditing = false
|
||||
}
|
||||
|
||||
function handleInputKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') {
|
||||
finishEdit()
|
||||
} else if (e.key === 'Escape') {
|
||||
isEditing = false
|
||||
editValue = node.name
|
||||
editValue = node.name // Reset to original
|
||||
onCancelEdit?.()
|
||||
}
|
||||
}
|
||||
|
||||
function handleInputBlur() {
|
||||
function handleInputBlur(e: FocusEvent) {
|
||||
finishEdit()
|
||||
}
|
||||
|
||||
// Single effect for DOM operations only
|
||||
$effect(() => {
|
||||
if (isEditing && inputElement) {
|
||||
inputElement.focus()
|
||||
inputElement.select()
|
||||
}
|
||||
})
|
||||
|
||||
// Automatically enter edit mode for newly created files
|
||||
$effect(() => {
|
||||
if (pathToRename === node.path && !isEditing) {
|
||||
isEditing = true
|
||||
if (isEditing && textInputElement) {
|
||||
// Reset edit value when entering edit mode
|
||||
editValue = node.name
|
||||
// If this is a folder, expand it
|
||||
if (node.isFolder) {
|
||||
expanded = true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Expand parent folders when a child needs to be renamed
|
||||
$effect(() => {
|
||||
if (pathToRename && node.isFolder && pathToRename.startsWith(node.path + '/')) {
|
||||
expanded = true
|
||||
}
|
||||
})
|
||||
|
||||
// Expand folder when pathToExpand matches this node or a parent of pathToExpand
|
||||
$effect(() => {
|
||||
if (pathToExpand && node.isFolder) {
|
||||
// Expand if this folder is the target or an ancestor of the target
|
||||
if (node.path === pathToExpand || pathToExpand.startsWith(node.path + '/')) {
|
||||
expanded = true
|
||||
}
|
||||
// Focus and select input (DOM side effects)
|
||||
tick().then(() => {
|
||||
textInputElement?.focus()
|
||||
textInputElement?.select()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
@@ -164,22 +168,26 @@
|
||||
|
||||
switch (ext) {
|
||||
case 'json':
|
||||
return { icon: FileJson, className: 'text-yellow-500' }
|
||||
return { icon: JsonIcon }
|
||||
case 'tsx':
|
||||
return { icon: ReactIcon }
|
||||
case 'jsx':
|
||||
return { icon: FileCode, className: 'text-blue-500' }
|
||||
return { icon: ReactIcon }
|
||||
case 'ts':
|
||||
return { icon: TypeScript }
|
||||
case 'js':
|
||||
return { icon: FileCode, className: 'text-blue-400' }
|
||||
return { icon: JavaScriptIcon }
|
||||
case 'svelte':
|
||||
return { icon: FileCode, className: 'text-orange-500' }
|
||||
return { icon: SvelteIcon }
|
||||
case 'vue':
|
||||
return { icon: FileCode, className: 'text-green-500' }
|
||||
return { icon: VueIcon }
|
||||
case 'css':
|
||||
return { icon: CssIcon }
|
||||
case 'scss':
|
||||
case 'sass':
|
||||
return { icon: SassIcon }
|
||||
case 'less':
|
||||
return { icon: Palette, className: 'text-pink-500' }
|
||||
return { icon: LessIcon }
|
||||
case 'png':
|
||||
case 'jpg':
|
||||
case 'jpeg':
|
||||
@@ -188,6 +196,15 @@
|
||||
case 'webp':
|
||||
case 'ico':
|
||||
return { icon: ImageIcon, className: 'text-purple-500' }
|
||||
case 'html':
|
||||
case 'htm':
|
||||
return { icon: HtmlIcon }
|
||||
case 'md':
|
||||
case 'markdown':
|
||||
return { icon: MarkdownIcon }
|
||||
case 'yaml':
|
||||
case 'yml':
|
||||
return { icon: YamlIcon }
|
||||
default:
|
||||
return { icon: File, className: 'text-tertiary' }
|
||||
}
|
||||
@@ -203,7 +220,7 @@
|
||||
>
|
||||
{#if isEditing}
|
||||
<div
|
||||
class="w-full flex items-center gap-1 px-2 py-1 text-xs rounded {isSelected
|
||||
class="w-full flex items-center gap-1 px-2 min-h-6 text-xs rounded {isSelected
|
||||
? 'bg-blue-100 dark:bg-blue-900/30'
|
||||
: ''}"
|
||||
style="padding-left: {level * 12}px"
|
||||
@@ -217,26 +234,28 @@
|
||||
<ChevronRight size={12} />
|
||||
{/if}
|
||||
</span>
|
||||
<IconComponent size={12} class="flex-shrink-0 {fileIcon.className}" />
|
||||
<IconComponent size={14} class="flex-shrink-0 {fileIcon.className}" />
|
||||
{:else}
|
||||
{@const IconComponent = fileIcon.icon}
|
||||
<span class="flex-shrink-0"></span>
|
||||
<IconComponent size={12} class="flex-shrink-0 {fileIcon.className}" />
|
||||
<IconComponent size={14} class="flex-shrink-0 {fileIcon.className}" />
|
||||
{/if}
|
||||
<input
|
||||
bind:this={inputElement}
|
||||
<TextInput
|
||||
bind:this={textInputElement}
|
||||
bind:value={editValue}
|
||||
onkeydown={handleInputKeydown}
|
||||
onblur={handleInputBlur}
|
||||
class="flex-1 min-w-0 bg-surface border border-blue-500 rounded px-1 text-primary font-mono focus:outline-none focus:ring-1 focus:ring-blue-500"
|
||||
type="text"
|
||||
inputProps={{
|
||||
onkeydown: handleInputKeydown,
|
||||
onblur: (e) => handleInputBlur(e),
|
||||
type: 'text'
|
||||
}}
|
||||
size="xs"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<button
|
||||
onclick={handleClick}
|
||||
class="w-full flex items-center gap-1 px-2 py-1 text-xs hover:bg-surface-hover transition-colors rounded text-left {isSelected
|
||||
? 'bg-blue-100 dark:bg-blue-900/30'
|
||||
? 'bg-surface-accent-selected'
|
||||
: ''}"
|
||||
style="padding-left: {level * 12}px"
|
||||
>
|
||||
@@ -255,32 +274,44 @@
|
||||
<span class="flex-shrink-0"></span>
|
||||
<IconComponent size={12} class="flex-shrink-0 {fileIcon.className}" />
|
||||
{/if}
|
||||
<span class="truncate text-primary font-mono">{node.name}</span>
|
||||
<span class={twMerge('truncate text-primary font-normal', isSelected ? 'text-accent' : '')}
|
||||
>{node.name}</span
|
||||
>
|
||||
</button>
|
||||
|
||||
{#if isHovered && !isEditing}
|
||||
<div
|
||||
class="absolute right-1 top-1 flex gap-0.5 bg-surface rounded shadow-sm border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
{#if !noEdit}
|
||||
<button
|
||||
onclick={handleEdit}
|
||||
class="p-0.5 hover:bg-surface-hover rounded transition-colors"
|
||||
title="Rename"
|
||||
>
|
||||
<Pencil size={12} class="text-secondary" />
|
||||
</button>
|
||||
<button
|
||||
onclick={handleDelete}
|
||||
class="p-0.5 hover:bg-surface-hover rounded transition-colors"
|
||||
title="Delete"
|
||||
>
|
||||
<Trash2 size={12} class="text-red-500" />
|
||||
</button>
|
||||
{:else}
|
||||
<Lock size={12} class="text-secondary" />
|
||||
{/if}
|
||||
</div>
|
||||
{#if isHovered || dropdownOpen}
|
||||
{#if !noEdit}
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Rename',
|
||||
icon: Pencil,
|
||||
action: handleEdit
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: Trash2,
|
||||
action: handleDelete,
|
||||
type: 'delete'
|
||||
}
|
||||
]}
|
||||
placement="bottom-end"
|
||||
class="absolute -translate-y-1/2 top-1/2 right-1"
|
||||
bind:open={dropdownOpen}
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<Button
|
||||
iconOnly
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
nonCaptureEvent
|
||||
startIcon={{ icon: Ellipsis }}
|
||||
></Button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
{:else}
|
||||
<Lock size={12} class="text-secondary absolute -translate-y-1/2 top-1/2 right-2" />
|
||||
{/if}
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
@@ -294,9 +325,10 @@
|
||||
{onAddFolder}
|
||||
{onRename}
|
||||
{onDelete}
|
||||
{onRequestEdit}
|
||||
{onCancelEdit}
|
||||
{selectedPath}
|
||||
{pathToRename}
|
||||
{pathToExpand}
|
||||
{pathToEdit}
|
||||
level={level + 1}
|
||||
/>
|
||||
{/each}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import PanelSection from '../apps/editor/settingsPanel/common/PanelSection.svelte'
|
||||
import type { DataTableRef } from './dataTableRefUtils'
|
||||
import DefaultDatabaseSelector from './DefaultDatabaseSelector.svelte'
|
||||
import { Button } from '../common'
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export type { DataTableRef } from './dataTableRefUtils'
|
||||
@@ -86,7 +87,7 @@
|
||||
</script>
|
||||
|
||||
{#snippet actionButtons()}
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center gap-1">
|
||||
<!-- Settings popover for default database/schema -->
|
||||
{#if !hideDefaultSelector}
|
||||
<DefaultDatabaseSelector
|
||||
@@ -97,14 +98,16 @@
|
||||
{/if}
|
||||
|
||||
<!-- Add datatable button -->
|
||||
<button
|
||||
onclick={() => onAdd?.()}
|
||||
class="pt-1.5 pb-0.5 px-1 hover:bg-surface-hover rounded transition-colors flex items-center gap-0.5"
|
||||
<Button
|
||||
onClick={() => onAdd?.()}
|
||||
title="Add datatable reference"
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
btnClasses="px-1 gap-0.5"
|
||||
>
|
||||
<Plus size={12} class="text-secondary" />
|
||||
<Database size={12} class="text-tertiary" />
|
||||
</button>
|
||||
<Plus size={12} />
|
||||
<Database size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
@@ -180,7 +183,7 @@
|
||||
{#if standalone}
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-sm text-primary">Existing tables to use</span>
|
||||
<span class="text-xs text-emphasis font-semibold">Existing tables to use</span>
|
||||
{@render actionButtons()}
|
||||
</div>
|
||||
{@render tableList()}
|
||||
@@ -188,7 +191,7 @@
|
||||
{:else}
|
||||
<PanelSection
|
||||
fullHeight={false}
|
||||
size="lg"
|
||||
size="sm"
|
||||
title="data"
|
||||
id="app-editor-data-panel"
|
||||
tooltip="Data tables to use in the app. Adding some here does not change the behavior of the app, since they can be used in code directly regardless. But it allows AI to always keep their schema in context as well as allowing quick access and clear view of the app's data layer."
|
||||
|
||||
@@ -878,7 +878,7 @@
|
||||
appPath={path}
|
||||
{selectedRunnable}
|
||||
{initRunnablesContent}
|
||||
{runnables}
|
||||
bind:runnables
|
||||
onSelectionChange={(selection) => {
|
||||
console.log('handle selection', selection)
|
||||
|
||||
|
||||
@@ -8,10 +8,10 @@
|
||||
import {
|
||||
Bug,
|
||||
DiffIcon,
|
||||
EllipsisVertical,
|
||||
FileJson,
|
||||
FileUp,
|
||||
History,
|
||||
MoreVertical,
|
||||
Pen,
|
||||
Save,
|
||||
WandSparkles
|
||||
@@ -48,7 +48,7 @@
|
||||
import { aiChatManager } from '../copilot/chat/AIChatManager.svelte'
|
||||
import { AIBtnClasses } from '../copilot/chat/AIButtonStyle'
|
||||
import type { RawAppData } from './dataTableRefUtils'
|
||||
|
||||
|
||||
// async function hash(message) {
|
||||
// try {
|
||||
// const msgUint8 = new TextEncoder().encode(message) // encode as (utf-8) Uint8Array
|
||||
@@ -632,7 +632,7 @@
|
||||
<Drawer bind:open={saveDrawerOpen} size="800px">
|
||||
<DrawerContent title="Deploy" on:close={() => closeSaveDrawer()}>
|
||||
{#snippet actions()}
|
||||
<div class="flex flex-row gap-4">
|
||||
<div class="flex flex-row gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
disabled={!savedApp || savedApp.draft_only}
|
||||
@@ -675,6 +675,8 @@
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
variant="accent"
|
||||
unifiedSize="md"
|
||||
startIcon={{ icon: Save }}
|
||||
disabled={pathError != '' || customPathError != '' || app == undefined}
|
||||
on:click={() => {
|
||||
@@ -781,11 +783,13 @@
|
||||
<div class="flex flex-row gap-2 justify-end items-center overflow-visible">
|
||||
<DropdownV2 items={moreItems} class="h-auto">
|
||||
{#snippet buttonReplacement()}
|
||||
<Button nonCaptureEvent size="xs" color="light">
|
||||
<div class="flex flex-row items-center">
|
||||
<MoreVertical size={14} />
|
||||
</div>
|
||||
</Button>
|
||||
<Button
|
||||
nonCaptureEvent
|
||||
unifiedSize="md"
|
||||
variant="subtle"
|
||||
startIcon={{ icon: EllipsisVertical }}
|
||||
iconOnly
|
||||
></Button>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
|
||||
@@ -795,8 +799,8 @@
|
||||
jobsDrawerOpen = true
|
||||
}}
|
||||
color="light"
|
||||
size="xs"
|
||||
variant="border"
|
||||
unifiedSize="md"
|
||||
variant="default"
|
||||
btnClasses="relative"
|
||||
>
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
@@ -811,8 +815,7 @@
|
||||
</div>
|
||||
<AppExportButton bind:this={appExport} />
|
||||
<Button
|
||||
unifiedSized="sm"
|
||||
color="light"
|
||||
unifiedSize="md"
|
||||
variant="default"
|
||||
onClick={() => aiChatManager.toggleOpen()}
|
||||
startIcon={{ icon: WandSparkles }}
|
||||
@@ -825,7 +828,8 @@
|
||||
loading={loading.save}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={() => saveDraft()}
|
||||
size="xs"
|
||||
unifiedSize="md"
|
||||
variant="default"
|
||||
disabled={!newApp && !savedApp}
|
||||
shortCut={{ key: 'S' }}
|
||||
>
|
||||
@@ -835,7 +839,8 @@
|
||||
loading={loading.save}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={save}
|
||||
size="xs"
|
||||
unifiedSize="md"
|
||||
variant="accent"
|
||||
dropdownItems={appPath != ''
|
||||
? () => [
|
||||
{
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Button } from '$lib/components/common'
|
||||
import { Plus, AlertCircle, AlertTriangle } from 'lucide-svelte'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Plus, File, AlertCircle, AlertTriangle } from 'lucide-svelte'
|
||||
import PanelSection from '../apps/editor/settingsPanel/common/PanelSection.svelte'
|
||||
import Popover from '../Popover.svelte'
|
||||
|
||||
import type { Runnable } from '../apps/inputType'
|
||||
import { getNextId } from '$lib/components/flows/idUtils'
|
||||
import { rawAppLintStore } from './lintStore'
|
||||
import RunnableRow from './RunnableRow.svelte'
|
||||
|
||||
interface Props {
|
||||
selectedRunnable: string | undefined
|
||||
runnables: Record<string, Runnable>
|
||||
onSelect?: (id: string) => void
|
||||
}
|
||||
|
||||
let { selectedRunnable = $bindable(), runnables }: Props = $props()
|
||||
let { selectedRunnable = $bindable(), runnables, onSelect }: Props = $props()
|
||||
|
||||
// Subscribe to lint store for reactive updates
|
||||
let lintSnapshot = $state(rawAppLintStore.getSnapshot())
|
||||
@@ -42,22 +44,24 @@
|
||||
}
|
||||
|
||||
selectedRunnable = nid
|
||||
onSelect?.(nid)
|
||||
}
|
||||
</script>
|
||||
|
||||
<PanelSection size="lg" fullHeight={false} title="backend" id="app-editor-runnable-panel">
|
||||
<PanelSection size="sm" fullHeight={false} title="backend" id="app-editor-runnable-panel">
|
||||
{#snippet action()}
|
||||
<div class="flex flex-row gap-1">
|
||||
<Button
|
||||
size="xs"
|
||||
variant="default"
|
||||
btnClasses="!rounded-full !p-1"
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
title="Create a new background runnable"
|
||||
aria-label="Create a new background runnable"
|
||||
on:click={createBackgroundScript}
|
||||
id="create-background-runnable"
|
||||
btnClasses="gap-0.5 px-1"
|
||||
>
|
||||
<Plus size={14} class="!text-primary" />
|
||||
<Plus size={12} />
|
||||
<File size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -67,17 +71,21 @@
|
||||
{#if Object.keys(runnables ?? {}).length > 0}
|
||||
{#each Object.entries(runnables ?? {}) as [id, runnable]}
|
||||
{#if runnable}
|
||||
<button
|
||||
<RunnableRow
|
||||
{id}
|
||||
class="panel-item
|
||||
{selectedRunnable === id
|
||||
? 'border-blue-500 bg-blue-100 dark:bg-frost-900/50'
|
||||
: 'hover:bg-blue-50 dark:hover:bg-frost-900/50'}"
|
||||
onclick={() => (selectedRunnable = id)}
|
||||
>
|
||||
<span class="text-2xs truncate">{runnable?.name}</span>
|
||||
<Badge color="indigo">{id}</Badge>
|
||||
</button>
|
||||
{runnable}
|
||||
isSelected={selectedRunnable === id}
|
||||
onSelect={() => {
|
||||
selectedRunnable = id
|
||||
onSelect?.(id)
|
||||
}}
|
||||
onDelete={() => {
|
||||
delete runnables[id]
|
||||
if (selectedRunnable === id) {
|
||||
selectedRunnable = undefined
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else}
|
||||
|
||||
@@ -9,16 +9,23 @@
|
||||
appPath: string
|
||||
initRunnablesContent: Record<string, string>
|
||||
/** Called when code is selected in the editor */
|
||||
onSelectionChange?: (selection: {
|
||||
content: string
|
||||
startLine: number
|
||||
endLine: number
|
||||
startColumn: number
|
||||
endColumn: number
|
||||
} | null) => void
|
||||
onSelectionChange?: (
|
||||
selection: {
|
||||
content: string
|
||||
startLine: number
|
||||
endLine: number
|
||||
startColumn: number
|
||||
endColumn: number
|
||||
} | null
|
||||
) => void
|
||||
}
|
||||
|
||||
let { runnables, selectedRunnable = $bindable(), appPath, onSelectionChange }: Props = $props()
|
||||
let {
|
||||
runnables = $bindable(),
|
||||
selectedRunnable = $bindable(),
|
||||
appPath,
|
||||
onSelectionChange
|
||||
}: Props = $props()
|
||||
</script>
|
||||
|
||||
{#if !selectedRunnable}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from 'lucide-svelte'
|
||||
import PanelSection from '../apps/editor/settingsPanel/common/PanelSection.svelte'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
|
||||
export type Modules = {
|
||||
installed: Record<string, string>
|
||||
@@ -37,30 +38,30 @@
|
||||
</script>
|
||||
|
||||
<PanelSection
|
||||
size="sm"
|
||||
size="xs"
|
||||
collapsible
|
||||
initiallyCollapsed
|
||||
titlePadding="pl-1 !text-tertiary"
|
||||
titlePadding="!text-tertiary"
|
||||
fullHeight={false}
|
||||
title="packages ({Object.keys(props.modules?.installed ?? {}).length})"
|
||||
id="app-editor-frontend-panel-modules"
|
||||
>
|
||||
<input type="text" class="w-full max-w-sm" placeholder="Search packages" bind:value={search} />
|
||||
<TextInput inputProps={{ placeholder: 'Search packages' }} size="sm" bind:value={search} />
|
||||
<div class="mt-2 flex flex-col gap-4 w-full">
|
||||
{#each ['direct', 'indirect', 'dev'] as type}
|
||||
{@const typeModules = filteredModules[type]}
|
||||
{#if typeModules.length > 0}
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-sm px-2 text-secondary font-mono uppercase"
|
||||
<div class="text-xs px-2 text-hint text-2xs"
|
||||
>{type}
|
||||
<span class="text-2xs text-tertiary">({typeModules.length})</span></div
|
||||
<span>({typeModules.length})</span></div
|
||||
>
|
||||
{#each typeModules as mod (mod)}
|
||||
<div class="text-xs px-2 text-secondary font-mono flex justify-between w-full"
|
||||
<div class="text-xs px-2 text-primary font-mono flex justify-between w-full"
|
||||
><div class="truncate">{mod}</div>
|
||||
<div class="flex items-center gap-1">
|
||||
{#if props.modules?.installed?.[mod]}
|
||||
<span class="text-2xs px-2 text-tertiary font-mono">
|
||||
<span class="text-2xs px-2 text-hint">
|
||||
{props.modules?.installed?.[mod]}
|
||||
</span>
|
||||
{:else}
|
||||
@@ -72,5 +73,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if filteredModules.direct.length === 0 && filteredModules.indirect.length === 0 && filteredModules.dev.length === 0}
|
||||
<span class="text-xs px-2 text-hint text-2xs"
|
||||
>No packages found. Edit <b>package.json</b> to add them.</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</PanelSection>
|
||||
|
||||
@@ -74,47 +74,116 @@
|
||||
}
|
||||
}
|
||||
|
||||
const fileTree = $derived(buildFileTree(Object.keys(files ?? {})))
|
||||
// Track pending new file/folder that hasn't been confirmed yet
|
||||
let pendingNewFilePath = $state<string | undefined>(undefined)
|
||||
|
||||
let pathToRename = $state<string | undefined>(undefined)
|
||||
let pathToExpand = $state<string | undefined>(undefined)
|
||||
const fileTree = $derived(
|
||||
buildFileTree([
|
||||
...Object.keys(files ?? {}),
|
||||
...(pendingNewFilePath ? [pendingNewFilePath] : [])
|
||||
])
|
||||
)
|
||||
|
||||
let pathToEdit = $state<string | undefined>(undefined)
|
||||
|
||||
// Helper to find a unique path by appending numbers if needed
|
||||
function getUniquePath(basePath: string): string {
|
||||
const existingPaths = new Set([...Object.keys(files ?? {}), pendingNewFilePath].filter(Boolean))
|
||||
|
||||
if (!existingPaths.has(basePath)) {
|
||||
return basePath
|
||||
}
|
||||
|
||||
// Split path into name and extension (for files) or handle folders
|
||||
const isFolder = basePath.endsWith('/')
|
||||
let pathWithoutTrailing = isFolder ? basePath.slice(0, -1) : basePath
|
||||
const lastSlash = pathWithoutTrailing.lastIndexOf('/')
|
||||
const parentPath = pathWithoutTrailing.substring(0, lastSlash + 1)
|
||||
const fileName = pathWithoutTrailing.substring(lastSlash + 1)
|
||||
|
||||
let nameWithoutExt: string
|
||||
let ext: string
|
||||
|
||||
if (isFolder) {
|
||||
nameWithoutExt = fileName
|
||||
ext = ''
|
||||
} else {
|
||||
const dotIndex = fileName.lastIndexOf('.')
|
||||
if (dotIndex > 0) {
|
||||
nameWithoutExt = fileName.substring(0, dotIndex)
|
||||
ext = fileName.substring(dotIndex)
|
||||
} else {
|
||||
nameWithoutExt = fileName
|
||||
ext = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Try incrementing numbers until we find a unique path
|
||||
let counter = 1
|
||||
let candidatePath: string
|
||||
do {
|
||||
const newName = `${nameWithoutExt} (${counter})${ext}`
|
||||
candidatePath = isFolder ? `${parentPath}${newName}/` : `${parentPath}${newName}`
|
||||
counter++
|
||||
} while (existingPaths.has(candidatePath))
|
||||
|
||||
return candidatePath
|
||||
}
|
||||
|
||||
function handleFileClick(path: string) {
|
||||
console.log('File clicked:', path)
|
||||
selectedDocument = path
|
||||
onSelectFile?.(path)
|
||||
// Only open files in the editor, not folders
|
||||
if (!path.endsWith('/')) {
|
||||
onSelectFile?.(path)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddFile(folderPath: string) {
|
||||
console.log('Add file to:', folderPath)
|
||||
if (files) {
|
||||
const nfiles = { ...files }
|
||||
// Ensure folderPath ends with /
|
||||
const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/'
|
||||
const newPath = normalizedFolder + 'newfile.txt'
|
||||
nfiles[newPath] = ''
|
||||
files = nfiles
|
||||
pathToRename = newPath
|
||||
pathToExpand = normalizedFolder
|
||||
}
|
||||
// Ensure folderPath ends with /
|
||||
const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/'
|
||||
const basePath = normalizedFolder + 'newfile.txt'
|
||||
const newPath = getUniquePath(basePath)
|
||||
// Don't update files yet - just mark as pending and enter edit mode
|
||||
pendingNewFilePath = newPath
|
||||
pathToEdit = newPath
|
||||
}
|
||||
|
||||
function handleRename(oldPath: string, newName: string) {
|
||||
if (files) {
|
||||
const nfiles = { ...files }
|
||||
const pathParts = oldPath.split('/').filter(Boolean)
|
||||
const parentPath = '/' + pathParts.slice(0, -1).join('/')
|
||||
let newPath = parentPath === '/' ? '/' + newName : parentPath + '/' + newName
|
||||
const pathParts = oldPath.split('/').filter(Boolean)
|
||||
const parentPath = '/' + pathParts.slice(0, -1).join('/')
|
||||
let newPath = parentPath === '/' ? '/' + newName : parentPath + '/' + newName
|
||||
|
||||
// Check if this is a folder (ends with /)
|
||||
const isFolder = oldPath.endsWith('/')
|
||||
// Check if this is a folder (ends with /)
|
||||
const isFolder = oldPath.endsWith('/')
|
||||
|
||||
if (isFolder) {
|
||||
// For folders, ensure new path also ends with /
|
||||
if (!newPath.endsWith('/')) {
|
||||
newPath = newPath + '/'
|
||||
}
|
||||
// For folders, ensure new path also ends with /
|
||||
if (isFolder && !newPath.endsWith('/')) {
|
||||
newPath = newPath + '/'
|
||||
}
|
||||
|
||||
// Check if this is a pending new file/folder being created
|
||||
const isPendingNew = pendingNewFilePath === oldPath
|
||||
|
||||
// For existing items, skip if name didn't change
|
||||
if (!isPendingNew && oldPath === newPath) {
|
||||
pathToEdit = undefined
|
||||
return
|
||||
}
|
||||
|
||||
if (!files) {
|
||||
files = {}
|
||||
}
|
||||
const nfiles = { ...files }
|
||||
|
||||
if (isFolder) {
|
||||
if (isPendingNew) {
|
||||
// Creating a new folder - just add it with the final name
|
||||
nfiles[newPath] = ''
|
||||
pendingNewFilePath = undefined
|
||||
} else {
|
||||
// Renaming existing folder
|
||||
const oldFolderPath = oldPath
|
||||
const newFolderPath = newPath
|
||||
|
||||
@@ -138,107 +207,99 @@
|
||||
nfiles[newPath] = nfiles[old]
|
||||
delete nfiles[old]
|
||||
})
|
||||
|
||||
selectedDocument = newFolderPath
|
||||
} else {
|
||||
// For files, simple rename
|
||||
nfiles[newPath] = nfiles[oldPath]
|
||||
delete nfiles[oldPath]
|
||||
selectedDocument = newPath
|
||||
}
|
||||
|
||||
files = nfiles
|
||||
pathToRename = undefined
|
||||
selectedDocument = newPath
|
||||
} else {
|
||||
if (isPendingNew) {
|
||||
// Creating a new file - just add it with the final name
|
||||
nfiles[newPath] = ''
|
||||
pendingNewFilePath = undefined
|
||||
} else {
|
||||
// Renaming existing file
|
||||
nfiles[newPath] = nfiles[oldPath]
|
||||
delete nfiles[oldPath]
|
||||
}
|
||||
selectedDocument = newPath
|
||||
}
|
||||
|
||||
files = nfiles
|
||||
pathToEdit = undefined
|
||||
|
||||
// Select the new file in the editor (only for files, not folders)
|
||||
if (!isFolder) {
|
||||
onSelectFile?.(newPath)
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddFolder(folderPath: string) {
|
||||
console.log('Add folder to:', folderPath)
|
||||
if (files) {
|
||||
const nfiles = { ...files }
|
||||
// Ensure folderPath ends with /
|
||||
const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/'
|
||||
const newPath = normalizedFolder + 'newfolder/'
|
||||
nfiles[newPath] = ''
|
||||
files = nfiles
|
||||
pathToRename = newPath
|
||||
pathToExpand = normalizedFolder
|
||||
}
|
||||
// Ensure folderPath ends with /
|
||||
const normalizedFolder = folderPath.endsWith('/') ? folderPath : folderPath + '/'
|
||||
const basePath = normalizedFolder + 'newfolder/'
|
||||
const newPath = getUniquePath(basePath)
|
||||
// Don't update files yet - just mark as pending and enter edit mode
|
||||
pendingNewFilePath = newPath
|
||||
pathToEdit = newPath
|
||||
}
|
||||
|
||||
function handleAddRootFile() {
|
||||
console.log('Add file to root or selected folder')
|
||||
if (files) {
|
||||
const nfiles = { ...files }
|
||||
let newPath: string
|
||||
let targetFolder: string | undefined
|
||||
let basePath: string
|
||||
|
||||
if (selectedDocument) {
|
||||
// If a folder is selected, add the file inside it
|
||||
if (selectedDocument.endsWith('/')) {
|
||||
newPath = selectedDocument + 'newfile.txt'
|
||||
targetFolder = selectedDocument
|
||||
} else {
|
||||
// If a file is selected, add the new file in the same folder
|
||||
const pathParts = selectedDocument.split('/').filter(Boolean)
|
||||
if (pathParts.length > 1) {
|
||||
// File is in a subfolder
|
||||
const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/'
|
||||
newPath = parentPath + 'newfile.txt'
|
||||
targetFolder = parentPath
|
||||
} else {
|
||||
// File is at root
|
||||
newPath = '/newfile.txt'
|
||||
}
|
||||
}
|
||||
if (selectedDocument) {
|
||||
// If a folder is selected, add the file inside it
|
||||
if (selectedDocument.endsWith('/')) {
|
||||
basePath = selectedDocument + 'newfile.txt'
|
||||
} else {
|
||||
newPath = '/newfile.txt'
|
||||
}
|
||||
|
||||
nfiles[newPath] = ''
|
||||
files = nfiles
|
||||
pathToRename = newPath
|
||||
if (targetFolder) {
|
||||
pathToExpand = targetFolder
|
||||
// If a file is selected, add the new file in the same folder
|
||||
const pathParts = selectedDocument.split('/').filter(Boolean)
|
||||
if (pathParts.length > 1) {
|
||||
// File is in a subfolder
|
||||
const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/'
|
||||
basePath = parentPath + 'newfile.txt'
|
||||
} else {
|
||||
// File is at root
|
||||
basePath = '/newfile.txt'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
basePath = '/newfile.txt'
|
||||
}
|
||||
|
||||
const newPath = getUniquePath(basePath)
|
||||
// Don't update files yet - just mark as pending and enter edit mode
|
||||
pendingNewFilePath = newPath
|
||||
pathToEdit = newPath
|
||||
}
|
||||
|
||||
function handleAddRootFolder() {
|
||||
if (files) {
|
||||
const nfiles = { ...files }
|
||||
let newPath: string
|
||||
let targetFolder: string | undefined
|
||||
let basePath: string
|
||||
|
||||
if (selectedDocument) {
|
||||
// If a folder is selected, add the folder inside it
|
||||
if (selectedDocument.endsWith('/')) {
|
||||
newPath = selectedDocument + 'newfolder/'
|
||||
targetFolder = selectedDocument
|
||||
} else {
|
||||
// If a file is selected, add the new folder in the same folder
|
||||
const pathParts = selectedDocument.split('/').filter(Boolean)
|
||||
if (pathParts.length > 1) {
|
||||
// File is in a subfolder
|
||||
const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/'
|
||||
newPath = parentPath + 'newfolder/'
|
||||
targetFolder = parentPath
|
||||
} else {
|
||||
// File is at root
|
||||
newPath = '/newfolder/'
|
||||
}
|
||||
}
|
||||
if (selectedDocument) {
|
||||
// If a folder is selected, add the folder inside it
|
||||
if (selectedDocument.endsWith('/')) {
|
||||
basePath = selectedDocument + 'newfolder/'
|
||||
} else {
|
||||
newPath = '/newfolder/'
|
||||
}
|
||||
|
||||
nfiles[newPath] = ''
|
||||
files = nfiles
|
||||
pathToRename = newPath
|
||||
if (targetFolder) {
|
||||
pathToExpand = targetFolder
|
||||
// If a file is selected, add the new folder in the same folder
|
||||
const pathParts = selectedDocument.split('/').filter(Boolean)
|
||||
if (pathParts.length > 1) {
|
||||
// File is in a subfolder
|
||||
const parentPath = '/' + pathParts.slice(0, -1).join('/') + '/'
|
||||
basePath = parentPath + 'newfolder/'
|
||||
} else {
|
||||
// File is at root
|
||||
basePath = '/newfolder/'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
basePath = '/newfolder/'
|
||||
}
|
||||
|
||||
const newPath = getUniquePath(basePath)
|
||||
// Don't update files yet - just mark as pending and enter edit mode
|
||||
pendingNewFilePath = newPath
|
||||
pathToEdit = newPath
|
||||
}
|
||||
|
||||
function handleDelete(path: string) {
|
||||
@@ -272,26 +333,30 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<PanelSection size="lg" fullHeight={false} title="frontend" id="app-editor-frontend-panel">
|
||||
<PanelSection size="sm" fullHeight={false} title="frontend" id="app-editor-frontend-panel">
|
||||
{#snippet action()}
|
||||
<div class="flex gap-1">
|
||||
<div class="flex gap-0.5">
|
||||
<button
|
||||
onclick={handleAddRootFile}
|
||||
class="p-0.5 hover:bg-surface-hover rounded transition-colors flex items-center gap-0.5"
|
||||
<div class="flex gap-1">
|
||||
<Button
|
||||
onClick={handleAddRootFile}
|
||||
title="Add file to root"
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
btnClasses="px-1 gap-0.5"
|
||||
>
|
||||
<Plus size={12} class="text-secondary" />
|
||||
<File size={12} class="text-tertiary" />
|
||||
</button>
|
||||
<button
|
||||
onclick={handleAddRootFolder}
|
||||
class="p-0.5 hover:bg-surface-hover rounded transition-colors flex items-center gap-0.5"
|
||||
<Plus size={12} />
|
||||
<File size={12} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleAddRootFolder}
|
||||
title="Add folder to root"
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
btnClasses="px-1 gap-0.5"
|
||||
>
|
||||
<Plus size={12} class="text-secondary" />
|
||||
<Folder size={12} class="text-tertiary" />
|
||||
</button>
|
||||
<Plus size={12} />
|
||||
<Folder size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -305,8 +370,12 @@
|
||||
onRename={handleRename}
|
||||
onDelete={handleDelete}
|
||||
selectedPath={selectedDocument}
|
||||
{pathToRename}
|
||||
{pathToExpand}
|
||||
{pathToEdit}
|
||||
onRequestEdit={(path) => (pathToEdit = path)}
|
||||
onCancelEdit={() => {
|
||||
pathToEdit = undefined
|
||||
pendingNewFilePath = undefined
|
||||
}}
|
||||
/>
|
||||
{/each}
|
||||
<FileTreeNode
|
||||
@@ -327,7 +396,13 @@
|
||||
<RawAppModules {modules} />
|
||||
|
||||
<div class="py-4"></div>
|
||||
<RawAppInlineScriptPanelList bind:selectedRunnable {runnables} />
|
||||
<RawAppInlineScriptPanelList
|
||||
bind:selectedRunnable
|
||||
{runnables}
|
||||
onSelect={() => {
|
||||
selectedDocument = undefined
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="py-4"></div>
|
||||
<RawAppDataTableList
|
||||
@@ -348,17 +423,20 @@
|
||||
|
||||
{#if historyManager && onHistorySelect && onManualSnapshot}
|
||||
<div class="py-4"></div>
|
||||
<PanelSection fullHeight={false} size="md" title="history" id="app-editor-history-panel">
|
||||
<PanelSection fullHeight={false} size="sm" title="history" id="app-editor-history-panel">
|
||||
{#snippet action()}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-2xs text-tertiary">{historyManager.allEntries.length}/50</span>
|
||||
<Button
|
||||
size="xs2"
|
||||
color="dark"
|
||||
variant="border"
|
||||
startIcon={{ icon: Camera }}
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
on:click={onManualSnapshot}
|
||||
></Button>
|
||||
btnClasses="px-1 gap-0.5"
|
||||
title="Create a new snapshot"
|
||||
>
|
||||
<Plus size={12} />
|
||||
<Camera size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
{/snippet}
|
||||
<RawAppHistoryList
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import { Badge, Button } from '$lib/components/common'
|
||||
import { Ellipsis } from 'lucide-svelte'
|
||||
import DropdownV2 from '../DropdownV2.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import type { Runnable } from '../apps/inputType'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
runnable: Runnable
|
||||
isSelected: boolean
|
||||
onSelect: () => void
|
||||
onDelete: () => void
|
||||
}
|
||||
|
||||
let { id, runnable, isSelected, onSelect, onDelete }: Props = $props()
|
||||
|
||||
let dropdownOpen = $state(false)
|
||||
</script>
|
||||
|
||||
<button
|
||||
{id}
|
||||
class="relative w-full gap-1 flex items-center h-6 rounded-md px-1 group
|
||||
{isSelected ? 'bg-surface-accent-selected text-accent' : 'hover:bg-surface-hover'}"
|
||||
onclick={onSelect}
|
||||
>
|
||||
<Badge color="indigo" class={isSelected ? 'bg-surface-tertiary' : ''}>{id}</Badge>
|
||||
<span class="text-xs truncate font-normal pr-6">{runnable?.name}</span>
|
||||
<DropdownV2
|
||||
items={[
|
||||
{
|
||||
displayName: 'Delete',
|
||||
action: onDelete,
|
||||
type: 'delete'
|
||||
}
|
||||
]}
|
||||
class={twMerge(
|
||||
'absolute -translate-y-1/2 top-1/2 right-1 hidden group-hover:block',
|
||||
dropdownOpen ? 'block' : ''
|
||||
)}
|
||||
bind:open={dropdownOpen}
|
||||
>
|
||||
{#snippet buttonReplacement()}
|
||||
<Button
|
||||
iconOnly
|
||||
unifiedSize="xs"
|
||||
variant="subtle"
|
||||
startIcon={{ icon: Ellipsis }}
|
||||
nonCaptureEvent
|
||||
/>
|
||||
{/snippet}
|
||||
</DropdownV2>
|
||||
</button>
|
||||
@@ -57,6 +57,10 @@
|
||||
inputEl?.focus()
|
||||
}
|
||||
|
||||
export function select() {
|
||||
inputEl?.select()
|
||||
}
|
||||
|
||||
let inputEl: HTMLInputElement | HTMLTextAreaElement | undefined = $state()
|
||||
|
||||
let {
|
||||
|
||||
@@ -24,15 +24,7 @@
|
||||
import Select from '$lib/components/select/Select.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Button from '$lib/components/common/button/Button.svelte'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Sparkles,
|
||||
ArrowRight,
|
||||
Plus,
|
||||
List,
|
||||
Ban,
|
||||
ExternalLinkIcon
|
||||
} from 'lucide-svelte'
|
||||
import { Sparkles, Plus, List, Ban, ExternalLinkIcon } from 'lucide-svelte'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import RawAppDataTableList from '$lib/components/raw_apps/RawAppDataTableList.svelte'
|
||||
@@ -41,6 +33,7 @@
|
||||
import { copilotInfo } from '$lib/aiStore'
|
||||
import { aiChatManager, AIMode } from '$lib/components/copilot/chat/AIChatManager.svelte'
|
||||
import TextInput from '$lib/components/text_input/TextInput.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
|
||||
let nodraft = $page.url.searchParams.get('nodraft')
|
||||
const templatePath = $page.url.searchParams.get('template')
|
||||
@@ -366,7 +359,7 @@
|
||||
<div class="flex flex-col gap-6 min-w-sm">
|
||||
<!-- Summary -->
|
||||
<div>
|
||||
<h2 class="text-sm font-medium text-primary mb-2">Summary</h2>
|
||||
<h2 class="text-xs font-semibold text-emphasis mb-1">Summary</h2>
|
||||
<TextInput
|
||||
bind:value={appSummary}
|
||||
inputProps={{
|
||||
@@ -376,118 +369,117 @@
|
||||
</div>
|
||||
|
||||
<!-- Template Selection -->
|
||||
<div class="border-t pt-4">
|
||||
<h2 class="text-sm font-medium text-primary mb-2">Framework</h2>
|
||||
<div class="pt-6">
|
||||
<h2 class="text-xs font-semibold text-emphasis mb-1">Framework</h2>
|
||||
<div class="flex flex-wrap gap-3">
|
||||
{#each templates as t, i}
|
||||
<button
|
||||
onclick={() => (selectedTemplateIndex = i)}
|
||||
class="w-24 h-24 flex justify-between py-5 flex-col {selectedTemplateIndex === i
|
||||
? 'bg-surface-selected ring-2 ring-blue-500'
|
||||
? 'bg-surface-accent-selected border border-accent'
|
||||
: ''} hover:bg-surface-hover border rounded-lg transition-all"
|
||||
>
|
||||
<div class="w-full flex items-center justify-center">
|
||||
<FileEditorIcon file={'.' + t.icon} />
|
||||
<FileEditorIcon file={'.' + t.icon} size={32} />
|
||||
</div>
|
||||
<div class="center-center w-full text-sm">{t.name}</div>
|
||||
<div class="center-center w-full text-sm text-secondary">{t.name}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Configuration -->
|
||||
<div class="border-t pt-4">
|
||||
<h2 class="text-sm font-medium text-primary mb-3">Data Configuration</h2>
|
||||
<div class="pt-6">
|
||||
<h2 class="text-xs font-semibold text-emphasis mb-1">Data configuration</h2>
|
||||
|
||||
{#if hasNoDatatables}
|
||||
<div
|
||||
class="flex items-center gap-2 p-3 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800"
|
||||
>
|
||||
<AlertTriangle size={16} class="text-yellow-600 dark:text-yellow-400 shrink-0" />
|
||||
<div class="text-sm text-yellow-800 dark:text-yellow-200">
|
||||
<span class="font-medium">No datatables configured.</span>
|
||||
You can still create an app, but for data storage you won't be able to use data tables
|
||||
which are <b>HIGHLY RECOMMENDED</b>.
|
||||
<br />
|
||||
<Alert type="warning" title="No datatables configured.">
|
||||
You can still create an app, but for data storage you won't be able to use data tables
|
||||
which are <b>highly recommended</b>.
|
||||
<br />
|
||||
|
||||
{#if $userStore?.is_admin}
|
||||
Configure datatables in
|
||||
<a
|
||||
href="/workspace/settings?tab=windmill_data_tables"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1"
|
||||
>workspace settings <ExternalLinkIcon size={16} />
|
||||
</a> to enable this feature.
|
||||
{:else}
|
||||
<b>
|
||||
Ask your workspace admin to configure datatables in workspace settings to enable
|
||||
this feature.
|
||||
</b>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if $userStore?.is_admin}
|
||||
Configure datatables in
|
||||
<a
|
||||
href="/workspace_settings?tab=windmill_data_tables"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1"
|
||||
>workspace settings <ExternalLinkIcon size={16} />
|
||||
</a> to enable this feature.
|
||||
{:else}
|
||||
Ask your workspace admin to configure datatables in workspace settings to enable this
|
||||
feature.
|
||||
{/if}
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Default Datatable & Schema -->
|
||||
<div>
|
||||
<span class="text-xs text-tertiary mb-1 block">Default settings for new tables</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex gap-2 items-center">
|
||||
<Select
|
||||
transformInputSelectedText={(text) => 'datatable: ' + text}
|
||||
disablePortal
|
||||
items={datatableItems}
|
||||
bind:value={selectedDatatable}
|
||||
placeholder="Datatable"
|
||||
size="sm"
|
||||
class="w-40"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-2xs text-tertiary">Schema</span>
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs text-secondary mb-1 block">Default settings for new tables</span>
|
||||
<div class="flex flex-col gap-4 rounded-md p-4 border">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<label class="text-xs text-emphasis font-semibold" for="datatable"
|
||||
>Datatable</label
|
||||
>
|
||||
<Select
|
||||
id="datatable"
|
||||
disablePortal
|
||||
items={datatableItems}
|
||||
bind:value={selectedDatatable}
|
||||
placeholder="Datatable"
|
||||
size="sm"
|
||||
class="w-40"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-emphasis font-semibold">Schema</span>
|
||||
|
||||
<div class="flex flex-row gap-1 w-full items-center">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={schemaMode} noWFull>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="none" label="None" icon={Ban} {item} size="sm" />
|
||||
<ToggleButton value="new" label="New" icon={Plus} {item} size="sm" />
|
||||
<ToggleButton
|
||||
value="existing"
|
||||
label="Existing"
|
||||
icon={List}
|
||||
{item}
|
||||
size="sm"
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if schemaMode === 'new'}
|
||||
<TextInput
|
||||
bind:value={newSchemaName}
|
||||
inputProps={{
|
||||
placeholder: 'Schema name',
|
||||
oninput: () => (userEditedSchemaName = true)
|
||||
}}
|
||||
class="flex-1"
|
||||
error={newSchemaAlreadyExists}
|
||||
/>
|
||||
{:else if schemaMode === 'existing'}
|
||||
<div class="flex-1">
|
||||
<Select
|
||||
disablePortal
|
||||
items={schemaItems}
|
||||
bind:value={selectedSchema}
|
||||
placeholder="Schema"
|
||||
<div class="flex flex-row gap-1 w-full items-center">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={schemaMode} noWFull>
|
||||
{#snippet children({ item })}
|
||||
<ToggleButton value="none" label="None" icon={Ban} {item} size="sm" />
|
||||
<ToggleButton value="new" label="New" icon={Plus} {item} size="sm" />
|
||||
<ToggleButton
|
||||
value="existing"
|
||||
label="Existing"
|
||||
icon={List}
|
||||
{item}
|
||||
size="sm"
|
||||
/>
|
||||
{/snippet}
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
{#if schemaMode === 'new'}
|
||||
<TextInput
|
||||
bind:value={newSchemaName}
|
||||
inputProps={{
|
||||
placeholder: 'Schema name',
|
||||
oninput: () => (userEditedSchemaName = true)
|
||||
}}
|
||||
class="flex-1"
|
||||
error={newSchemaAlreadyExists}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
{:else if schemaMode === 'existing'}
|
||||
<div class="flex-1">
|
||||
<Select
|
||||
disablePortal
|
||||
items={schemaItems}
|
||||
bind:value={selectedSchema}
|
||||
placeholder="Schema"
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if newSchemaAlreadyExists}
|
||||
<span class="text-xs text-red-500"
|
||||
>Schema "{newSchemaName}" already exists</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
{#if newSchemaAlreadyExists}
|
||||
<span class="text-xs text-red-500">Schema "{newSchemaName}" already exists</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -502,7 +494,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Pre-whitelisted Tables -->
|
||||
<div class="border-t pt-3">
|
||||
<div class="pt-6">
|
||||
<RawAppDataTableList
|
||||
dataTableRefs={preWhitelistedTables}
|
||||
defaultDatatable={selectedDatatable}
|
||||
@@ -520,38 +512,30 @@
|
||||
</div>
|
||||
|
||||
<!-- AI Prompt (Optional) -->
|
||||
<div class="border-t pt-4">
|
||||
<h2 class="text-sm font-medium text-primary mb-2 flex items-center gap-2">
|
||||
<Sparkles size={16} class="text-blue-500" />
|
||||
<div class="pt-6">
|
||||
<h2 class="text-xs font-semibold text-emphasis mb-1 flex items-center gap-2">
|
||||
<Sparkles size={16} class="text-ai" />
|
||||
Start with AI
|
||||
<span class="text-xs font-normal text-tertiary">(optional)</span>
|
||||
</h2>
|
||||
|
||||
{#if !isAiEnabled}
|
||||
<div
|
||||
class="flex items-center gap-2 p-3 rounded-lg bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<AlertTriangle size={16} class="text-gray-500 shrink-0" />
|
||||
<div class="text-sm text-tertiary">
|
||||
AI is not configured for this workspace. You can still create an app manually but <b
|
||||
>using AI is highly recommended</b
|
||||
>.
|
||||
<br />
|
||||
{#if $userStore?.is_admin}
|
||||
Configure AI in
|
||||
<a
|
||||
href="/workspace/settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1"
|
||||
>workspace settings <ExternalLinkIcon size={16} />
|
||||
</a>
|
||||
to enable this feature.
|
||||
{:else}
|
||||
Ask your workspace admin to configure AI in workspace settings to enable this
|
||||
feature.
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Alert type="info" title="AI is not configured for this workspace.">
|
||||
You can still create an app manually but using AI is highly recommended.
|
||||
<br />
|
||||
{#if $userStore?.is_admin}
|
||||
Configure AI in
|
||||
<a
|
||||
href="/workspace_settings?tab=ai"
|
||||
target="_blank"
|
||||
class="inline-flex items-center gap-1 font-semibold"
|
||||
>workspace settings <ExternalLinkIcon size={16} />
|
||||
</a>
|
||||
to enable this feature.
|
||||
{:else}
|
||||
Ask your workspace admin to configure AI in workspace settings to enable this feature.
|
||||
{/if}
|
||||
</Alert>
|
||||
{:else}
|
||||
<div class="flex flex-col gap-2">
|
||||
<TextInput
|
||||
@@ -572,9 +556,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="border-t pt-4 flex justify-end gap-3">
|
||||
<div class="pt-6 flex justify-end gap-3">
|
||||
<Button
|
||||
color="light"
|
||||
variant="default"
|
||||
size="sm"
|
||||
on:click={() => startApp(false)}
|
||||
disabled={!templates[selectedTemplateIndex] || newSchemaAlreadyExists}
|
||||
@@ -583,14 +567,13 @@
|
||||
</Button>
|
||||
{#if isAiEnabled}
|
||||
<Button
|
||||
color="blue"
|
||||
size="sm"
|
||||
variant="accent"
|
||||
on:click={() => startApp(true)}
|
||||
disabled={!templates[selectedTemplateIndex] ||
|
||||
!initialPrompt.trim() ||
|
||||
newSchemaAlreadyExists}
|
||||
startIcon={{ icon: Sparkles }}
|
||||
endIcon={{ icon: ArrowRight }}
|
||||
btnClasses="bg-ai text-white"
|
||||
>
|
||||
Start with AI
|
||||
</Button>
|
||||
|
||||
@@ -37,21 +37,35 @@ const App = () => {
|
||||
};
|
||||
|
||||
export default App;
|
||||
`;
|
||||
`
|
||||
|
||||
const appSvelte = `<style>
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
const appSvelte = `<script lang="ts">
|
||||
import { backend } from "./wmill";
|
||||
|
||||
let name = "world";
|
||||
let result = "";
|
||||
|
||||
async function callScript() {
|
||||
result = await backend.a({ x: name });
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<h1>Hello {name}</h1>
|
||||
|
||||
<input type="text" bind:value={name} placeholder="Enter a name" />
|
||||
<button on:click={callScript}>Call runnable 'a'</button>
|
||||
|
||||
{#if result}
|
||||
<p>Result: {result}</p>
|
||||
{/if}
|
||||
</main>
|
||||
|
||||
<script>
|
||||
let name = 'world';
|
||||
</script>`
|
||||
<style>
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
</style>`
|
||||
|
||||
const indexSvelte = `
|
||||
import { mount } from 'svelte';
|
||||
@@ -81,13 +95,13 @@ createApp(App).mount('#root')`
|
||||
const indexCss = `.myclass {
|
||||
border: 1px solid gray;
|
||||
padding: 2px;
|
||||
}`;
|
||||
}`
|
||||
|
||||
export const react19Template = {
|
||||
'/index.tsx': reactIndex,
|
||||
'/App.tsx': appTsx,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
'/index.tsx': reactIndex,
|
||||
'/App.tsx': appTsx,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
"dependencies": {
|
||||
"react": "19.0.0",
|
||||
"react-dom": "19.0.0",
|
||||
@@ -97,14 +111,14 @@ export const react19Template = {
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
}`,
|
||||
}`
|
||||
}
|
||||
|
||||
export const react18Template = {
|
||||
'/index.tsx': reactIndex,
|
||||
'/App.tsx': appTsx,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
'/index.tsx': reactIndex,
|
||||
'/App.tsx': appTsx,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
"dependencies": {
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
@@ -113,31 +127,31 @@ export const react18Template = {
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@types/react": "^19.0.0"
|
||||
}
|
||||
}`,
|
||||
}`
|
||||
}
|
||||
|
||||
export const svelte5Template = {
|
||||
'/index.ts': indexSvelte,
|
||||
'/App.svelte': appSvelte,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
'/index.ts': indexSvelte,
|
||||
'/App.svelte': appSvelte,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
"dependencies": {
|
||||
"svelte": "5.45.2",
|
||||
"windmill-client": "^1"
|
||||
}
|
||||
}`,
|
||||
}`
|
||||
}
|
||||
|
||||
export const vueTemplate = {
|
||||
'/index.ts': indexVue,
|
||||
'/App.vue': appVue,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
'/index.ts': indexVue,
|
||||
'/App.vue': appVue,
|
||||
'/index.css': indexCss,
|
||||
'/package.json': `{
|
||||
"dependencies": {
|
||||
"core-js": "3.26.1",
|
||||
"vue": "3.5.13"
|
||||
}
|
||||
}`,
|
||||
}`
|
||||
}
|
||||
|
||||
export const appVueRouter = `
|
||||
|
||||
Reference in New Issue
Block a user