mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-10 00:05:27 +00:00
feat(frontend): DB Studio improvements (#3389)
* fix(frontend): wip * fix(frontend): validate column def * fix(frontend): add column definition loading sate * fix(frontend): only reload columns if static * fix(frontend): improve validation * fix(frontend): improve reactivity * fix(frontend): fix colum defs sync * fix(frontend): fix colum defs sync + insert * fix(frontend): fix insert * fix(frontend): fix * fix(frontend): fix insert * fix(frontend): fix insert * fix(frontend): fix hideSearch + hideInsert * fix(frontend): fix early return * fix(frontend): fix delete + policy * fix(frontend): fix delete * fix(frontend): restrict resource + table to static only
This commit is contained in:
@@ -57,6 +57,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
if (lastTable && !table) {
|
||||
lastTable = undefined
|
||||
}
|
||||
|
||||
const gridItem = findGridItem($app, id)
|
||||
|
||||
if (!gridItem) {
|
||||
@@ -64,7 +68,7 @@
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
gridItem.data.configuration.columnDefs = { value: [], type: 'static' }
|
||||
gridItem.data.configuration.columnDefs = { value: [], type: 'static', loading: false }
|
||||
|
||||
$app = {
|
||||
...$app
|
||||
@@ -319,6 +323,32 @@
|
||||
let lastTable: string | undefined = undefined
|
||||
let timeout: NodeJS.Timeout | undefined = undefined
|
||||
|
||||
function isSubset(subset: Record<string, any>, superset: Record<string, any>) {
|
||||
return Object.keys(subset).every((key) => {
|
||||
return superset[key] === subset[key]
|
||||
})
|
||||
}
|
||||
|
||||
function shouldReturnEarly(subset: Record<string, any>, superset: Record<string, any>): boolean {
|
||||
const subsetKeys = Object.keys(subset)
|
||||
const supersetKeys = Object.keys(superset)
|
||||
|
||||
if (supersetKeys.length === 0) return false
|
||||
|
||||
if (subsetKeys.length !== supersetKeys.length) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
JSON.stringify(supersetKeys.sort()) === JSON.stringify(subsetKeys.sort()) &&
|
||||
!subsetKeys.every((key) => isSubset(subset[key], superset[key]))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
async function listColumnsIfAvailable() {
|
||||
const selected = resolvedConfig.type.selected
|
||||
let table = resolvedConfig.type.configuration?.[resolvedConfig.type.selected]?.table
|
||||
@@ -327,6 +357,18 @@
|
||||
|
||||
lastTable = table
|
||||
|
||||
const gridItem = findGridItem($app, id)
|
||||
if (!gridItem) return
|
||||
|
||||
let columnDefs = gridItem.data.configuration.columnDefs as StaticInput<TableMetadata>
|
||||
|
||||
if (columnDefs.type !== 'static') return
|
||||
|
||||
//@ts-ignore
|
||||
gridItem.data.configuration.columnDefs.loading = true
|
||||
gridItem.data = gridItem.data
|
||||
$app = $app
|
||||
|
||||
let tableMetadata = await loadTableMetaData(
|
||||
resolvedConfig.type.configuration[selected].resource,
|
||||
$workspaceStore,
|
||||
@@ -336,23 +378,21 @@
|
||||
|
||||
if (!tableMetadata) return
|
||||
|
||||
const gridItem = findGridItem($app, id)
|
||||
if (!gridItem) return
|
||||
|
||||
let columnDefs = gridItem.data.configuration.columnDefs as StaticInput<TableMetadata>
|
||||
|
||||
let old: TableMetadata = (columnDefs?.value as TableMetadata) ?? []
|
||||
if (!Array.isArray(old)) {
|
||||
console.log('old is not an array RESET')
|
||||
old = []
|
||||
}
|
||||
// console.log('OLD', old)
|
||||
// console.log(tableMetadata)
|
||||
|
||||
const oldMap = Object.fromEntries(old.filter((x) => x != undefined).map((x) => [x.field, x]))
|
||||
const newMap = Object.fromEntries(tableMetadata?.map((x) => [x.field, x]) ?? [])
|
||||
|
||||
// if they are the same, do nothing
|
||||
if (JSON.stringify(oldMap) === JSON.stringify(newMap)) {
|
||||
if (shouldReturnEarly(newMap, oldMap)) {
|
||||
//@ts-ignore
|
||||
gridItem.data.configuration.columnDefs.loading = false
|
||||
gridItem.data = gridItem.data
|
||||
|
||||
$app = $app
|
||||
return
|
||||
}
|
||||
|
||||
@@ -375,7 +415,21 @@
|
||||
ncols = ncols.map((x) => {
|
||||
let o = {}
|
||||
Object.keys(x).forEach((k) => {
|
||||
o[k.toLowerCase()] = x[k]
|
||||
if (
|
||||
[
|
||||
'field',
|
||||
'datatype',
|
||||
'defaultvalue',
|
||||
'isprimarykey',
|
||||
'isidentity',
|
||||
'isnullable',
|
||||
'isenum'
|
||||
].includes(k.toLocaleLowerCase())
|
||||
) {
|
||||
o[k.toLowerCase()] = x[k]
|
||||
} else {
|
||||
o[k] = x[k]
|
||||
}
|
||||
})
|
||||
return o
|
||||
})
|
||||
@@ -383,7 +437,7 @@
|
||||
state = undefined
|
||||
|
||||
//@ts-ignore
|
||||
gridItem.data.configuration.columnDefs = { value: ncols, type: 'static' }
|
||||
gridItem.data.configuration.columnDefs = { value: ncols, type: 'static', loading: false }
|
||||
gridItem.data = gridItem.data
|
||||
|
||||
$app = $app
|
||||
@@ -415,7 +469,8 @@
|
||||
id: 'dbexplorer-count-' + id,
|
||||
next: (value) => {
|
||||
if (value?.error) {
|
||||
sendUserToast(value.error, true)
|
||||
const message = value?.error?.message ?? value?.error
|
||||
sendUserToast(message, true)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -445,6 +500,7 @@
|
||||
async function insert() {
|
||||
try {
|
||||
const selected = resolvedConfig.type.selected
|
||||
|
||||
await insertRowRunnable?.insertRow(
|
||||
resolvedConfig.type.configuration[selected].resource,
|
||||
$workspaceStore,
|
||||
@@ -471,21 +527,22 @@
|
||||
function onDelete(e) {
|
||||
const data = { ...e.detail }
|
||||
delete data['__index']
|
||||
let primaryColumns = getPrimaryKeys(resolvedConfig.columnDefs)
|
||||
let getPrimaryKeysresolvedConfig = resolvedConfig.columnDefs?.filter((x) =>
|
||||
primaryColumns.includes(x.field)
|
||||
)
|
||||
|
||||
const selected = resolvedConfig.type.selected
|
||||
|
||||
deleteRow?.triggerDelete(
|
||||
resolvedConfig.type.configuration[selected].resource,
|
||||
resolvedConfig.type.configuration[selected].table ?? 'unknown',
|
||||
getPrimaryKeysresolvedConfig,
|
||||
resolvedConfig.columnDefs,
|
||||
data,
|
||||
selected
|
||||
)
|
||||
}
|
||||
|
||||
let refreshCount = 0
|
||||
|
||||
$: hideSearch = resolvedConfig.hideSearch as boolean
|
||||
$: hideInsert = resolvedConfig.hideInsert as boolean
|
||||
</script>
|
||||
|
||||
{#each Object.keys(components['dbexplorercomponent'].initialData.configuration) as key (key)}
|
||||
@@ -543,25 +600,31 @@
|
||||
{outputs}
|
||||
>
|
||||
<div class="h-full" bind:clientHeight={componentContainerHeight}>
|
||||
<div class="flex p-2 justify-between gap-4" bind:clientHeight={buttonContainerHeight}>
|
||||
<DebouncedInput
|
||||
class="w-full max-w-[300px]"
|
||||
type="text"
|
||||
bind:value={quicksearch}
|
||||
placeholder="Quicksearch"
|
||||
/>
|
||||
<Button
|
||||
startIcon={{ icon: Plus }}
|
||||
color="dark"
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
args = {}
|
||||
insertDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
Insert
|
||||
</Button>
|
||||
</div>
|
||||
{#if !(hideSearch === true && hideInsert === true)}
|
||||
<div class="flex p-2 justify-between gap-4" bind:clientHeight={buttonContainerHeight}>
|
||||
{#if hideSearch !== true}
|
||||
<DebouncedInput
|
||||
class="w-full max-w-[300px]"
|
||||
type="text"
|
||||
bind:value={quicksearch}
|
||||
placeholder="Quicksearch"
|
||||
/>
|
||||
{/if}
|
||||
{#if hideInsert !== true}
|
||||
<Button
|
||||
startIcon={{ icon: Plus }}
|
||||
color="dark"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
args = {}
|
||||
insertDrawer?.openDrawer()
|
||||
}}
|
||||
>
|
||||
Insert
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.resource && resolvedConfig.type.configuration?.[resolvedConfig?.type?.selected]?.table}
|
||||
<!-- {JSON.stringify(lastInput)} -->
|
||||
<!-- <span class="text-xs">{JSON.stringify(configuration.columnDefs)}</span> -->
|
||||
@@ -576,7 +639,7 @@
|
||||
{customCss}
|
||||
{outputs}
|
||||
allowDelete={resolvedConfig.allowDelete ?? false}
|
||||
containerHeight={componentContainerHeight - buttonContainerHeight}
|
||||
containerHeight={componentContainerHeight - (buttonContainerHeight ?? 0)}
|
||||
on:update={onUpdate}
|
||||
on:delete={onDelete}
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type RunnableComponent from '../../helpers/RunnableComponent.svelte'
|
||||
import RunnableWrapper from '../../helpers/RunnableWrapper.svelte'
|
||||
import { initOutput } from '../../../editor/appUtils'
|
||||
import { type ColumnDef, type DbType } from './utils'
|
||||
import { getPrimaryKeys, type ColumnDef, type DbType } from './utils'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { getDeleteInput } from './queries/delete'
|
||||
|
||||
@@ -29,11 +29,12 @@
|
||||
export async function triggerDelete(
|
||||
resource: string,
|
||||
table: string,
|
||||
columns: ColumnDef[],
|
||||
allColumns: ColumnDef[],
|
||||
data: Record<string, any>,
|
||||
dbType: DbType
|
||||
) {
|
||||
// const datatype = tableMetaData?.find((column) => column.isprimarykey)?.datatype
|
||||
let primaryColumns = getPrimaryKeys(allColumns)
|
||||
let columns = allColumns?.filter((x) => primaryColumns.includes(x.field))
|
||||
|
||||
input = getDeleteInput(resource, table, columns, dbType)
|
||||
|
||||
|
||||
@@ -100,7 +100,6 @@
|
||||
const args = await parseSQLArgs(insertCode, dbType)
|
||||
|
||||
fields.forEach((field) => {
|
||||
console.log(field)
|
||||
const schemaProperty: SchemaProperty = {
|
||||
type: 'string'
|
||||
}
|
||||
@@ -111,7 +110,7 @@
|
||||
}
|
||||
|
||||
if (field.defaultValue) {
|
||||
if (schemaProperty.type === 'number') {
|
||||
if (schemaProperty.type === 'number' || schemaProperty.type === 'integer') {
|
||||
schemaProperty.default = field.defaultValue ? Number(field.defaultValue) : undefined
|
||||
} else if (schemaProperty.type === 'boolean') {
|
||||
schemaProperty.default = field.defaultValue?.toLocaleLowerCase() === 'true'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AppInput, RunnableByName } from '$lib/components/apps/inputType'
|
||||
import { getLanguageByResourceType, type ColumnDef, buildParameters, type DbType } from '../utils'
|
||||
|
||||
function updateWithAllValues(table: string, columns: ColumnDef[], dbType: DbType) {
|
||||
function deleteWithAllValues(table: string, columns: ColumnDef[], dbType: DbType) {
|
||||
let query = buildParameters(columns, dbType)
|
||||
|
||||
switch (dbType) {
|
||||
@@ -48,11 +48,11 @@ export function getDeleteInput(
|
||||
return undefined
|
||||
}
|
||||
|
||||
const updateRunnable: RunnableByName = {
|
||||
const deleteRunnable: RunnableByName = {
|
||||
name: 'AppDbExplorer',
|
||||
type: 'runnableByName',
|
||||
inlineScript: {
|
||||
content: updateWithAllValues(table, columns, dbType),
|
||||
content: deleteWithAllValues(table, columns, dbType),
|
||||
language: getLanguageByResourceType(dbType),
|
||||
schema: {
|
||||
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
||||
@@ -63,8 +63,8 @@ export function getDeleteInput(
|
||||
}
|
||||
}
|
||||
|
||||
const updateQuery: AppInput = {
|
||||
runnable: updateRunnable,
|
||||
const deleteQuery: AppInput = {
|
||||
runnable: deleteRunnable,
|
||||
fields: {
|
||||
database: {
|
||||
type: 'static',
|
||||
@@ -77,5 +77,5 @@ export function getDeleteInput(
|
||||
fieldType: 'object'
|
||||
}
|
||||
|
||||
return updateQuery
|
||||
return deleteQuery
|
||||
}
|
||||
|
||||
@@ -1,13 +1,27 @@
|
||||
import type { AppInput } from '$lib/components/apps/inputType'
|
||||
import { buildParameters, type DbType } from '../utils'
|
||||
import { buildParameters, ColumnIdentity, type DbType } from '../utils'
|
||||
import { getLanguageByResourceType, type ColumnDef } from '../utils'
|
||||
|
||||
function mapDataTypeToDbType(dataType: string): string {
|
||||
if (dataType === 'integer') {
|
||||
return 'int'
|
||||
}
|
||||
|
||||
if (dataType === 'boolean') {
|
||||
return 'bool'
|
||||
}
|
||||
|
||||
return dataType
|
||||
}
|
||||
|
||||
function formatInsertValues(columns: ColumnDef[], dbType: DbType, startIndex: number = 1): string {
|
||||
switch (dbType) {
|
||||
case 'mysql':
|
||||
return columns.map((c) => `:${c.field}`).join(', ')
|
||||
case 'postgresql':
|
||||
return columns.map((c, i) => `$${startIndex + i}::${c.datatype}`).join(', ')
|
||||
return columns
|
||||
.map((c, i) => `$${startIndex + i}::${mapDataTypeToDbType(c.datatype)}`)
|
||||
.join(', ')
|
||||
case 'ms_sql_server':
|
||||
return columns.map((c, i) => `@p${startIndex + i}`).join(', ')
|
||||
case 'snowflake':
|
||||
@@ -23,36 +37,72 @@ function formatColumnNames(columns: ColumnDef[]): string {
|
||||
return columns.map((c) => c.field).join(', ')
|
||||
}
|
||||
|
||||
function getUserDefaultValue(column: ColumnDef) {
|
||||
if (column.defaultValueNull) {
|
||||
return 'NULL'
|
||||
} else if (column.defaultUserValue) {
|
||||
return typeof column.defaultUserValue === 'string'
|
||||
? `'${column.defaultUserValue}'`
|
||||
: column.defaultUserValue
|
||||
}
|
||||
}
|
||||
|
||||
function formatDefaultValues(columns: ColumnDef[]): string {
|
||||
const defaultValues = columns
|
||||
.map((c) => {
|
||||
if (c.defaultValueNull) {
|
||||
return 'NULL'
|
||||
} else {
|
||||
return typeof c.defaultUserValue === 'string'
|
||||
? `'${c.defaultUserValue}'`
|
||||
: c.defaultUserValue
|
||||
const userDefaultValue = getUserDefaultValue(c)
|
||||
if (c.overrideDefaultValue === true) {
|
||||
return userDefaultValue
|
||||
}
|
||||
|
||||
return userDefaultValue ?? c.defaultvalue
|
||||
})
|
||||
.join(', ')
|
||||
|
||||
return defaultValues
|
||||
}
|
||||
|
||||
function shouldOmitColumnInInsert(column: ColumnDef) {
|
||||
if (!column.hideInsert || column.isidentity === ColumnIdentity.Always) {
|
||||
return true
|
||||
}
|
||||
|
||||
const userDefaultValue =
|
||||
(column.defaultUserValue !== undefined && column.defaultUserValue !== '') ||
|
||||
column.defaultValueNull === true
|
||||
const dbDefaultValue = Boolean(column.defaultvalue)
|
||||
|
||||
if (column.isnullable === 'NO') {
|
||||
if (!userDefaultValue && !dbDefaultValue && column.isidentity === ColumnIdentity.No) {
|
||||
throw new Error(`Column ${column.field} is not nullable and has no default value`)
|
||||
}
|
||||
|
||||
if (!userDefaultValue && !dbDefaultValue) {
|
||||
// Should be omitted if it's an identity column and we have no default value
|
||||
return column.isidentity !== ColumnIdentity.No
|
||||
}
|
||||
|
||||
// Should be omitted if the user had not provided a default value and the database has a default value
|
||||
return !userDefaultValue && dbDefaultValue
|
||||
} else if (column.isnullable === 'YES') {
|
||||
return !userDefaultValue
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function makeInsertQuery(table: string, columns: ColumnDef[], dbType: DbType) {
|
||||
if (!table) throw new Error('Table name is required')
|
||||
|
||||
const columnsInsert = columns.filter((x) => !x.hideInsert)
|
||||
const columnsDefault = columns.filter(
|
||||
(x) => x.hideInsert && (x.overrideDefaultValue || x.defaultvalue === null)
|
||||
)
|
||||
|
||||
const columnsDefault = columns.filter((c) => !shouldOmitColumnInInsert(c))
|
||||
const allInsertColumns = columnsInsert.concat(columnsDefault)
|
||||
|
||||
let query = buildParameters(columnsInsert, dbType)
|
||||
|
||||
query += '\n'
|
||||
|
||||
const shouldInsertComma = columnsDefault.length > 0 && columnsInsert.length > 0
|
||||
const shouldInsertComma = columnsDefault.length > 0
|
||||
const columnNames = formatColumnNames(allInsertColumns)
|
||||
const insertValues = formatInsertValues(columnsInsert, dbType)
|
||||
const defaultValues = formatDefaultValues(columnsDefault)
|
||||
|
||||
+26
-1
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { GridApi, createGrid, type IDatasource } from 'ag-grid-community'
|
||||
import { isObject } from '$lib/utils'
|
||||
import { isObject, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { AppViewerContext, ComponentCustomCSS } from '../../../types'
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
import { Button } from '$lib/components/common'
|
||||
import { cellRendererFactory } from './utils'
|
||||
import { Trash2 } from 'lucide-svelte'
|
||||
import type { ColumnDef } from '../dbtable/utils'
|
||||
// import 'ag-grid-community/dist/styles/ag-theme-alpine-dark.css'
|
||||
|
||||
export let id: string
|
||||
@@ -125,6 +126,13 @@
|
||||
$: eGui && mountGrid()
|
||||
|
||||
function transformColumnDefs(columnDefs: any[]) {
|
||||
const { isValid, errors } = validateColumnDefs(columnDefs)
|
||||
|
||||
if (!isValid) {
|
||||
sendUserToast(`Invalid columnDefs: ${errors.join('\n')}`, true)
|
||||
return []
|
||||
}
|
||||
|
||||
let r = columnDefs?.filter((x) => x && !x.ignored) ?? []
|
||||
if (allowDelete) {
|
||||
r.push({
|
||||
@@ -161,6 +169,23 @@
|
||||
|
||||
let firstRow = 0
|
||||
let lastRow = 0
|
||||
|
||||
function validateColumnDefs(columnDefs: ColumnDef[]): { isValid: boolean; errors: string[] } {
|
||||
let isValid = true
|
||||
const errors: string[] = []
|
||||
|
||||
// Validate each column definition
|
||||
columnDefs.forEach((colDef, index) => {
|
||||
// Check if 'field' property exists and is a non-empty string
|
||||
if (!colDef.field || typeof colDef.field !== 'string' || colDef.field.trim() === '') {
|
||||
isValid = false
|
||||
errors.push(`Column at index ${index} is missing a valid 'field' property.`)
|
||||
}
|
||||
})
|
||||
|
||||
return { isValid, errors }
|
||||
}
|
||||
|
||||
function mountGrid() {
|
||||
if (eGui) {
|
||||
createGrid(
|
||||
|
||||
@@ -216,14 +216,15 @@
|
||||
input: getInsertInput(tableValue, columnDefs, resourceValue, dbType),
|
||||
id: x.id + '_insert'
|
||||
})
|
||||
r.push({
|
||||
input: getDeleteInput(resourceValue, tableValue, columnDefs, dbType),
|
||||
id: x.id + '_delete'
|
||||
})
|
||||
|
||||
let primaryColumns = getPrimaryKeys(columnDefs)
|
||||
let columns = columnDefs?.filter((x) => primaryColumns.includes(x.field))
|
||||
|
||||
r.push({
|
||||
input: getDeleteInput(resourceValue, tableValue, columns, dbType),
|
||||
id: x.id + '_delete'
|
||||
})
|
||||
|
||||
columnDefs
|
||||
.filter((col) => col.editable || config.allEditable.value)
|
||||
.forEach((column) => {
|
||||
|
||||
@@ -2521,7 +2521,6 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
timingFunction: {
|
||||
fieldType: 'select',
|
||||
type: 'static',
|
||||
|
||||
selectOptions: selectOptions.animationTimingFunctionOptions,
|
||||
value: 'linear',
|
||||
tooltip:
|
||||
@@ -3369,14 +3368,16 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
type: 'static',
|
||||
fieldType: 'resource',
|
||||
subFieldType: 'postgres',
|
||||
value: ''
|
||||
value: '',
|
||||
allowTypeChange: false
|
||||
} as StaticAppInput,
|
||||
table: {
|
||||
fieldType: 'select',
|
||||
subFieldType: 'db-table',
|
||||
type: 'static',
|
||||
selectOptions: [],
|
||||
value: undefined
|
||||
value: undefined,
|
||||
allowTypeChange: false
|
||||
}
|
||||
},
|
||||
mysql: {
|
||||
@@ -3445,7 +3446,8 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
type: 'static',
|
||||
fieldType: 'array',
|
||||
subFieldType: 'db-explorer',
|
||||
value: []
|
||||
value: [],
|
||||
loading: false
|
||||
} as StaticAppInput,
|
||||
whereClause: {
|
||||
type: 'static',
|
||||
@@ -3498,6 +3500,18 @@ See date-fns format for more information. By default, it is 'dd.MM.yyyy HH:mm'
|
||||
fieldType: 'object',
|
||||
value: {},
|
||||
tooltip: 'any configuration that can be passed to ag-grid top level'
|
||||
},
|
||||
hideInsert: {
|
||||
type: 'static',
|
||||
fieldType: 'boolean',
|
||||
value: false,
|
||||
tooltip: 'Hide the insert button'
|
||||
},
|
||||
hideSearch: {
|
||||
type: 'static',
|
||||
fieldType: 'boolean',
|
||||
value: false,
|
||||
tooltip: 'Hide the search bar'
|
||||
}
|
||||
},
|
||||
componentInput: undefined
|
||||
|
||||
+10
-2
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/common'
|
||||
import { GripVertical, Plus, X } from 'lucide-svelte'
|
||||
import { GripVertical, Loader2, Plus, X } from 'lucide-svelte'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import type { InputType, StaticInput, StaticOptions } from '../../inputType'
|
||||
import SubTypeEditor from './SubTypeEditor.svelte'
|
||||
@@ -10,7 +10,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import QuickAddColumn from './QuickAddColumn.svelte'
|
||||
|
||||
export let componentInput: StaticInput<any[]>
|
||||
export let componentInput: StaticInput<any[]> & { loading?: boolean }
|
||||
export let subFieldType: InputType | undefined = undefined
|
||||
export let selectOptions: StaticOptions['selectOptions'] | undefined = undefined
|
||||
export let id: string | undefined
|
||||
@@ -320,6 +320,14 @@
|
||||
{/each}
|
||||
</section>
|
||||
{/if}
|
||||
{#if subFieldType === 'db-explorer'}
|
||||
{#if componentInput.loading}
|
||||
<div class="flex flex-row gap-2 w-full items-center text-xs">
|
||||
<Loader2 class="animate-spin" size={14} />
|
||||
Loading columns defintions...
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if subFieldType !== 'db-explorer'}
|
||||
<Button size="xs" color="light" startIcon={{ icon: Plus }} on:click={() => addElementByType()}>
|
||||
Add
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
fileUpload={config?.['fileUpload']}
|
||||
loading={config?.['loading']}
|
||||
documentationLink={config?.['documentationLink']}
|
||||
allowTypeChange={config?.['allowTypeChange']}
|
||||
{showOnDemandOnlyToggle}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
Reference in New Issue
Block a user