mirror of
https://github.com/dbgate/dbgate
synced 2024-11-07 20:26:23 +00:00
sql generator - basic concept
This commit is contained in:
parent
9c27c224ec
commit
4762597741
19
packages/api/src/controllers/sqlgen.js
Normal file
19
packages/api/src/controllers/sqlgen.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const databaseConnections = require('./databaseConnections');
|
||||||
|
const connections = require('./connections');
|
||||||
|
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||||
|
const { SqlGenerator } = require('dbgate-tools')
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
preview_meta: {
|
||||||
|
method: 'post',
|
||||||
|
},
|
||||||
|
async preview({ conid, database, objects, options }) {
|
||||||
|
const structure = await databaseConnections.structure({ conid, database })
|
||||||
|
const connection = await connections.get({ conid })
|
||||||
|
const driver = requireEngineDriver(connection);
|
||||||
|
const dmp = driver.createDumper()
|
||||||
|
const generator = new SqlGenerator(structure, options, objects, dmp);
|
||||||
|
generator.dump();
|
||||||
|
return dmp.s;
|
||||||
|
},
|
||||||
|
};
|
@ -26,6 +26,7 @@ const uploads = require('./controllers/uploads');
|
|||||||
const plugins = require('./controllers/plugins');
|
const plugins = require('./controllers/plugins');
|
||||||
const files = require('./controllers/files');
|
const files = require('./controllers/files');
|
||||||
const scheduler = require('./controllers/scheduler');
|
const scheduler = require('./controllers/scheduler');
|
||||||
|
const sqlgen = require('./controllers/sqlgen');
|
||||||
|
|
||||||
const { rundir } = require('./utility/directories');
|
const { rundir } = require('./utility/directories');
|
||||||
|
|
||||||
@ -72,6 +73,7 @@ function start(argument = null) {
|
|||||||
useController(app, '/plugins', plugins);
|
useController(app, '/plugins', plugins);
|
||||||
useController(app, '/files', files);
|
useController(app, '/files', files);
|
||||||
useController(app, '/scheduler', scheduler);
|
useController(app, '/scheduler', scheduler);
|
||||||
|
useController(app, '/sqlgen', sqlgen);
|
||||||
|
|
||||||
// if (process.env.PAGES_DIRECTORY) {
|
// if (process.env.PAGES_DIRECTORY) {
|
||||||
// app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
|
// app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
|
||||||
|
62
packages/tools/src/SqlGenerator.ts
Normal file
62
packages/tools/src/SqlGenerator.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { DatabaseInfo, FunctionInfo, ProcedureInfo, TableInfo, ViewInfo } from 'dbgate-types';
|
||||||
|
import { SqlDumper } from './SqlDumper';
|
||||||
|
|
||||||
|
interface SqlGeneratorOptions {
|
||||||
|
dropTables: boolean;
|
||||||
|
checkIfTableExists: boolean;
|
||||||
|
dropReferences: boolean;
|
||||||
|
createTables: boolean;
|
||||||
|
createReferences: boolean;
|
||||||
|
createForeignKeys: boolean;
|
||||||
|
createIndexes: boolean;
|
||||||
|
insert: boolean;
|
||||||
|
skipAutoincrementColumn: boolean;
|
||||||
|
disableConstraints: boolean;
|
||||||
|
omitNulls: boolean;
|
||||||
|
truncate: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SqlGeneratorObject {
|
||||||
|
schemaName: string;
|
||||||
|
pureName: string;
|
||||||
|
objectTypeField: 'tables' | 'views' | 'procedures' | 'functions';
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SqlGenerator {
|
||||||
|
private tables: TableInfo[];
|
||||||
|
private views: ViewInfo[];
|
||||||
|
private procedures: ProcedureInfo[];
|
||||||
|
private functions: FunctionInfo[];
|
||||||
|
constructor(
|
||||||
|
public dbinfo: DatabaseInfo,
|
||||||
|
public options: SqlGeneratorOptions,
|
||||||
|
public objects: SqlGeneratorObject[],
|
||||||
|
public dmp: SqlDumper
|
||||||
|
) {
|
||||||
|
this.tables = this.extract('tables');
|
||||||
|
this.views = this.extract('views');
|
||||||
|
this.procedures = this.extract('procedures');
|
||||||
|
this.functions = this.extract('functions');
|
||||||
|
}
|
||||||
|
|
||||||
|
checkDumper() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dump() {
|
||||||
|
if (this.options.createTables) {
|
||||||
|
for (const table of this.tables) {
|
||||||
|
this.dmp.createTable(table);
|
||||||
|
if (this.checkDumper()) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extract(objectTypeField) {
|
||||||
|
return this.dbinfo[objectTypeField].filter(x =>
|
||||||
|
this.objects.find(
|
||||||
|
y => x.pureName == y.pureName && x.schemaName == y.schemaName && y.objectTypeField == objectTypeField
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,3 +8,4 @@ export * from './driverBase';
|
|||||||
export * from './SqlDumper';
|
export * from './SqlDumper';
|
||||||
export * from './testPermission';
|
export * from './testPermission';
|
||||||
export * from './splitPostgresQuery';
|
export * from './splitPostgresQuery';
|
||||||
|
export * from './SqlGenerator';
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
import FontIcon from '../icons/FontIcon.svelte';
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
import contextMenu from '../utility/contextMenu';
|
import contextMenu from '../utility/contextMenu';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let icon;
|
export let icon;
|
||||||
export let title;
|
export let title;
|
||||||
export let data;
|
export let data;
|
||||||
|
export let module;
|
||||||
|
|
||||||
export let isBold = false;
|
export let isBold = false;
|
||||||
export let isBusy = false;
|
export let isBusy = false;
|
||||||
@ -16,22 +18,54 @@
|
|||||||
export let extInfo = undefined;
|
export let extInfo = undefined;
|
||||||
export let menu = undefined;
|
export let menu = undefined;
|
||||||
export let expandIcon = undefined;
|
export let expandIcon = undefined;
|
||||||
|
export let checkedObjectsStore = null;
|
||||||
|
|
||||||
|
$: isChecked = checkedObjectsStore && $checkedObjectsStore.find(x => module.extractKey(data) == module.extractKey(x));
|
||||||
|
|
||||||
function handleExpand() {
|
function handleExpand() {
|
||||||
dispatch('expand');
|
dispatch('expand');
|
||||||
}
|
}
|
||||||
|
function handleClick() {
|
||||||
|
if (checkedObjectsStore) {
|
||||||
|
if (isChecked) {
|
||||||
|
checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y)));
|
||||||
|
} else {
|
||||||
|
checkedObjectsStore.update(x => [...x, data]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dispatch('click');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setChecked(value) {
|
||||||
|
if (!value && isChecked) {
|
||||||
|
checkedObjectsStore.update(x => x.filter(y => module.extractKey(data) != module.extractKey(y)));
|
||||||
|
}
|
||||||
|
if (value && !isChecked) {
|
||||||
|
checkedObjectsStore.update(x => [...x, data]);
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="main"
|
class="main"
|
||||||
class:isBold
|
class:isBold
|
||||||
draggable={true}
|
draggable={true}
|
||||||
on:click
|
on:click={handleClick}
|
||||||
use:contextMenu={menu}
|
use:contextMenu={menu}
|
||||||
on:dragstart={e => {
|
on:dragstart={e => {
|
||||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{#if checkedObjectsStore}
|
||||||
|
<CheckboxField
|
||||||
|
checked={!!isChecked}
|
||||||
|
on:change={e => {
|
||||||
|
// @ts-ignore
|
||||||
|
setChecked(e.target.checked);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
{#if expandIcon}
|
{#if expandIcon}
|
||||||
<span class="expand-icon" on:click|stopPropagation={handleExpand}>
|
<span class="expand-icon" on:click|stopPropagation={handleExpand}>
|
||||||
<FontIcon icon={expandIcon} />
|
<FontIcon icon={expandIcon} />
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
export let group;
|
export let group;
|
||||||
export let items;
|
export let items;
|
||||||
export let module;
|
export let module;
|
||||||
|
export let checkedObjectsStore = null;
|
||||||
|
|
||||||
let isExpanded = true;
|
let isExpanded = true;
|
||||||
|
|
||||||
@ -26,7 +27,7 @@
|
|||||||
|
|
||||||
{#if isExpanded}
|
{#if isExpanded}
|
||||||
{#each filtered as item (module.extractKey(item.data))}
|
{#each filtered as item (module.extractKey(item.data))}
|
||||||
<AppObjectListItem {...$$restProps} {module} data={item.data} on:objectClick />
|
<AppObjectListItem {...$$restProps} {module} data={item.data} {checkedObjectsStore} on:objectClick />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
export let isExpandable = undefined;
|
export let isExpandable = undefined;
|
||||||
export let filter;
|
export let filter;
|
||||||
export let expandIconFunc = undefined;
|
export let expandIconFunc = undefined;
|
||||||
|
export let checkedObjectsStore = null;
|
||||||
|
|
||||||
export let groupFunc = undefined;
|
export let groupFunc = undefined;
|
||||||
|
|
||||||
@ -36,7 +37,15 @@
|
|||||||
|
|
||||||
{#if groupFunc}
|
{#if groupFunc}
|
||||||
{#each _.keys(groups) as group (group)}
|
{#each _.keys(groups) as group (group)}
|
||||||
<AppObjectGroup {group} {module} items={groups[group]} {expandIconFunc} {isExpandable} {subItemsComponent} />
|
<AppObjectGroup
|
||||||
|
{group}
|
||||||
|
{module}
|
||||||
|
items={groups[group]}
|
||||||
|
{expandIconFunc}
|
||||||
|
{isExpandable}
|
||||||
|
{subItemsComponent}
|
||||||
|
{checkedObjectsStore}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{:else}
|
{:else}
|
||||||
{#each filtered as data (module.extractKey(data))}
|
{#each filtered as data (module.extractKey(data))}
|
||||||
@ -48,6 +57,7 @@
|
|||||||
{isExpandable}
|
{isExpandable}
|
||||||
on:objectClick
|
on:objectClick
|
||||||
{expandIconFunc}
|
{expandIconFunc}
|
||||||
|
{checkedObjectsStore}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
export let expandOnClick;
|
export let expandOnClick;
|
||||||
export let isExpandable = undefined;
|
export let isExpandable = undefined;
|
||||||
export let expandIconFunc = plusExpandIcon;
|
export let expandIconFunc = plusExpandIcon;
|
||||||
|
export let checkedObjectsStore = null;
|
||||||
|
|
||||||
let isExpanded = false;
|
let isExpanded = false;
|
||||||
|
|
||||||
@ -41,6 +42,8 @@
|
|||||||
on:click={handleExpand}
|
on:click={handleExpand}
|
||||||
on:expand={handleExpandButton}
|
on:expand={handleExpandButton}
|
||||||
expandIcon={getExpandIcon(expandable, subItemsComponent, isExpanded, expandIconFunc)}
|
expandIcon={getExpandIcon(expandable, subItemsComponent, isExpanded, expandIconFunc)}
|
||||||
|
{checkedObjectsStore}
|
||||||
|
{module}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#if isExpanded && subItemsComponent}
|
{#if isExpanded && subItemsComponent}
|
||||||
|
@ -198,11 +198,8 @@ registerCommand({
|
|||||||
testEnabled: () => getCurrentDatabase() != null,
|
testEnabled: () => getCurrentDatabase() != null,
|
||||||
onClick: () =>
|
onClick: () =>
|
||||||
showModal(SqlGeneratorModal, {
|
showModal(SqlGeneratorModal, {
|
||||||
importToArchive: true,
|
|
||||||
initialValues: {
|
|
||||||
conid: getCurrentDatabase()?.connection?._id,
|
conid: getCurrentDatabase()?.connection?._id,
|
||||||
database: getCurrentDatabase()?.name,
|
database: getCurrentDatabase()?.name,
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -162,7 +162,6 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.content {
|
.content {
|
||||||
border-top: 1px solid var(--theme-border);
|
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
<!-- Modal content -->
|
<!-- Modal content -->
|
||||||
<div class="window" class:fullScreen use:clickOutside on:clickOutside={handleCloseModal}>
|
<div class="window" class:fullScreen use:clickOutside on:clickOutside={handleCloseModal}>
|
||||||
{#if $$slots.header}
|
{#if $$slots.header}
|
||||||
<div class="header">
|
<div class="header" class:fullScreen>
|
||||||
<div><slot name="header" /></div>
|
<div><slot name="header" /></div>
|
||||||
<div class="close" on:click={handleCloseModal}>
|
<div class="close" on:click={handleCloseModal}>
|
||||||
<FontIcon icon="icon close" />
|
<FontIcon icon="icon close" />
|
||||||
@ -107,6 +107,10 @@
|
|||||||
background-color: var(--theme-bg-modalheader);
|
background-color: var(--theme-bg-modalheader);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header.fullScreen {
|
||||||
|
border-bottom: 1px solid var(--theme-border);
|
||||||
|
}
|
||||||
|
|
||||||
.content:not(.fullScreen) {
|
.content:not(.fullScreen) {
|
||||||
border-bottom: 1px solid var(--theme-border);
|
border-bottom: 1px solid var(--theme-border);
|
||||||
border-top: 1px solid var(--theme-border);
|
border-top: 1px solid var(--theme-border);
|
||||||
|
@ -1,7 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { writable } from 'svelte/store';
|
||||||
|
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||||
|
import * as databaseObjectAppObject from '../appobj/DatabaseObjectAppObject.svelte';
|
||||||
|
|
||||||
|
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||||
|
|
||||||
import LargeButton from '../elements/LargeButton.svelte';
|
import LargeButton from '../elements/LargeButton.svelte';
|
||||||
|
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||||
|
import SearchInput from '../elements/SearchInput.svelte';
|
||||||
|
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||||
|
import FormFieldTemplateTiny from '../forms/FormFieldTemplateTiny.svelte';
|
||||||
|
import FormProvider from '../forms/FormProvider.svelte';
|
||||||
|
import FormProviderCore from '../forms/FormProviderCore.svelte';
|
||||||
|
import FormValues from '../forms/FormValues.svelte';
|
||||||
|
|
||||||
import FontIcon from '../icons/FontIcon.svelte';
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import SqlEditor from '../query/SqlEditor.svelte';
|
||||||
|
import axiosInstance from '../utility/axiosInstance';
|
||||||
|
import { useDatabaseInfo } from '../utility/metadataLoaders';
|
||||||
|
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
|
||||||
|
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
|
||||||
|
import WidgetsInnerContainer from '../widgets/WidgetsInnerContainer.svelte';
|
||||||
|
|
||||||
import ModalBase from './ModalBase.svelte';
|
import ModalBase from './ModalBase.svelte';
|
||||||
import { closeCurrentModal } from './modalTools';
|
import { closeCurrentModal } from './modalTools';
|
||||||
@ -10,9 +30,38 @@
|
|||||||
export let database;
|
export let database;
|
||||||
|
|
||||||
let busy = false;
|
let busy = false;
|
||||||
|
let managerSize;
|
||||||
|
let objectsFilter = '';
|
||||||
|
let sqlPreview = '';
|
||||||
|
|
||||||
|
const checkedObjectsStore = writable([]);
|
||||||
|
const valuesStore = writable({});
|
||||||
|
|
||||||
|
$: console.log('checkedObjectsStore', $checkedObjectsStore);
|
||||||
|
|
||||||
|
$: objects = useDatabaseInfo({ conid, database });
|
||||||
|
|
||||||
|
$: generatePreview($valuesStore, $checkedObjectsStore);
|
||||||
|
|
||||||
|
$: objectList = _.flatten(
|
||||||
|
['tables', 'views', 'procedures', 'functions'].map(objectTypeField =>
|
||||||
|
_.sortBy(
|
||||||
|
(($objects || {})[objectTypeField] || []).map(obj => ({ ...obj, objectTypeField })),
|
||||||
|
['schemaName', 'pureName']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
async function generatePreview(options, objects) {
|
||||||
|
const response = await axiosInstance.post('sqlgen/preview', { conid, database, objects, options });
|
||||||
|
if (_.isString(response.data)) {
|
||||||
|
sqlPreview = response.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalBase {...$$restProps} fullScreen>
|
<FormProviderCore values={valuesStore} template={FormFieldTemplateTiny}>
|
||||||
|
<ModalBase {...$$restProps} fullScreen>
|
||||||
<svelte:fragment slot="header">
|
<svelte:fragment slot="header">
|
||||||
SQL Generator
|
SQL Generator
|
||||||
{#if busy}
|
{#if busy}
|
||||||
@ -20,9 +69,74 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
|
||||||
|
<svelte:fragment slot="1">
|
||||||
|
<WidgetColumnBar>
|
||||||
|
<WidgetColumnBarItem title="Choose objects" name="objects" height="60%">
|
||||||
|
<SearchBoxWrapper>
|
||||||
|
<SearchInput placeholder="Search tables or objects" bind:value={objectsFilter} />
|
||||||
|
</SearchBoxWrapper>
|
||||||
|
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<AppObjectList
|
||||||
|
list={objectList.map(x => ({ ...x, conid, database }))}
|
||||||
|
module={databaseObjectAppObject}
|
||||||
|
groupFunc={data => _.startCase(data.objectTypeField)}
|
||||||
|
isExpandable={data => data.objectTypeField == 'tables' || data.objectTypeField == 'views'}
|
||||||
|
filter={objectsFilter}
|
||||||
|
{checkedObjectsStore}
|
||||||
|
/>
|
||||||
|
</WidgetsInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem title="Settings" name="settings">
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<FormValues let:values>
|
||||||
|
<FormCheckboxField label="Drop tables" name="dropTables" />
|
||||||
|
{#if values.dropTables}
|
||||||
|
<div class="ml-1">
|
||||||
|
<FormCheckboxField label="Test if exists" name="checkIfTableExists" defaultValue={true} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<FormCheckboxField label="Drop references" name="dropReferences" />
|
||||||
|
|
||||||
|
<FormCheckboxField label="Create tables" name="createTables" />
|
||||||
|
<FormCheckboxField label="Create references" name="createReferences" />
|
||||||
|
<FormCheckboxField label="Create foreign keys" name="createForeignKeys" />
|
||||||
|
<FormCheckboxField label="Create indexes" name="createIndexes" />
|
||||||
|
|
||||||
|
<FormCheckboxField label="Insert" name="insert" />
|
||||||
|
{#if values.insert}
|
||||||
|
<div class="ml-1">
|
||||||
|
<FormCheckboxField label="Skip autoincrement column" name="skipAutoincrementColumn" />
|
||||||
|
<FormCheckboxField label="Disable constraints" name="disableConstraints" defaultValue={true} />
|
||||||
|
<FormCheckboxField label="Omit NULL values" name="omitNulls" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<FormCheckboxField label="Truncate tables (delete all rows)" name="truncate" />
|
||||||
|
|
||||||
|
<!-- <HashCheckBox label='Drop' hashName={`gensql.drop${objTypePascal}`} onChange={onChange} />
|
||||||
|
{
|
||||||
|
getHashValue(`gensql.drop${objTypePascal}`) == '1' &&
|
||||||
|
<HashCheckBox label='Test if exists' hashName={`gensql.checkIf${objTypePascal}Exists`} indent={1} onChange={onChange} defaultChecked />
|
||||||
|
}
|
||||||
|
<HashCheckBox label='Create' hashName={`gensql.create${objTypePascal}`} onChange={onChange} /> -->
|
||||||
|
</FormValues>
|
||||||
|
</WidgetsInnerContainer>
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
</WidgetColumnBar>
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="2">
|
||||||
|
<SqlEditor readOnly value={sqlPreview} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</HorizontalSplitter>
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<div class="flex m-2">
|
<div class="flex m-2">
|
||||||
<LargeButton on:click={closeCurrentModal} icon="icon close">Close</LargeButton>
|
<LargeButton on:click={closeCurrentModal} icon="icon close">Close</LargeButton>
|
||||||
</div>
|
</div>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ModalBase>
|
</ModalBase>
|
||||||
|
</FormProviderCore>
|
||||||
|
Loading…
Reference in New Issue
Block a user