mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-22 22:46:01 +00:00
Merge branch 'master' into feature/svelte4
This commit is contained in:
@@ -20,14 +20,14 @@
|
||||
installNewVolatileConnectionListener,
|
||||
refreshPublicCloudFiles,
|
||||
} from './utility/api';
|
||||
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
|
||||
import { getAllApps, getConfig, getSettings } from './utility/metadataLoaders';
|
||||
import AppTitleProvider from './utility/AppTitleProvider.svelte';
|
||||
import getElectron from './utility/getElectron';
|
||||
import AppStartInfo from './widgets/AppStartInfo.svelte';
|
||||
import SettingsListener from './utility/SettingsListener.svelte';
|
||||
import { handleAuthOnStartup } from './clientAuth';
|
||||
import { initializeAppUpdates } from './utility/appUpdate';
|
||||
import { _t } from './translations';
|
||||
import { _t, getCurrentTranslations, saveSelectedLanguageToCache } from './translations';
|
||||
import { installCloudListeners } from './utility/cloudListeners';
|
||||
|
||||
export let isAdminPage = false;
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
const connections = await apiCall('connections/list');
|
||||
const settings = await getSettings();
|
||||
const apps = await getUsedApps();
|
||||
const apps = await getAllApps();
|
||||
const loadedApiValue = !!(settings && connections && config && apps);
|
||||
|
||||
if (loadedApiValue) {
|
||||
@@ -61,6 +61,13 @@
|
||||
initializeAppUpdates();
|
||||
installCloudListeners();
|
||||
refreshPublicCloudFiles();
|
||||
saveSelectedLanguageToCache(config.preferrendLanguage);
|
||||
|
||||
const electron = getElectron();
|
||||
if (electron) {
|
||||
electron.send('translation-data', JSON.stringify(getCurrentTranslations()));
|
||||
global.TRANSLATION_DATA = getCurrentTranslations();
|
||||
}
|
||||
}
|
||||
|
||||
loadedApi = loadedApiValue;
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
import ErrorInfo from './elements/ErrorInfo.svelte';
|
||||
import { isOneOfPage } from './utility/pageDefs';
|
||||
import { openWebLink } from './utility/simpleTools';
|
||||
import FontIcon from './icons/FontIcon.svelte';
|
||||
|
||||
const config = useConfig();
|
||||
const values = writable({ amoid: null, databaseServer: null });
|
||||
@@ -22,17 +23,15 @@
|
||||
$: trialDaysLeft = $config?.trialDaysLeft;
|
||||
|
||||
let errorMessage = '';
|
||||
let expiredMessageSet = false;
|
||||
|
||||
$: if (isExpired && !expiredMessageSet) {
|
||||
errorMessage = 'Your license is expired';
|
||||
expiredMessageSet = true;
|
||||
}
|
||||
let isInsertingLicense = false;
|
||||
|
||||
$: trialButtonAvailable = !isExpired && trialDaysLeft == null;
|
||||
|
||||
// $: console.log('CONFIG', $config);
|
||||
|
||||
$: {
|
||||
if ($config?.isLicenseValid) {
|
||||
if ($config?.isLicenseValid && trialDaysLeft == null) {
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
}
|
||||
}
|
||||
@@ -41,83 +40,124 @@
|
||||
<FormProviderCore {values}>
|
||||
<SpecialPageLayout>
|
||||
{#if getElectron() || ($config?.storageDatabase && hasPermission('admin/license'))}
|
||||
<div class="heading">License</div>
|
||||
<FormTextAreaField label="Enter your license key" name="licenseKey" rows={5} />
|
||||
<div class="heading">Thank you for using DbGate!</div>
|
||||
|
||||
<div class="submit">
|
||||
<FormSubmit
|
||||
value="Save license"
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
const { licenseKey } = e.detail;
|
||||
const resp = await apiCall('config/save-license-key', { licenseKey, tryToRenew: true });
|
||||
if (resp?.status == 'ok') {
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = resp?.errorMessage || 'Error saving license key';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if isExpired}
|
||||
<div class="infotext"><FontIcon icon="img warn" /> Your license has expired. Please insert new license.</div>
|
||||
{:else if trialDaysLeft > 0}
|
||||
<div class="infotext">
|
||||
<FontIcon icon="img warn" /> Your trial period will expire in {trialDaysLeft} day{trialDaysLeft != 1
|
||||
? 's'
|
||||
: ''}.
|
||||
</div>
|
||||
{:else}
|
||||
<div class="infotext">
|
||||
<FontIcon icon="img info" /> Proceed by selecting a licensing option or providing your license key.
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isExpired && trialDaysLeft == null}
|
||||
{#if isInsertingLicense}
|
||||
<FormTextAreaField label="Enter your license key" name="licenseKey" rows={5} />
|
||||
|
||||
<div class="submit">
|
||||
<div class="flex flex1">
|
||||
<div class="col-6 flex">
|
||||
<FormSubmit
|
||||
value="Save license"
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
const { licenseKey } = e.detail;
|
||||
const resp = await apiCall('config/save-license-key', { licenseKey, tryToRenew: true });
|
||||
if (resp?.status == 'ok') {
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = resp?.errorMessage || 'Error saving license key';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-6 flex">
|
||||
<FormStyledButton
|
||||
value="Cancel"
|
||||
on:click={() => {
|
||||
isInsertingLicense = false;
|
||||
errorMessage = '';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !isInsertingLicense}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Start 30-day trial"
|
||||
on:click={async e => {
|
||||
errorMessage = '';
|
||||
const license = await apiCall('config/start-trial');
|
||||
if (license?.status == 'ok') {
|
||||
value="Insert license key"
|
||||
on:click={() => {
|
||||
isInsertingLicense = true;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if trialButtonAvailable}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Start 30-day trial"
|
||||
on:click={async e => {
|
||||
errorMessage = '';
|
||||
const license = await apiCall('config/start-trial');
|
||||
if (license?.status == 'ok') {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = license?.errorMessage || 'Error starting trial';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if trialDaysLeft > 0}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value={`Continue trial (${trialDaysLeft} days left)`}
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
} else {
|
||||
errorMessage = license?.errorMessage || 'Error starting trial';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if trialDaysLeft > 0}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value={`Continue trial (${trialDaysLeft} days left)`}
|
||||
value="Purchase DbGate Premium"
|
||||
on:click={async e => {
|
||||
sessionStorage.setItem('continueTrialConfirmed', '1');
|
||||
internalRedirectTo(isOneOfPage('admin-license') ? '/admin.html' : '/index.html');
|
||||
// openWebLink(
|
||||
// `https://auth.dbgate.eu/create-checkout-session-simple?source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
// openWebLink(
|
||||
// `https://auth-proxy.dbgate.udolni.net/redirect-to-purchase?product=${getElectron() ? 'premium' : 'teram-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
openWebLink(
|
||||
`https://auth.dbgate.eu/redirect-to-purchase?product=${getElectron() ? 'premium' : 'team-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Purchase DbGate Premium"
|
||||
on:click={async e => {
|
||||
// openWebLink(
|
||||
// `https://auth.dbgate.eu/create-checkout-session-simple?source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
// openWebLink(
|
||||
// `https://auth-proxy.dbgate.udolni.net/redirect-to-purchase?product=${getElectron() ? 'premium' : 'teram-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
// );
|
||||
|
||||
openWebLink(
|
||||
`https://auth.dbgate.eu/redirect-to-purchase?product=${getElectron() ? 'premium' : 'team-premium'}&source=trial-${isExpired ? 'expired' : (trialDaysLeft ?? 'no')}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if getElectron()}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Exit"
|
||||
on:click={e => {
|
||||
getElectron().send('quit-app');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{#if getElectron()}
|
||||
<div class="submit">
|
||||
<FormStyledButton
|
||||
value="Exit"
|
||||
on:click={e => {
|
||||
getElectron().send('quit-app');
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
{#if errorMessage}
|
||||
@@ -125,8 +165,8 @@
|
||||
{/if}
|
||||
|
||||
<div class="purchase-info">
|
||||
For more info about DbGate licensing, you could visit <Link href="https://dbgate.eu/">dbgate.eu</Link> web or contact
|
||||
us at <Link href="mailto:sales@dbgate.eu">sales@dbgate.eu</Link>
|
||||
For more info about DbGate licensing, you could visit <Link href="https://dbgate.io/">dbgate.io</Link> web or contact
|
||||
us at <Link href="mailto:sales@dbgate.io">sales@dbgate.io</Link>
|
||||
</div>
|
||||
{:else}
|
||||
<ErrorInfo message="License for DbGate is not valid. Please contact administrator." />
|
||||
@@ -141,6 +181,10 @@
|
||||
font-size: xx-large;
|
||||
}
|
||||
|
||||
.infotext {
|
||||
margin: 1em;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: var(--dim-large-form-margin);
|
||||
display: flex;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<div class="heading">Configuration error</div>
|
||||
{#if $config?.checkedLicense?.status == 'error'}
|
||||
<ErrorInfo
|
||||
message={`Invalid license. Please contact sales@dbgate.eu for more details. ${$config?.checkedLicense?.error}`}
|
||||
message={`Invalid license. Please contact sales@dbgate.io for more details. ${$config?.checkedLicense?.error || ''}`}
|
||||
/>
|
||||
{:else if $config?.configurationError}
|
||||
<ErrorInfo message={$config?.configurationError} />
|
||||
|
||||
39
packages/web/src/admin/FolderPermissionChooser.svelte
Normal file
39
packages/web/src/admin/FolderPermissionChooser.svelte
Normal file
@@ -0,0 +1,39 @@
|
||||
<script lang='ts'>
|
||||
import PermissionCheckBox from './PermissionCheckBox.svelte';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
|
||||
const { values } = getFormContext();
|
||||
|
||||
export let onSetPermission;
|
||||
export let label;
|
||||
export let folder;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<PermissionCheckBox
|
||||
{label}
|
||||
permission={`files/${folder}/*`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<PermissionCheckBox
|
||||
label="Read"
|
||||
permission={`files/${folder}/read`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
{disabled}
|
||||
/>
|
||||
<PermissionCheckBox
|
||||
label="Write"
|
||||
permission={`files/${folder}/write`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
1
packages/web/src/ai/QueryAiAssistant.svelte
Normal file
1
packages/web/src/ai/QueryAiAssistant.svelte
Normal file
@@ -0,0 +1 @@
|
||||
This component is only for Premium edition
|
||||
@@ -53,14 +53,15 @@
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.fileName,
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
label: _t('appFile.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('appFile.renameFile', { defaultMessage: 'Rename file' }),
|
||||
onConfirm: newFile => {
|
||||
apiCall('apps/rename-file', {
|
||||
file: data.fileName,
|
||||
@@ -74,7 +75,7 @@
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.fileName}?`,
|
||||
message: _t('appFile.deleteFileConfirm', { defaultMessage: 'Really delete file {fileName}?', values: { fileName: data.fileName } }),
|
||||
onConfirm: () => {
|
||||
apiCall('apps/delete-file', {
|
||||
file: data.fileName,
|
||||
@@ -101,10 +102,10 @@
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
{ text: 'Rename', onClick: handleRename },
|
||||
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.json') && { text: 'Open JSON', onClick: handleOpenJsonFile },
|
||||
{ text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
{ text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
data.fileType.endsWith('.sql') && { text: _t('common.openSql', { defaultMessage: 'Open SQL' }), onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.json') && { text: _t('common.openJson', { defaultMessage: 'Open JSON' }), onClick: handleOpenJsonFile },
|
||||
|
||||
// data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
|
||||
];
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { useConnectionList } from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -34,8 +35,8 @@
|
||||
|
||||
showModal(InputTextModal, {
|
||||
value: name,
|
||||
label: 'New application name',
|
||||
header: 'Rename application',
|
||||
label: _t('appFolder.newApplicationName', { defaultMessage: 'New application name' }),
|
||||
header: _t('appFolder.renameApplication', { defaultMessage: 'Rename application' }),
|
||||
onConfirm: async newFolder => {
|
||||
await apiCall('apps/rename-folder', {
|
||||
folder: data.name,
|
||||
@@ -60,16 +61,16 @@
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Delete', onClick: handleDelete },
|
||||
{ text: 'Rename', onClick: handleRename },
|
||||
{ text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
{ text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
|
||||
$currentDatabase && [
|
||||
!isOnCurrentDb($currentDatabase, $connections) && {
|
||||
text: 'Enable on current database',
|
||||
text: _t('appFolder.enableOnCurrentDatabase', { defaultMessage: 'Enable on current database' }),
|
||||
onClick: () => setOnCurrentDb(true),
|
||||
},
|
||||
isOnCurrentDb($currentDatabase, $connections) && {
|
||||
text: 'Disable on current database',
|
||||
text: _t('appFolder.disableOnCurrentDatabase', { defaultMessage: 'Disable on current database' }),
|
||||
onClick: () => setOnCurrentDb(false),
|
||||
},
|
||||
],
|
||||
@@ -90,7 +91,7 @@
|
||||
title={data.name}
|
||||
icon={'img app'}
|
||||
statusIcon={isOnCurrentDb($currentDatabase, $connections) ? 'icon check' : null}
|
||||
statusTitle={`Application ${data.name} is used for database ${$currentDatabase?.name}`}
|
||||
statusTitle={_t('appFolder.applicationUsedForDatabase', { defaultMessage: 'Application {application} is used for database {database}', values: { application: data.name, database: $currentDatabase?.name } })}
|
||||
isBold={data.name == $currentApplication}
|
||||
on:click={() => ($currentApplication = data.name)}
|
||||
menu={createMenu}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
export let filter = null;
|
||||
export let disableHover = false;
|
||||
export let divProps = {};
|
||||
export let additionalIcons = null;
|
||||
|
||||
$: isChecked =
|
||||
checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x));
|
||||
@@ -160,6 +161,11 @@
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if additionalIcons}
|
||||
{#each additionalIcons as ic}
|
||||
<FontIcon icon={ic.icon} title={ic.title} colorClass={ic.colorClass} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if extInfo}
|
||||
<span class="ext-info">
|
||||
<TokenizedFilteredText text={extInfo} {filter} />
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div on:drop={handleDrop}>
|
||||
<div on:drop={handleDrop} data-testid={`app-object-group-items-${_.kebabCase(group)}`}>
|
||||
{#each items as item}
|
||||
<AppObjectListItem
|
||||
isHidden={!item.isMatched}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import Link from '../elements/Link.svelte';
|
||||
import { focusedConnectionOrDatabase } from '../stores';
|
||||
import { tick } from 'svelte';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let list;
|
||||
export let module;
|
||||
@@ -38,8 +39,19 @@
|
||||
|
||||
$: matcher = module.createMatcher && module.createMatcher(filter, passProps?.searchSettings);
|
||||
|
||||
$: listTranslated = (list || []).map(data => ({
|
||||
...data,
|
||||
group: data?.group && _tval(data.group),
|
||||
title: data?.title && _tval(data.title),
|
||||
description: data?.description && _tval(data.description),
|
||||
args: (data?.args || []).map(x => ({
|
||||
...x,
|
||||
label: x?.label && _tval(x.label),
|
||||
})),
|
||||
}));
|
||||
|
||||
$: dataLabeled = _.compact(
|
||||
(list || []).map(data => {
|
||||
(listTranslated || []).map(data => {
|
||||
const matchResult = matcher ? matcher(data) : true;
|
||||
|
||||
let isMatched = true;
|
||||
@@ -102,7 +114,8 @@
|
||||
|
||||
$: groups = groupFunc ? extendGroups(_.groupBy(dataLabeled, 'group'), emptyGroupNames) : null;
|
||||
|
||||
$: listLimited = isExpandedBySearch && !expandLimited ? filtered.slice(0, filter.trim().length < 3 ? 1 : 3) : list;
|
||||
$: listLimited =
|
||||
isExpandedBySearch && !expandLimited ? filtered.slice(0, filter.trim().length < 3 ? 1 : 3) : listTranslated;
|
||||
$: isListLimited = isExpandedBySearch && listLimited.length < filtered.length;
|
||||
$: listMissingItems = isListLimited ? filtered.slice(listLimited.length) : [];
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@
|
||||
import { apiCall } from '../utility/api';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
$: isZipped = data.folderName?.endsWith('.zip');
|
||||
@@ -89,8 +90,8 @@
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.fileName,
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
label: _t('archiveFile.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('archiveFile.renameFile', { defaultMessage: 'Rename file' }),
|
||||
onConfirm: newFile => {
|
||||
apiCall('archive/rename-file', {
|
||||
file: data.fileName,
|
||||
@@ -104,7 +105,7 @@
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.fileName}?`,
|
||||
message: _t('archiveFile.deleteFileConfirm', { defaultMessage: 'Really delete file {fileName}?', values: { fileName: data.fileName } }),
|
||||
onConfirm: () => {
|
||||
apiCall('archive/delete-file', {
|
||||
file: data.fileName,
|
||||
@@ -147,10 +148,10 @@
|
||||
}
|
||||
|
||||
return [
|
||||
data.fileType == 'jsonl' && { text: 'Open', onClick: handleOpenArchive },
|
||||
data.fileType == 'jsonl' && { text: 'Open in text editor', onClick: handleOpenJsonLinesText },
|
||||
!isZipped && { text: 'Delete', onClick: handleDelete },
|
||||
!isZipped && { text: 'Rename', onClick: handleRename },
|
||||
data.fileType == 'jsonl' && { text: _t('common.open', { defaultMessage: 'Open' }), onClick: handleOpenArchive },
|
||||
data.fileType == 'jsonl' && { text: _t('common.openInTextEditor', { defaultMessage: 'Open in text editor' }), onClick: handleOpenJsonLinesText },
|
||||
!isZipped && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
!isZipped && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
data.fileType == 'jsonl' &&
|
||||
createQuickExportMenu(
|
||||
fmt => async () => {
|
||||
@@ -185,19 +186,19 @@
|
||||
},
|
||||
}
|
||||
),
|
||||
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
|
||||
data.fileType.endsWith('.sql') && { text: _t('common.openSql', { defaultMessage: 'Open SQL' }), onClick: handleOpenSqlFile },
|
||||
data.fileType.endsWith('.yaml') && { text: _t('common.openYaml', { defaultMessage: 'Open YAML' }), onClick: handleOpenYamlFile },
|
||||
!isZipped &&
|
||||
isProApp() &&
|
||||
data.fileType == 'jsonl' && {
|
||||
text: 'Open in profiler',
|
||||
text: _t('common.openInProfiler', { defaultMessage: 'Open in profiler' }),
|
||||
submenu: getExtensions()
|
||||
.drivers.filter(eng => eng.profilerFormatterFunction)
|
||||
.map(eng => ({
|
||||
text: eng.title,
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Profiler',
|
||||
title: _t('common.profiler', { defaultMessage: 'Profiler' }),
|
||||
icon: 'img profiler',
|
||||
tabComponent: 'ProfilerTab',
|
||||
props: {
|
||||
|
||||
@@ -21,14 +21,15 @@
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { extractShellConnection } from '../impexp/createImpExpScript';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: data.name.endsWith('.link')
|
||||
? `Really delete link to folder ${data.name}? Folder content remains untouched.`
|
||||
: `Really delete folder ${data.name}?`,
|
||||
? _t('archiveFolder.deleteLinkConfirm', { defaultMessage: 'Really delete link to folder {folderName}? Folder content remains untouched.', values: { folderName: data.name } })
|
||||
: _t('archiveFolder.deleteFolderConfirm', { defaultMessage: 'Really delete folder {folderName}?', values: { folderName: data.name } }),
|
||||
onConfirm: () => {
|
||||
apiCall('archive/delete-folder', { folder: data.name });
|
||||
},
|
||||
@@ -42,8 +43,8 @@
|
||||
|
||||
showModal(InputTextModal, {
|
||||
value: name,
|
||||
label: 'New folder name',
|
||||
header: 'Rename folder',
|
||||
label: _t('archiveFolder.newFolderName', { defaultMessage: 'New folder name' }),
|
||||
header: _t('archiveFolder.renameFolder', { defaultMessage: 'Rename folder' }),
|
||||
onConfirm: async newFolder => {
|
||||
await apiCall('archive/rename-folder', {
|
||||
folder: data.name,
|
||||
@@ -95,7 +96,7 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
const handleCompareWithCurrentDb = () => {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Compare',
|
||||
title: _t('common.compare', { defaultMessage: 'Compare' }),
|
||||
icon: 'img compare',
|
||||
tabComponent: 'CompareModelTab',
|
||||
props: {
|
||||
@@ -153,7 +154,7 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
});
|
||||
},
|
||||
{
|
||||
formatLabel: 'ZIP files',
|
||||
formatLabel: _t('common.zipFiles', { defaultMessage: 'ZIP files' }),
|
||||
formatExtension: 'zip',
|
||||
defaultFileName: data.name?.endsWith('.zip') ? data.name : data.name + '.zip',
|
||||
}
|
||||
@@ -162,28 +163,28 @@ await dbgateApi.deployDb(${JSON.stringify(
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
data.name != 'default' && { text: 'Delete', onClick: handleDelete },
|
||||
data.name != 'default' && { text: 'Rename', onClick: handleRename },
|
||||
isProApp() && { text: 'Data deployer', onClick: handleOpenDataDeployTab },
|
||||
data.name != 'default' && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
data.name != 'default' && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
isProApp() && { text: _t('common.dataDeployer', { defaultMessage: 'Data deployer' }), onClick: handleOpenDataDeployTab },
|
||||
$currentDatabase && [
|
||||
{ text: 'Generate deploy DB SQL', onClick: handleGenerateDeploySql },
|
||||
{ text: 'Shell: Deploy DB', onClick: handleGenerateDeployScript },
|
||||
{ text: _t('archiveFolder.generateDeployDbSql', { defaultMessage: 'Generate deploy DB SQL' }), onClick: handleGenerateDeploySql },
|
||||
hasPermission(`run-shell-script`) && { text: _t('archiveFolder.shellDeployDb', { defaultMessage: 'Shell: Deploy DB' }), onClick: handleGenerateDeployScript },
|
||||
],
|
||||
data.name != 'default' &&
|
||||
isProApp() &&
|
||||
data.name.endsWith('.zip') && { text: 'Unpack ZIP', onClick: () => handleZipUnzip('archive/unzip') },
|
||||
data.name.endsWith('.zip') && { text: _t('archiveFolder.unpackZip', { defaultMessage: 'Unpack ZIP' }), onClick: () => handleZipUnzip('archive/unzip') },
|
||||
data.name != 'default' &&
|
||||
isProApp() &&
|
||||
!data.name.endsWith('.zip') && { text: 'Pack (create ZIP)', onClick: () => handleZipUnzip('archive/zip') },
|
||||
!data.name.endsWith('.zip') && { text: _t('archiveFolder.packZip', { defaultMessage: 'Pack (create ZIP)' }), onClick: () => handleZipUnzip('archive/zip') },
|
||||
|
||||
isProApp() && { text: 'Download ZIP', onClick: handleDownloadZip },
|
||||
isProApp() && { text: _t('archiveFolder.downloadZip', { defaultMessage: 'Download ZIP' }), onClick: handleDownloadZip },
|
||||
|
||||
data.name != 'default' &&
|
||||
hasPermission('dbops/model/compare') &&
|
||||
isProApp() &&
|
||||
_.get($currentDatabase, 'connection._id') && {
|
||||
onClick: handleCompareWithCurrentDb,
|
||||
text: `Compare with ${_.get($currentDatabase, 'name')}`,
|
||||
text: _t('archiveFolder.compareWithCurrentDb', { defaultMessage: 'Compare with {name}', values: { name: _.get($currentDatabase, 'name') } }),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
getOpenedTabs,
|
||||
openedConnections,
|
||||
openedSingleDatabaseConnections,
|
||||
pinnedDatabases,
|
||||
} from '../stores';
|
||||
import { filterName, filterNameCompoud } from 'dbgate-tools';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
@@ -130,7 +131,7 @@
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseList, useAllApps } from '../utility/metadataLoaders';
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
import { apiCall, removeVolatileMapping } from '../utility/api';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
@@ -152,6 +153,8 @@
|
||||
let engineStatusIcon = null;
|
||||
let engineStatusTitle = null;
|
||||
|
||||
$: isPinned = data.singleDatabase && !!$pinnedDatabases.find(x => x?.connection?._id == data?._id);
|
||||
|
||||
const electron = getElectron();
|
||||
|
||||
const handleConnect = (disableExpand = false) => {
|
||||
@@ -276,7 +279,7 @@
|
||||
showModal(InputTextModal, {
|
||||
header: _t('connection.createDatabase', { defaultMessage: 'Create database' }),
|
||||
value: 'newdb',
|
||||
label: _t('connection.databaseName', { defaultMessage: 'Database name' }),
|
||||
label: _t('connection.database', { defaultMessage: 'Database name' }),
|
||||
onConfirm: name =>
|
||||
apiCall('server-connections/create-database', {
|
||||
conid: data._id,
|
||||
@@ -382,7 +385,8 @@
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
data.databasePermissionRole
|
||||
),
|
||||
],
|
||||
|
||||
@@ -426,7 +430,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -454,6 +458,19 @@
|
||||
.find(x => x.isNewQuery)
|
||||
.onClick();
|
||||
}}
|
||||
onPin={!isPinned && data.singleDatabase
|
||||
? () =>
|
||||
pinnedDatabases.update(list => [
|
||||
...list,
|
||||
{
|
||||
name: data.defaultDatabase,
|
||||
connection: data,
|
||||
},
|
||||
])
|
||||
: null}
|
||||
onUnpin={isPinned && data.singleDatabase
|
||||
? () => pinnedDatabases.update(list => list.filter(x => x?.connection?._id != data?._id))
|
||||
: null}
|
||||
isChoosed={data._id == $focusedConnectionOrDatabase?.conid &&
|
||||
(data.singleDatabase
|
||||
? $focusedConnectionOrDatabase?.database == data.defaultDatabase
|
||||
|
||||
@@ -46,7 +46,8 @@
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
databasePermissionRole
|
||||
) {
|
||||
const apps = filterAppsForDatabase(connection, name, $apps);
|
||||
const handleNewQuery = () => {
|
||||
@@ -404,19 +405,36 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateNewApp = () => {
|
||||
showModal(InputTextModal, {
|
||||
header: _t('database.newApplication', { defaultMessage: 'New application' }),
|
||||
label: _t('database.applicationName', { defaultMessage: 'Application name' }),
|
||||
value: _.startCase(name),
|
||||
onConfirm: async appName => {
|
||||
const newAppId = await apiCall('apps/create-app-from-db', {
|
||||
appName,
|
||||
server: connection?.server,
|
||||
database: name,
|
||||
});
|
||||
openApplicationEditor(newAppId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const driver = findEngineDriver(connection, getExtensions());
|
||||
|
||||
const commands = _.flatten((apps || []).map(x => x.commands || []));
|
||||
const commands = _.flatten((apps || []).map(x => Object.values(x.files || {}).filter(x => x.type == 'command')));
|
||||
|
||||
const isSqlOrDoc =
|
||||
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
|
||||
|
||||
return [
|
||||
hasPermission(`dbops/query`) && {
|
||||
onClick: handleNewQuery,
|
||||
text: _t('database.newQuery', { defaultMessage: 'New query' }),
|
||||
isNewQuery: true,
|
||||
},
|
||||
hasPermission(`dbops/query`) &&
|
||||
isAllowedDatabaseRunScript(databasePermissionRole) && {
|
||||
onClick: handleNewQuery,
|
||||
text: _t('database.newQuery', { defaultMessage: 'New query' }),
|
||||
isNewQuery: true,
|
||||
},
|
||||
hasPermission(`dbops/model/edit`) &&
|
||||
!connection.isReadOnly &&
|
||||
driver?.databaseEngineTypes?.includes('sql') && {
|
||||
@@ -428,8 +446,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
driver?.databaseEngineTypes?.includes('document') && {
|
||||
onClick: handleNewCollection,
|
||||
text: _t('database.newCollection', {
|
||||
defaultMessage: 'New {collectionLabel}',
|
||||
values: { collectionLabel: driver?.collectionSingularLabel ?? 'collection/container' },
|
||||
defaultMessage: 'New collection/container'
|
||||
}),
|
||||
},
|
||||
hasPermission(`dbops/query`) &&
|
||||
@@ -545,12 +562,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
{ divider: true },
|
||||
|
||||
driver?.databaseEngineTypes?.includes('sql') &&
|
||||
hasPermission(`run-shell-script`) &&
|
||||
hasPermission(`dbops/dropdb`) && {
|
||||
onClick: handleGenerateDropAllObjectsScript,
|
||||
text: _t('database.shellDropAllObjects', { defaultMessage: 'Shell: Drop all objects' }),
|
||||
},
|
||||
|
||||
{
|
||||
hasPermission(`run-shell-script`) && {
|
||||
onClick: handleGenerateRunScript,
|
||||
text: _t('database.shellRunScript', { defaultMessage: 'Shell: Run script' }),
|
||||
},
|
||||
@@ -561,11 +579,26 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
text: _t('database.dataDeployer', { defaultMessage: 'Data deployer' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
hasPermission(`files/apps/write`) && {
|
||||
onClick: handleCreateNewApp,
|
||||
text: _t('database.createNewApplication', { defaultMessage: 'Create new application' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
apps?.length > 0 && {
|
||||
text: _t('database.editApplications', { defaultMessage: 'Edit application' }),
|
||||
submenu: apps.map((app: any) => ({
|
||||
text: app.applicationName,
|
||||
onClick: () => openApplicationEditor(app.appid),
|
||||
})),
|
||||
},
|
||||
|
||||
{ divider: true },
|
||||
|
||||
commands.length > 0 && [
|
||||
commands.map((cmd: any) => ({
|
||||
text: cmd.name,
|
||||
text: cmd.label,
|
||||
onClick: () => {
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql: cmd.sql,
|
||||
@@ -615,17 +648,17 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
getConnectionLabel,
|
||||
} from 'dbgate-tools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseInfo, useAllApps, useDatabaseInfoPeek } from '../utility/metadataLoaders';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ConfirmSqlModal, { runOperationOnDatabase, saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
|
||||
import { filterAppsForDatabase } from '../utility/appTools';
|
||||
import { filterAppsForDatabase, openApplicationEditor } from '../utility/appTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
import NewCollectionModal from '../modals/NewCollectionModal.svelte';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import hasPermission, { isAllowedDatabaseRunScript } from '../utility/hasPermission';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import newTable from '../tableeditor/newTable';
|
||||
import { loadSchemaList, switchCurrentDatabase } from '../utility/common';
|
||||
@@ -636,6 +669,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
|
||||
import { getDatabaseClickActionSetting } from '../settings/settingsTools';
|
||||
import { _t } from '../translations';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -647,13 +681,19 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
$extensions,
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections
|
||||
$openedSingleDatabaseConnections,
|
||||
data.databasePermissionRole
|
||||
);
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: isLoadingSchemas = $loadingSchemaLists[`${data?.connection?._id}::${data?.name}`];
|
||||
$: dbInfo = useDatabaseInfoPeek({ conid: data?.connection?._id, database: data?.name });
|
||||
|
||||
$: appsForDb = filterAppsForDatabase(data?.connection, data?.name, $apps, $dbInfo);
|
||||
|
||||
// $: console.log('AppsForDB:', data?.name, appsForDb);
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -676,6 +716,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
switchCurrentDatabase(data);
|
||||
}
|
||||
}}
|
||||
additionalIcons={appsForDb?.length > 0
|
||||
? appsForDb.map(ic => ({
|
||||
icon: ic.applicationIcon || 'img app',
|
||||
title: ic.applicationName,
|
||||
colorClass: ic.applicationColor ? `color-icon-${ic.applicationColor}` : undefined,
|
||||
}))
|
||||
: null}
|
||||
on:mousedown={() => {
|
||||
$focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection };
|
||||
}}
|
||||
@@ -697,6 +744,9 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
).length
|
||||
)
|
||||
: ''}
|
||||
statusIconBefore={data.databasePermissionRole == 'read_content' || data.databasePermissionRole == 'view'
|
||||
? 'icon lock'
|
||||
: null}
|
||||
menu={createMenu}
|
||||
showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin}
|
||||
onPin={isPinned ? null : () => pinnedDatabases.update(list => [...list, data])}
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
<script lang="ts" context="module">
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import { _t, _tval, DefferedTranslationResult } from '../translations';
|
||||
import sqlFormatter from 'sql-formatter';
|
||||
|
||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
export const createMatcher =
|
||||
(filter, cfg = DEFAULT_OBJECT_SEARCH_SETTINGS) =>
|
||||
({ schemaName, pureName, objectComment, tableEngine, columns, objectTypeField, tableName, createSql }) => {
|
||||
({
|
||||
schemaName,
|
||||
pureName,
|
||||
objectComment,
|
||||
tableEngine,
|
||||
columns,
|
||||
objectTypeField,
|
||||
tableName,
|
||||
createSql,
|
||||
tableRowCount,
|
||||
}) => {
|
||||
const mainArgs = [];
|
||||
const childArgs = [];
|
||||
if (cfg.schemaName) mainArgs.push(schemaName);
|
||||
@@ -12,6 +24,7 @@
|
||||
if (objectTypeField == 'tables') {
|
||||
if (cfg.tableComment) mainArgs.push(objectComment);
|
||||
if (cfg.tableEngine) mainArgs.push(tableEngine);
|
||||
if (cfg.tablesWithRows && !tableRowCount) return 'none';
|
||||
|
||||
for (const column of columns || []) {
|
||||
if (cfg.columnName) childArgs.push(column.columnName);
|
||||
@@ -45,26 +58,26 @@
|
||||
schedulerEvents: 'icon scheduler-event',
|
||||
};
|
||||
|
||||
const defaultTabs = {
|
||||
tables: 'TableDataTab',
|
||||
collections: 'CollectionDataTab',
|
||||
views: 'ViewDataTab',
|
||||
matviews: 'ViewDataTab',
|
||||
queries: 'QueryDataTab',
|
||||
procedures: 'SqlObjectTab',
|
||||
functions: 'SqlObjectTab',
|
||||
triggers: 'SqlObjectTab',
|
||||
};
|
||||
// const defaultTabs = {
|
||||
// tables: 'TableDataTab',
|
||||
// collections: 'CollectionDataTab',
|
||||
// views: 'ViewDataTab',
|
||||
// matviews: 'ViewDataTab',
|
||||
// queries: 'QueryDataTab',
|
||||
// procedures: 'SqlObjectTab',
|
||||
// functions: 'SqlObjectTab',
|
||||
// triggers: 'SqlObjectTab',
|
||||
// };
|
||||
|
||||
function createScriptTemplatesSubmenu(objectTypeField) {
|
||||
return {
|
||||
label: 'SQL template',
|
||||
label: _t('dbObject.sqlTemplate', { defaultMessage: 'SQL template' }),
|
||||
submenu: getSupportedScriptTemplates(objectTypeField),
|
||||
};
|
||||
}
|
||||
|
||||
interface DbObjMenuItem {
|
||||
label?: string;
|
||||
label?: string | DefferedTranslationResult;
|
||||
tab?: string;
|
||||
forceNewTab?: boolean;
|
||||
initialData?: any;
|
||||
@@ -76,7 +89,8 @@
|
||||
isRename?: boolean;
|
||||
isTruncate?: boolean;
|
||||
isCopyTableName?: boolean;
|
||||
isDuplicateTable?: boolean;
|
||||
isTableBackup?: boolean;
|
||||
isTableRestore?: boolean;
|
||||
isDiagram?: boolean;
|
||||
functionName?: string;
|
||||
isExport?: boolean;
|
||||
@@ -94,6 +108,8 @@
|
||||
}
|
||||
|
||||
function createMenusCore(objectTypeField, driver, data): DbObjMenuItem[] {
|
||||
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
|
||||
switch (objectTypeField) {
|
||||
case 'tables':
|
||||
return [
|
||||
@@ -102,19 +118,19 @@
|
||||
divider: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design query',
|
||||
label: _t('dbObject.designQuery', { defaultMessage: 'Design query' }),
|
||||
isQueryDesigner: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design perspective query',
|
||||
label: _t('dbObject.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }),
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
createScriptTemplatesSubmenu('tables'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE TABLE',
|
||||
@@ -143,45 +159,52 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop table',
|
||||
label: _t('dbObject.dropTable', { defaultMessage: 'Drop table' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/rename') &&
|
||||
!driver?.dialect.disableRenameTable && {
|
||||
label: 'Rename table',
|
||||
label: _t('dbObject.renameTable', { defaultMessage: 'Rename table' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/truncate') && {
|
||||
label: 'Truncate table',
|
||||
label: _t('dbObject.truncateTable', { defaultMessage: 'Truncate table' }),
|
||||
isTruncate: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
{
|
||||
label: 'Copy table name',
|
||||
label: _t('dbObject.copyTableName', { defaultMessage: 'Copy table name' }),
|
||||
isCopyTableName: true,
|
||||
requiresWriteAccess: false,
|
||||
},
|
||||
hasPermission('dbops/table/backup') && {
|
||||
label: 'Create table backup',
|
||||
isDuplicateTable: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/backup') &&
|
||||
!backupMatch && {
|
||||
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
|
||||
isTableBackup: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/restore') &&
|
||||
backupMatch && {
|
||||
label: _t('dbObject.createRestoreScript', { defaultMessage: 'Create restore script' }),
|
||||
isTableRestore: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/view') && {
|
||||
label: 'Show diagram',
|
||||
label: _t('dbObject.showDiagram', { defaultMessage: 'Show diagram' }),
|
||||
isDiagram: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/export') && {
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
functionName: 'tableReader',
|
||||
isExport: true,
|
||||
},
|
||||
hasPermission('dbops/import') && {
|
||||
label: 'Import',
|
||||
label: _t('common.import', { defaultMessage: 'Import' }),
|
||||
isImport: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -193,18 +216,18 @@
|
||||
divider: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design query',
|
||||
label: _t('dbObject.designQuery', { defaultMessage: 'Design query' }),
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design perspective query',
|
||||
label: _t('dbObject.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }),
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
createScriptTemplatesSubmenu('views'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE VIEW',
|
||||
@@ -224,12 +247,12 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop view',
|
||||
label: _t('dbObject.dropView', { defaultMessage: 'Drop view' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Rename view',
|
||||
label: _t('dbObject.renameView', { defaultMessage: 'Rename view' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -237,7 +260,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
isExport: true,
|
||||
functionName: 'tableReader',
|
||||
},
|
||||
@@ -249,12 +272,12 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop view',
|
||||
label: _t('dbObject.dropView', { defaultMessage: 'Drop view' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Rename view',
|
||||
label: _t('dbObject.renameView', { defaultMessage: 'Rename view' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -262,12 +285,12 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Query designer',
|
||||
label: _t('dbObject.queryDesigner', { defaultMessage: 'Query designer' }),
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
createScriptTemplatesSubmenu('matviews'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE MATERIALIZED VIEW',
|
||||
@@ -287,7 +310,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
isExport: true,
|
||||
functionName: 'tableReader',
|
||||
},
|
||||
@@ -295,7 +318,7 @@
|
||||
case 'queries':
|
||||
return [
|
||||
{
|
||||
label: 'Open data',
|
||||
label: _t('dbObject.openData', { defaultMessage: 'Open data' }),
|
||||
tab: 'QueryDataTab',
|
||||
forceNewTab: true,
|
||||
},
|
||||
@@ -307,18 +330,18 @@
|
||||
divider: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop procedure',
|
||||
label: _t('dbObject.dropProcedure', { defaultMessage: 'Drop procedure' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Rename procedure',
|
||||
label: _t('dbObject.renameProcedure', { defaultMessage: 'Rename procedure' }),
|
||||
isRename: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
createScriptTemplatesSubmenu('procedures'),
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE PROCEDURE',
|
||||
@@ -341,7 +364,7 @@
|
||||
return [
|
||||
...defaultDatabaseObjectAppObjectActions['triggers'],
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop trigger',
|
||||
label: _t('dbObject.dropTrigger', { defaultMessage: 'Drop trigger' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -349,7 +372,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE TRIGGER',
|
||||
@@ -373,28 +396,28 @@
|
||||
divider: true,
|
||||
},
|
||||
isProApp() && {
|
||||
label: 'Design perspective query',
|
||||
label: _t('dbObject.designPerspectiveQuery', { defaultMessage: 'Design perspective query' }),
|
||||
tab: 'PerspectiveTab',
|
||||
forceNewTab: true,
|
||||
icon: 'img perspective',
|
||||
},
|
||||
hasPermission('dbops/export') && {
|
||||
label: 'Export',
|
||||
label: _t('common.export', { defaultMessage: 'Export' }),
|
||||
isExport: true,
|
||||
functionName: 'tableReader',
|
||||
},
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: `Drop ${driver?.collectionSingularLabel ?? 'collection/container'}`,
|
||||
label: _t('dbObject.dropCollection', { defaultMessage: 'Drop collection/container' }),
|
||||
isDropCollection: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/rename') && {
|
||||
label: `Rename ${driver?.collectionSingularLabel ?? 'collection/container'}`,
|
||||
label: _t('dbObject.renameCollection', { defaultMessage: 'Rename collection/container' }),
|
||||
isRenameCollection: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/backup') && {
|
||||
label: `Create ${driver?.collectionSingularLabel ?? 'collection/container'} backup`,
|
||||
label: _t('dbObject.createCollectionBackup', { defaultMessage: 'Create collection/container backup' }),
|
||||
isDuplicateCollection: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -407,7 +430,7 @@
|
||||
const menu: DbObjMenuItem[] = [
|
||||
...defaultDatabaseObjectAppObjectActions['schedulerEvents'],
|
||||
hasPermission('dbops/model/edit') && {
|
||||
label: 'Drop event',
|
||||
label: _t('dbObject.dropEvent', { defaultMessage: 'Drop event' }),
|
||||
isDrop: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
@@ -415,12 +438,12 @@
|
||||
|
||||
if (data?.status === 'ENABLED') {
|
||||
menu.push({
|
||||
label: 'Disable',
|
||||
label: _t('dbObject.disable', { defaultMessage: 'Disable' }),
|
||||
isDisableEvent: true,
|
||||
});
|
||||
} else {
|
||||
menu.push({
|
||||
label: 'Enable',
|
||||
label: _t('dbObject.enable', { defaultMessage: 'Enable' }),
|
||||
isEnableEvent: true,
|
||||
});
|
||||
}
|
||||
@@ -430,7 +453,7 @@
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: 'SQL generator',
|
||||
label: _t('dbObject.sqlGenerator', { defaultMessage: 'SQL generator' }),
|
||||
submenu: [
|
||||
{
|
||||
label: 'CREATE SCHEDULER EVENT',
|
||||
@@ -463,7 +486,7 @@
|
||||
if (menu.isQueryDesigner) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query #',
|
||||
title: _t('dbObject.query', { defaultMessage: 'Query #' }),
|
||||
icon: 'img query-design',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
focused: true,
|
||||
@@ -488,7 +511,7 @@
|
||||
} else if (menu.isDiagram) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Diagram #',
|
||||
title: _t('dbObject.diagram', { defaultMessage: 'Diagram #' }),
|
||||
icon: 'img diagram',
|
||||
tabComponent: 'DiagramTab',
|
||||
props: {
|
||||
@@ -578,7 +601,10 @@
|
||||
});
|
||||
} else if (menu.isDropCollection) {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really drop collection ${data.pureName}?`,
|
||||
message: _t('dbObject.confirmDropCollection', {
|
||||
defaultMessage: 'Really drop collection {name}?',
|
||||
values: { name: data.pureName },
|
||||
}),
|
||||
onConfirm: async () => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
runOperationOnDatabase(dbid, {
|
||||
@@ -592,8 +618,8 @@
|
||||
} else if (menu.isRenameCollection) {
|
||||
const driver = await getDriver();
|
||||
showModal(InputTextModal, {
|
||||
label: `New ${driver?.collectionSingularLabel ?? 'collection/container'} name`,
|
||||
header: `Rename ${driver?.collectionSingularLabel ?? 'collection/container'}`,
|
||||
label: _t('dbObject.newCollectionName', { defaultMessage: 'New collection/container name' }),
|
||||
header: _t('dbObject.renameCollection', { defaultMessage: 'Rename collection/container' }),
|
||||
value: data.pureName,
|
||||
onConfirm: async newName => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
@@ -609,7 +635,10 @@
|
||||
const driver = await getDriver();
|
||||
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really create ${driver?.collectionSingularLabel ?? 'collection/container'} copy named ${newName}?`,
|
||||
message: _t('dbObject.confirmCloneCollection', {
|
||||
defaultMessage: 'Really create collection/container copy named {name}?',
|
||||
values: { name: newName },
|
||||
}),
|
||||
onConfirm: async () => {
|
||||
const dbid = _.pick(data, ['conid', 'database']);
|
||||
runOperationOnDatabase(dbid, {
|
||||
@@ -619,7 +648,7 @@
|
||||
});
|
||||
},
|
||||
});
|
||||
} else if (menu.isDuplicateTable) {
|
||||
} else if (menu.isTableBackup) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const newTable = _.cloneDeep(data);
|
||||
@@ -653,6 +682,25 @@
|
||||
},
|
||||
engine: driver.engine,
|
||||
});
|
||||
} else if (menu.isTableRestore) {
|
||||
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const db = await getDatabaseInfo(data);
|
||||
if (db) {
|
||||
const originalTable = db?.tables?.find(x => x.pureName == backupMatch[1] && x.schemaName == data.schemaName);
|
||||
if (originalTable) {
|
||||
createTableRestoreScript(data, originalTable, dmp);
|
||||
newQuery({
|
||||
title: _t('dbObject.restoreScript', {
|
||||
defaultMessage: 'Restore {name} #',
|
||||
values: { name: backupMatch[1] },
|
||||
}),
|
||||
initialData: sqlFormatter.format(dmp.s),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (menu.isImport) {
|
||||
const { conid, database } = data;
|
||||
openImportExportTab({
|
||||
@@ -703,15 +751,30 @@
|
||||
}
|
||||
|
||||
function createMenus(objectTypeField, driver, data): ReturnType<typeof createMenusCore> {
|
||||
return createMenusCore(objectTypeField, driver, data).filter(x => {
|
||||
if (x.scriptTemplate) {
|
||||
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
|
||||
const coreMenus = createMenusCore(objectTypeField, driver, data);
|
||||
|
||||
const filteredSumenus = coreMenus.map(item => {
|
||||
if (!item) return item;
|
||||
if (!item.submenu) {
|
||||
return { ...item, label: _tval(item.label) };
|
||||
}
|
||||
if (x.sqlGeneratorProps) {
|
||||
return hasPermission(`dbops/sql-generator`);
|
||||
}
|
||||
return true;
|
||||
return {
|
||||
...item,
|
||||
submenu: item.submenu.filter(x => {
|
||||
if (x.scriptTemplate) {
|
||||
return hasPermission(`dbops/sql-template/${x.scriptTemplate}`);
|
||||
}
|
||||
if (x.sqlGeneratorProps) {
|
||||
return hasPermission(`dbops/sql-generator`);
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const filteredNoEmptySubmenus = _.compact(filteredSumenus).filter(x => !x.submenu || x.submenu.length > 0);
|
||||
|
||||
return filteredNoEmptySubmenus;
|
||||
}
|
||||
|
||||
function getObjectTitle(connection, schemaName, pureName) {
|
||||
@@ -727,7 +790,7 @@
|
||||
export async function openDatabaseObjectDetail(
|
||||
tabComponent,
|
||||
scriptTemplate,
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode },
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode, sql },
|
||||
forceNewTab?,
|
||||
initialData?,
|
||||
icon?,
|
||||
@@ -743,7 +806,9 @@
|
||||
openNewTab(
|
||||
{
|
||||
// title: getObjectTitle(connection, schemaName, pureName),
|
||||
title: tabComponent ? getObjectTitle(connection, schemaName, pureName) : 'Query #',
|
||||
title: tabComponent
|
||||
? getObjectTitle(connection, schemaName, pureName)
|
||||
: _t('dbObject.query', { defaultMessage: 'Query #' }),
|
||||
focused: tabComponent == null,
|
||||
tooltip,
|
||||
icon:
|
||||
@@ -762,6 +827,7 @@
|
||||
initialArgs: scriptTemplate ? { scriptTemplate } : null,
|
||||
defaultActionId,
|
||||
isRawMode,
|
||||
sql,
|
||||
},
|
||||
},
|
||||
initialData,
|
||||
@@ -783,7 +849,7 @@
|
||||
data,
|
||||
{ forceNewTab = false, tabPreviewMode = false, focusTab = false } = {}
|
||||
) {
|
||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||
const { schemaName, pureName, conid, database, objectTypeField, sql } = data;
|
||||
const driver = findEngineDriver(data, getExtensions());
|
||||
|
||||
const activeTab = getActiveTab();
|
||||
@@ -829,6 +895,7 @@
|
||||
objectTypeField,
|
||||
defaultActionId: prefferedAction.defaultActionId,
|
||||
isRawMode: prefferedAction?.isRawMode ?? false,
|
||||
sql,
|
||||
},
|
||||
forceNewTab,
|
||||
prefferedAction?.initialData,
|
||||
@@ -970,6 +1037,8 @@
|
||||
|
||||
return handleDatabaseObjectClick(data, { forceNewTab, tabPreviewMode, focusTab });
|
||||
}
|
||||
|
||||
export const TABLE_BACKUP_REGEX = /^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -987,7 +1056,7 @@
|
||||
} from '../stores';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { extractDbNameFromComposite, filterNameCompoud, getConnectionLabel } from 'dbgate-tools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import fullDisplayName from '../utility/fullDisplayName';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
@@ -1008,6 +1077,9 @@
|
||||
import { getSupportedScriptTemplates } from '../utility/applyScriptTemplate';
|
||||
import { getBoolSettingsValue, getOpenDetailOnArrowsSettings } from '../settings/settingsTools';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import formatFileSize from '../utility/formatFileSize';
|
||||
import { createTableRestoreScript } from '../utility/tableRestoreScript';
|
||||
import newQuery from '../query/newQuery';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -1036,6 +1108,9 @@
|
||||
if (data.tableRowCount != null) {
|
||||
res.push(`${formatRowCount(data.tableRowCount)} rows`);
|
||||
}
|
||||
if (data.sizeBytes) {
|
||||
res.push(formatFileSize(data.sizeBytes));
|
||||
}
|
||||
if (data.tableEngine) {
|
||||
res.push(data.tableEngine);
|
||||
}
|
||||
@@ -1044,14 +1119,21 @@
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
||||
|
||||
$: backupParsed = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
$: backupTitle =
|
||||
backupParsed != null
|
||||
? `${backupParsed[1]} (${backupParsed[2]}-${backupParsed[3]}-${backupParsed[4]} ${backupParsed[5]}:${backupParsed[6]}:${backupParsed[7]})`
|
||||
: null;
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
{...$$restProps}
|
||||
module={$$props.module}
|
||||
{data}
|
||||
title={data.schemaName && !passProps?.hideSchemaName ? `${data.schemaName}.${data.pureName}` : data.pureName}
|
||||
icon={databaseObjectIcons[data.objectTypeField]}
|
||||
title={backupTitle ??
|
||||
(data.schemaName && !passProps?.hideSchemaName ? `${data.schemaName}.${data.pureName}` : data.pureName)}
|
||||
icon={backupParsed ? 'img table-backup' : databaseObjectIcons[data.objectTypeField]}
|
||||
menu={createMenu}
|
||||
showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin}
|
||||
onPin={passProps?.ingorePin ? null : isPinned ? null : () => pinnedTables.update(list => [...list, data])}
|
||||
@@ -1062,6 +1144,7 @@
|
||||
: null}
|
||||
extInfo={getExtInfo(data)}
|
||||
isChoosed={matchDatabaseObjectAppObject($selectedDatabaseObjectAppObject, data)}
|
||||
statusIconBefore={data.tablePermissionRole == 'read' ? 'icon lock' : null}
|
||||
on:click={() => handleObjectClick(data, 'leftClick')}
|
||||
on:middleclick={() => handleObjectClick(data, 'middleClick')}
|
||||
on:dblclick={() => handleObjectClick(data, 'dblClick')}
|
||||
|
||||
@@ -142,6 +142,18 @@
|
||||
label: 'Model transform file',
|
||||
};
|
||||
|
||||
const apps: FileTypeHandler = isProApp()
|
||||
? {
|
||||
icon: 'img app',
|
||||
format: 'json',
|
||||
tabComponent: 'AppEditorTab',
|
||||
folder: 'apps',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Application file',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
export const SAVED_FILE_HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
@@ -154,6 +166,7 @@
|
||||
modtrans,
|
||||
datadeploy,
|
||||
dbcompare,
|
||||
apps,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
@@ -179,6 +192,8 @@
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { saveFileToDisk } from '../utility/exportFileTools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { showSnackbarError } from '../utility/snackbar';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
|
||||
@@ -200,20 +215,30 @@
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
handler?.tabComponent && { text: 'Open', onClick: openTab },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Create copy', onClick: handleCopy },
|
||||
hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete },
|
||||
folder == 'markdown' && { text: 'Show page', onClick: showMarkdownPage },
|
||||
{ text: 'Download', onClick: handleDownload },
|
||||
handler?.tabComponent && { text: _t('common.open', { defaultMessage: 'Open' }), onClick: openTab },
|
||||
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: _t('common.createCopy', { defaultMessage: 'Create copy' }), onClick: handleCopy },
|
||||
!data.teamFileId && hasPermission(`files/${data.folder}/write`) && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
data.teamFileId && data.allowWrite && { text: _t('common.rename', { defaultMessage: 'Rename' }), onClick: handleRename },
|
||||
data.teamFileId &&
|
||||
data.allowRead &&
|
||||
hasPermission('all-team-files/create') && { text: _t('common.createCopy', { defaultMessage: 'Create copy' }), onClick: handleCopy },
|
||||
data.teamFileId && data.allowWrite && { text: _t('common.delete', { defaultMessage: 'Delete' }), onClick: handleDelete },
|
||||
|
||||
folder == 'markdown' && { text: _t('common.showPage', { defaultMessage: 'Show page' }), onClick: showMarkdownPage },
|
||||
!data.teamFileId && { text: _t('common.download', { defaultMessage: 'Download' }), onClick: handleDownload },
|
||||
data.teamFileId && data.allowRead && { text: _t('common.download', { defaultMessage: 'Download' }), onClick: handleDownload },
|
||||
];
|
||||
}
|
||||
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete file ${data.file}?`,
|
||||
message: _t('common.reallyDeleteFile', { defaultMessage: 'Really delete file {file}?', values: { file: data.file } }),
|
||||
onConfirm: () => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/delete', { teamFileId: data.teamFileId });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/delete-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -228,10 +253,12 @@
|
||||
const handleRename = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.file,
|
||||
label: 'New file name',
|
||||
header: 'Rename file',
|
||||
label: _t('common.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('common.renameFile', { defaultMessage: 'Rename file' }),
|
||||
onConfirm: newFile => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/update', { teamFileId: data.teamFileId, name: newFile });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/rename-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -247,10 +274,12 @@
|
||||
const handleCopy = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: data.file,
|
||||
label: 'New file name',
|
||||
header: 'Copy file',
|
||||
label: _t('savedFile.newFileName', { defaultMessage: 'New file name' }),
|
||||
header: _t('savedFile.copyFile', { defaultMessage: 'Copy file' }),
|
||||
onConfirm: newFile => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
apiCall('team-files/copy', { teamFileId: data.teamFileId, newName: newFile });
|
||||
} else if (data.folid && data.cntid) {
|
||||
apiCall('cloud/copy-file', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -266,7 +295,12 @@
|
||||
const handleDownload = () => {
|
||||
saveFileToDisk(
|
||||
async filePath => {
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
await apiCall('team-files/export-file', {
|
||||
teamFileId: data.teamFileId,
|
||||
filePath,
|
||||
});
|
||||
} else if (data.folid && data.cntid) {
|
||||
await apiCall('cloud/export-file', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -286,7 +320,23 @@
|
||||
|
||||
async function openTab() {
|
||||
let dataContent;
|
||||
if (data.folid && data.cntid) {
|
||||
if (data.teamFileId) {
|
||||
if (data?.metadata?.autoExecute) {
|
||||
if (!data.allowUse) {
|
||||
showSnackbarError(_t('savedFile.noPermissionUseTeamFile', { defaultMessage: 'You do not have permission to use this team file' }));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!data.allowRead) {
|
||||
showSnackbarError(_t('savedFile.noPermissionReadTeamFile', { defaultMessage: 'You do not have permission to read this team file' }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
const resp = await apiCall('team-files/get-content', {
|
||||
teamFileId: data.teamFileId,
|
||||
});
|
||||
dataContent = resp.content;
|
||||
} else if (data.folid && data.cntid) {
|
||||
const resp = await apiCall('cloud/get-content', {
|
||||
folid: data.folid,
|
||||
cntid: data.cntid,
|
||||
@@ -311,6 +361,11 @@
|
||||
tooltip = `${getConnectionLabel(connection)}\n${database}`;
|
||||
}
|
||||
|
||||
if (data?.metadata?.connectionId) {
|
||||
connProps.conid = data.metadata.connectionId;
|
||||
connProps.database = data.metadata.databaseName;
|
||||
}
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: data.file,
|
||||
@@ -323,6 +378,8 @@
|
||||
savedFormat: handler.format,
|
||||
savedCloudFolderId: data.folid,
|
||||
savedCloudContentId: data.cntid,
|
||||
savedTeamFileId: data.teamFileId,
|
||||
hideEditor: data.teamFileId && data?.metadata?.autoExecute && !data.allowRead,
|
||||
...connProps,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { __t } from '../translations';
|
||||
export function matchDatabaseObjectAppObject(obj1, obj2) {
|
||||
return (
|
||||
obj1?.objectTypeField == obj2?.objectTypeField &&
|
||||
@@ -11,12 +12,12 @@ export function matchDatabaseObjectAppObject(obj1, obj2) {
|
||||
function getTableLikeActions(dataTab) {
|
||||
return [
|
||||
{
|
||||
label: 'Open data',
|
||||
label: __t('dbObject.openData', { defaultMessage: 'Open data' }),
|
||||
tab: dataTab,
|
||||
defaultActionId: 'openTable',
|
||||
},
|
||||
{
|
||||
label: 'Open raw data',
|
||||
label: __t('dbObject.openRawData', { defaultMessage: 'Open raw data' }),
|
||||
tab: dataTab,
|
||||
defaultActionId: 'openRawTable',
|
||||
isRawMode: true,
|
||||
@@ -33,13 +34,13 @@ function getTableLikeActions(dataTab) {
|
||||
// defaultActionId: 'openForm',
|
||||
// },
|
||||
{
|
||||
label: 'Open structure',
|
||||
label: __t('dbObject.openStructure', { defaultMessage: 'Open structure' }),
|
||||
tab: 'TableStructureTab',
|
||||
icon: 'img table-structure',
|
||||
defaultActionId: 'openStructure',
|
||||
},
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -53,7 +54,7 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
matviews: getTableLikeActions('ViewDataTab'),
|
||||
procedures: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -61,7 +62,7 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -69,7 +70,7 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
triggers: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
@@ -77,12 +78,12 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
collections: [
|
||||
{
|
||||
label: 'Open data',
|
||||
label: __t('dbObject.openData', { defaultMessage: 'Open data' }),
|
||||
tab: 'CollectionDataTab',
|
||||
defaultActionId: 'openTable',
|
||||
},
|
||||
{
|
||||
label: 'Open JSON',
|
||||
label: __t('dbObject.openJson', { defaultMessage: 'Open JSON' }),
|
||||
tab: 'CollectionDataTab',
|
||||
defaultActionId: 'openJson',
|
||||
initialData: {
|
||||
@@ -94,10 +95,18 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
],
|
||||
schedulerEvents: [
|
||||
{
|
||||
label: 'Show SQL',
|
||||
label: __t('dbObject.showSql', { defaultMessage: 'Show SQL' }),
|
||||
tab: 'SqlObjectTab',
|
||||
defaultActionId: 'showSql',
|
||||
icon: 'img sql-file',
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
{
|
||||
label: __t('dbObject.showQuery', { defaultMessage: 'Show query' }),
|
||||
tab: 'QueryDataTab',
|
||||
defaultActionId: 'showAppQuery',
|
||||
icon: 'img app-query',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
46
packages/web/src/buttons/CtaButton.svelte
Normal file
46
packages/web/src/buttons/CtaButton.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
export let disabled = false;
|
||||
export let title = null;
|
||||
|
||||
let domButton;
|
||||
|
||||
export function getBoundingClientRect() {
|
||||
return domButton.getBoundingClientRect();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
class="cta-button"
|
||||
{title}
|
||||
{disabled}
|
||||
on:click
|
||||
bind:this={domButton}
|
||||
data-testid={$$props['data-testid']}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
|
||||
<style>
|
||||
.cta-button {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
color: var(--theme-font-link);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.cta-button:hover:not(:disabled) {
|
||||
color: var(--theme-font-hover);
|
||||
}
|
||||
|
||||
.cta-button:disabled {
|
||||
color: var(--theme-font-3);
|
||||
cursor: not-allowed;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -9,6 +9,7 @@
|
||||
export let title = null;
|
||||
export let skipWidth = false;
|
||||
export let outline = false;
|
||||
export let colorClass = '';
|
||||
|
||||
function handleClick() {
|
||||
if (!disabled) dispatch('click');
|
||||
@@ -31,6 +32,8 @@
|
||||
bind:this={domButton}
|
||||
class:skipWidth
|
||||
class:outline
|
||||
class={colorClass}
|
||||
class:setBackgroundColor={!colorClass}
|
||||
/>
|
||||
|
||||
<style>
|
||||
@@ -38,19 +41,26 @@
|
||||
border: 1px solid var(--theme-bg-button-inv-2);
|
||||
padding: 5px;
|
||||
margin: 2px;
|
||||
background-color: var(--theme-bg-button-inv);
|
||||
color: var(--theme-font-inv-1);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.setBackgroundColor {
|
||||
background-color: var(--theme-bg-button-inv);
|
||||
}
|
||||
|
||||
input:not(.skipWidth) {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
input:hover:not(.disabled):not(.outline) {
|
||||
input:not(.setBackgroundColor) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input.setBackgroundColor:hover:not(.disabled):not(.outline) {
|
||||
background-color: var(--theme-bg-button-inv-2);
|
||||
}
|
||||
input:active:not(.disabled):not(.outline) {
|
||||
input.setBackgroundColor:active:not(.disabled):not(.outline) {
|
||||
background-color: var(--theme-bg-button-inv-3);
|
||||
}
|
||||
input.disabled {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
export let square = false;
|
||||
export let narrow = false;
|
||||
export let title = null;
|
||||
export let inlineBlock=false;
|
||||
|
||||
let domButton;
|
||||
|
||||
@@ -17,6 +18,7 @@
|
||||
class:disabled
|
||||
class:square
|
||||
class:narrow
|
||||
class:inlineBlock
|
||||
on:click
|
||||
bind:this={domButton}
|
||||
data-testid={$$props['data-testid']}
|
||||
@@ -71,4 +73,8 @@
|
||||
.square {
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.inlineBlock {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import getElectron from '../utility/getElectron';
|
||||
import InlineButtonLabel from '../buttons/InlineButtonLabel.svelte';
|
||||
import resolveApi, { resolveApiHeaders } from '../utility/resolveApi';
|
||||
import { _t } from '../translations';
|
||||
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
@@ -49,11 +50,11 @@
|
||||
</script>
|
||||
|
||||
{#if electron}
|
||||
<InlineButton on:click={handleOpenElectronFile} title="Open file" data-testid={$$props['data-testid']}>
|
||||
<InlineButton on:click={handleOpenElectronFile} title={_t('files.openFile', { defaultMessage: "Open file" })} data-testid={$$props['data-testid']}>
|
||||
<FontIcon {icon} />
|
||||
</InlineButton>
|
||||
{:else}
|
||||
<InlineButtonLabel on:click={() => {}} title="Upload file" data-testid={$$props['data-testid']} htmlFor={inputId}>
|
||||
<InlineButtonLabel on:click={() => {}} title={_t('files.uploadFile', { defaultMessage: "Upload file" })} data-testid={$$props['data-testid']} htmlFor={inputId}>
|
||||
<FontIcon {icon} />
|
||||
</InlineButtonLabel>
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let icon;
|
||||
export let title;
|
||||
@@ -21,7 +22,7 @@
|
||||
data-testid={$$props['data-testid']}
|
||||
title={disabled
|
||||
? isProFeature && !isProApp()
|
||||
? 'This feature is available only in DbGate Premium'
|
||||
? _t('common.featurePremium', { defaultMessage: 'This feature is available only in DbGate Premium' })
|
||||
: disabledMessage
|
||||
: undefined}
|
||||
>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script context="module">
|
||||
function getCommandTitle(command) {
|
||||
let res = command.text;
|
||||
let res = _tval(command.text);
|
||||
if (command.keyText || command.keyTextFromGroup) {
|
||||
res += ` (${formatKeyText(command.keyText || command.keyTextFromGroup)})`;
|
||||
}
|
||||
@@ -12,6 +12,8 @@
|
||||
import { commandsCustomized } from '../stores';
|
||||
import { formatKeyText } from '../utility/common';
|
||||
import ToolStripButton from './ToolStripButton.svelte';
|
||||
import _ from 'lodash';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let command;
|
||||
export let component = ToolStripButton;
|
||||
@@ -32,6 +34,6 @@
|
||||
{iconAfter}
|
||||
{...$$restProps}
|
||||
>
|
||||
{buttonLabel || cmd.toolbarName || cmd.name}
|
||||
{(_tval(buttonLabel) || _tval(cmd?.toolbarName) || _tval(cmd?.name))}
|
||||
</svelte:component>
|
||||
{/if}
|
||||
|
||||
@@ -4,15 +4,17 @@
|
||||
|
||||
const thisInstance = get_current_component();
|
||||
|
||||
export const activator = createActivator('ToolStripContainer', true);
|
||||
|
||||
$: isComponentActive = $isComponentActiveStore('ToolStripContainer', thisInstance);
|
||||
export let showAlways = false;
|
||||
export const activator = showAlways ? null : createActivator('ToolStripContainer', true);
|
||||
|
||||
export function activate() {
|
||||
activator?.activate();
|
||||
}
|
||||
|
||||
export let scrollContent = false;
|
||||
export let hideToolStrip = false;
|
||||
|
||||
$: isComponentActive = showAlways || ($isComponentActiveStore('ToolStripContainer', thisInstance) && !hideToolStrip);
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:component this={component} {title} {icon} on:click={handleClick}>
|
||||
<svelte:component this={component} {title} {icon} on:click={handleClick} {...$$restProps}>
|
||||
{label}
|
||||
<FontIcon icon="icon chevron-down" />
|
||||
</svelte:component>
|
||||
|
||||
@@ -23,7 +23,8 @@
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import ToolStripCommandButton from './ToolStripCommandButton.svelte';
|
||||
import ToolStripDropDownButton from './ToolStripDropDownButton.svelte';
|
||||
|
||||
import _ from 'lodash';
|
||||
import { _tval } from '../translations';
|
||||
export let quickExportHandlerRef = null;
|
||||
export let command = 'sqlDataGrid.export';
|
||||
export let label = 'Export';
|
||||
@@ -39,7 +40,7 @@
|
||||
|
||||
{#if hasPermission('dbops/export')}
|
||||
{#if quickExportHandlerRef}
|
||||
<ToolStripDropDownButton menu={getExportMenu} {label} icon="icon export" />
|
||||
<ToolStripDropDownButton menu={getExportMenu} label={_tval(label)} icon="icon export" />
|
||||
{:else}
|
||||
<ToolStripCommandButton {command} />
|
||||
{/if}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import FormStyledButtonLikeLabel from '../buttons/FormStyledButtonLikeLabel.svelte';
|
||||
import uploadFiles from '../utility/uploadFiles';
|
||||
import { _t } from '../translations';
|
||||
|
||||
const handleChange = e => {
|
||||
const files = [...e.target.files];
|
||||
@@ -9,6 +10,6 @@
|
||||
</script>
|
||||
|
||||
<div class="m-1">
|
||||
<FormStyledButtonLikeLabel htmlFor="uploadFileButton">Upload file</FormStyledButtonLikeLabel>
|
||||
<FormStyledButtonLikeLabel htmlFor="uploadFileButton">{_t('files.uploadFile', { defaultMessage: "Upload file" })}</FormStyledButtonLikeLabel>
|
||||
<input type="file" id="uploadFileButton" hidden on:change={handleChange} />
|
||||
</div>
|
||||
|
||||
398
packages/web/src/celldata/FormCellView.svelte
Normal file
398
packages/web/src/celldata/FormCellView.svelte
Normal file
@@ -0,0 +1,398 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { tick } from 'svelte';
|
||||
import CellValue from '../datagrid/CellValue.svelte';
|
||||
import { isJsonLikeLongString, safeJsonParse, parseCellValue, stringifyCellValue, filterName } from 'dbgate-tools';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import createRef from '../utility/createRef';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import EditCellDataModal from '../modals/EditCellDataModal.svelte';
|
||||
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||
import { _t } from '../translations';
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import JSONTree from '../jsontree/JSONTree.svelte';
|
||||
import Link from '../elements/Link.svelte';
|
||||
|
||||
export let selection;
|
||||
|
||||
$: firstSelection = selection?.[0];
|
||||
$: rowData = firstSelection?.rowData;
|
||||
$: editable = firstSelection?.editable;
|
||||
$: editorTypes = firstSelection?.editorTypes;
|
||||
$: displayColumns = firstSelection?.displayColumns || [];
|
||||
$: realColumnUniqueNames = firstSelection?.realColumnUniqueNames || [];
|
||||
$: grider = firstSelection?.grider;
|
||||
|
||||
$: uniqueRows = _.uniqBy(selection || [], 'row');
|
||||
$: isMultipleRows = uniqueRows.length > 1;
|
||||
|
||||
function areValuesEqual(val1, val2) {
|
||||
if (val1 === val2) return true;
|
||||
if (val1 == null && val2 == null) return true;
|
||||
if (val1 == null || val2 == null) return false;
|
||||
return _.isEqual(val1, val2);
|
||||
}
|
||||
|
||||
function getFieldValue(colName) {
|
||||
if (!isMultipleRows) return { value: rowData?.[colName], hasMultipleValues: false };
|
||||
|
||||
const values = uniqueRows.map(sel => sel.rowData?.[colName]);
|
||||
const firstValue = values[0];
|
||||
const allSame = values.every(v => areValuesEqual(v, firstValue));
|
||||
|
||||
return allSame ? { value: firstValue, hasMultipleValues: false } : { value: null, hasMultipleValues: true };
|
||||
}
|
||||
|
||||
let filter = '';
|
||||
let notNull = getLocalStorage('dataGridCellDataFormNotNull') === 'true';
|
||||
|
||||
$: orderedFields = realColumnUniqueNames
|
||||
.map(colName => {
|
||||
const col = displayColumns.find(c => c.uniqueName === colName);
|
||||
if (!col) return null;
|
||||
const { value, hasMultipleValues } = getFieldValue(colName);
|
||||
return {
|
||||
...col,
|
||||
value,
|
||||
hasMultipleValues,
|
||||
// columnName: col.columnName || colName,
|
||||
// uniqueName: colName,
|
||||
// value,
|
||||
// hasMultipleValues,
|
||||
// col,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
$: filteredFields = orderedFields
|
||||
.filter(field => filterName(filter, field.columnName))
|
||||
.filter(field => {
|
||||
if (notNull) {
|
||||
return field.value != null || field.hasMultipleValues;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
let editingColumn = null;
|
||||
let editValue = '';
|
||||
let domEditor = null;
|
||||
const isChangedRef = createRef(false);
|
||||
|
||||
function isJsonValue(value) {
|
||||
if (
|
||||
_.isPlainObject(value) &&
|
||||
!(value?.type == 'Buffer' && _.isArray(value.data)) &&
|
||||
!value.$oid &&
|
||||
!value.$bigint &&
|
||||
!value.$decimal
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (_.isArray(value)) return true;
|
||||
if (typeof value !== 'string') return false;
|
||||
if (!isJsonLikeLongString(value)) return false;
|
||||
const parsed = safeJsonParse(value);
|
||||
return parsed !== null && (_.isPlainObject(parsed) || _.isArray(parsed));
|
||||
}
|
||||
|
||||
function getJsonObject(value) {
|
||||
if (_.isPlainObject(value) || _.isArray(value)) return value;
|
||||
if (typeof value === 'string') return safeJsonParse(value);
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleClick(field) {
|
||||
if (!editable || !grider) return;
|
||||
if (isJsonValue(field.value)) return;
|
||||
// if (isJsonValue(field.value) && !field.hasMultipleValues) {
|
||||
// openEditModal(field);
|
||||
// return;
|
||||
// }
|
||||
startEditing(field);
|
||||
}
|
||||
|
||||
function handleDoubleClick(field) {
|
||||
if (!editable || !grider) return;
|
||||
if (isJsonValue(field.value) && !field.hasMultipleValues) {
|
||||
openEditModal(field);
|
||||
return;
|
||||
}
|
||||
startEditing(field);
|
||||
}
|
||||
|
||||
function startEditing(field) {
|
||||
if (!editable || !grider) return;
|
||||
editingColumn = field.uniqueName;
|
||||
editValue = field.hasMultipleValues ? '' : stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value;
|
||||
isChangedRef.set(false);
|
||||
tick().then(() => {
|
||||
if (!domEditor) return;
|
||||
domEditor.focus();
|
||||
if (!field.hasMultipleValues) domEditor.select();
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeyDown(event, field) {
|
||||
switch (event.keyCode) {
|
||||
case keycodes.escape:
|
||||
isChangedRef.set(false);
|
||||
editingColumn = null;
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
event.preventDefault();
|
||||
break;
|
||||
case keycodes.tab:
|
||||
case keycodes.upArrow:
|
||||
case keycodes.downArrow:
|
||||
const reverse = event.keyCode === keycodes.upArrow || (event.keyCode === keycodes.tab && event.shiftKey);
|
||||
event.preventDefault();
|
||||
moveToNextField(field, reverse);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function moveToNextField(field, reverse) {
|
||||
const currentIndex = filteredFields.findIndex(f => f.uniqueName === field.uniqueName);
|
||||
const nextIndex = reverse ? currentIndex - 1 : currentIndex + 1;
|
||||
const nextField = filteredFields[nextIndex];
|
||||
if (!nextField) return;
|
||||
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
if (nextIndex < 0 || nextIndex >= filteredFields.length) return;
|
||||
|
||||
tick().then(() => {
|
||||
startEditing(nextField);
|
||||
// if (isJsonValue(nextField.value)) {
|
||||
// openEditModal(nextField);
|
||||
// } else {
|
||||
// startEditing(nextField);
|
||||
// }
|
||||
});
|
||||
}
|
||||
|
||||
function handleSearchKeyDown(e) {
|
||||
if (e.keyCode === keycodes.backspace && (e.metaKey || e.ctrlKey)) {
|
||||
filter = '';
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function handleBlur(field) {
|
||||
if (isChangedRef.get()) {
|
||||
saveValue(field);
|
||||
}
|
||||
editingColumn = null;
|
||||
}
|
||||
|
||||
function setCellValue(fieldName, value) {
|
||||
if (!grider) return;
|
||||
|
||||
if (selection.length > 0) {
|
||||
const uniqueRowIndices = _.uniq(selection.map(x => x.row));
|
||||
grider.beginUpdate();
|
||||
for (const row of uniqueRowIndices) {
|
||||
grider.setCellValue(row, fieldName, value);
|
||||
}
|
||||
grider.endUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
function saveValue(field) {
|
||||
if (!grider) return;
|
||||
const parsedValue = parseCellValue(editValue, editorTypes);
|
||||
setCellValue(field.uniqueName, parsedValue);
|
||||
isChangedRef.set(false);
|
||||
}
|
||||
|
||||
function openEditModal(field) {
|
||||
if (!grider) return;
|
||||
showModal(EditCellDataModal, {
|
||||
value: field.value,
|
||||
dataEditorTypesBehaviour: editorTypes,
|
||||
onSave: value => setCellValue(field.uniqueName, value),
|
||||
});
|
||||
}
|
||||
|
||||
function getJsonParsedValue(value) {
|
||||
if (editorTypes?.explicitDataType) return null;
|
||||
if (!isJsonLikeLongString(value)) return null;
|
||||
return safeJsonParse(value);
|
||||
}
|
||||
|
||||
function handleEdit(field) {
|
||||
editingColumn = null;
|
||||
openEditModal(field);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="outer">
|
||||
<div class="content">
|
||||
{#if rowData}
|
||||
<div class="search-wrapper" on:keydown={handleSearchKeyDown}>
|
||||
<SearchBoxWrapper noMargin>
|
||||
<SearchInput
|
||||
placeholder={_t('tableCell.filterColumns', { defaultMessage: 'Filter columns' })}
|
||||
bind:value={filter}
|
||||
/>
|
||||
<CloseSearchButton bind:filter />
|
||||
</SearchBoxWrapper>
|
||||
<CheckboxField
|
||||
defaultChecked={notNull}
|
||||
on:change={e => {
|
||||
// @ts-ignore
|
||||
notNull = e.target.checked;
|
||||
setLocalStorage('dataGridCellDataFormNotNull', notNull ? 'true' : 'false');
|
||||
}}
|
||||
/>
|
||||
{_t('tableCell.hideNullValues', { defaultMessage: 'Hide NULL values' })}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="inner">
|
||||
{#if !rowData}
|
||||
<div class="no-data">{_t('tableCell.noDataSelected', { defaultMessage: 'No data selected' })}</div>
|
||||
{:else}
|
||||
{#each filteredFields as field (field.uniqueName)}
|
||||
<div class="field">
|
||||
<div class="field-name">
|
||||
<ColumnLabel {...field} showDataType /><Link onClick={() => handleEdit(field)}
|
||||
>{_t('tableCell.edit', { defaultMessage: 'Edit' })}
|
||||
</Link>
|
||||
</div>
|
||||
<div class="field-value" class:editable on:click={() => handleClick(field)}>
|
||||
{#if editingColumn === field.uniqueName}
|
||||
<div class="editor-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
bind:this={domEditor}
|
||||
bind:value={editValue}
|
||||
on:input={() => isChangedRef.set(true)}
|
||||
on:keydown={e => handleKeyDown(e, field)}
|
||||
on:blur={() => handleBlur(field)}
|
||||
class="inline-editor"
|
||||
/>
|
||||
</div>
|
||||
{:else if field.hasMultipleValues}
|
||||
<span class="multiple-values"
|
||||
>({_t('tableCell.multipleValues', { defaultMessage: 'Multiple values' })})</span
|
||||
>
|
||||
{:else if isJsonValue(field.value)}
|
||||
<JSONTree value={getJsonParsedValue(field.value)} />
|
||||
{:else}
|
||||
<CellValue
|
||||
{rowData}
|
||||
value={field.value}
|
||||
jsonParsedValue={getJsonParsedValue(field.value)}
|
||||
{editorTypes}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.outer {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-wrapper {
|
||||
padding: 4px 4px 0 4px;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--theme-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.inner {
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.field-name {
|
||||
background: var(--theme-bg-1);
|
||||
padding: 4px 8px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: var(--theme-font-2);
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.field-value {
|
||||
padding: 6px 8px;
|
||||
background: var(--theme-bg-0);
|
||||
min-height: 20px;
|
||||
word-break: break-all;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.field-value.editable {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
.editor-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inline-editor {
|
||||
flex: 1;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: var(--theme-bg-0);
|
||||
color: var(--theme-font-1);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.inline-editor:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.multiple-values {
|
||||
color: var(--theme-font-3);
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,9 @@
|
||||
if (value?.type == 'Buffer' && _.isArray(value?.data)) {
|
||||
return 'data:image/png;base64, ' + btoa(String.fromCharCode.apply(null, value?.data));
|
||||
}
|
||||
if (value?.$binary?.base64) {
|
||||
return 'data:image/png;base64, ' + value.$binary.base64;
|
||||
}
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.log('Error showing picture', err);
|
||||
|
||||
@@ -3,12 +3,21 @@
|
||||
|
||||
export let selection;
|
||||
export let wrap;
|
||||
|
||||
$: singleSelection = selection?.length == 1 && selection?.[0];
|
||||
$: grider = singleSelection?.grider;
|
||||
$: editable = grider?.editable ?? false;
|
||||
|
||||
function setCellValue(value) {
|
||||
if (!editable) return;
|
||||
grider.setCellValue(singleSelection.row, singleSelection.column, value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<textarea
|
||||
class="flex1"
|
||||
{wrap}
|
||||
readonly
|
||||
readonly={!editable}
|
||||
value={selection
|
||||
.map(cell => {
|
||||
const { value } = cell;
|
||||
@@ -16,4 +25,5 @@
|
||||
return cell.value;
|
||||
})
|
||||
.join('\n')}
|
||||
on:input={e => setCellValue(e.target['value'])}
|
||||
/>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script context="module">
|
||||
registerCommand({
|
||||
id: 'commandPalette.show',
|
||||
category: 'Command palette',
|
||||
name: 'Show',
|
||||
toolbarName: 'Command palette',
|
||||
category: __t('command.commandPalette', { defaultMessage: 'Command palette' }),
|
||||
name: __t('command.commandPalette.show', { defaultMessage: 'Show' }),
|
||||
toolbarName: __t('command.commandPalette', { defaultMessage: 'Command palette' }),
|
||||
toolbarOrder: 0,
|
||||
keyText: 'F1',
|
||||
toolbar: true,
|
||||
@@ -15,9 +15,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'database.search',
|
||||
category: 'Database',
|
||||
toolbarName: 'Database search',
|
||||
name: 'Search',
|
||||
category: __t('command.database', { defaultMessage: 'Database' }),
|
||||
toolbarName: __t('command.database.databaseSearch', { defaultMessage: 'Database search' }),
|
||||
name: __t('command.database.search', { defaultMessage: 'Search' }),
|
||||
keyText: isElectronAvailable() ? 'CtrlOrCommand+P' : 'F3',
|
||||
onClick: () => visibleCommandPalette.set('database'),
|
||||
testEnabled: () => getVisibleCommandPalette() != 'database',
|
||||
@@ -81,6 +81,7 @@
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
import registerCommand from './registerCommand';
|
||||
import { formatKeyText, switchCurrentDatabase } from '../utility/common';
|
||||
import { _tval, __t, _t } from '../translations';
|
||||
|
||||
let domInput;
|
||||
let filter = '';
|
||||
@@ -113,11 +114,11 @@
|
||||
($visibleCommandPalette == 'database'
|
||||
? extractDbItems($databaseInfo, { conid, database }, $connectionList)
|
||||
: parentCommand
|
||||
? parentCommand.getSubCommands()
|
||||
: sortedComands
|
||||
? parentCommand.getSubCommands()
|
||||
: sortedComands
|
||||
).filter(x => !x.isGroupCommand),
|
||||
{
|
||||
extract: x => x.text,
|
||||
extract: x => _tval(x.text),
|
||||
pre: '<b>',
|
||||
post: '</b>',
|
||||
}
|
||||
@@ -162,10 +163,10 @@
|
||||
on:clickOutside={() => {
|
||||
$visibleCommandPalette = null;
|
||||
}}
|
||||
data-testid='CommandPalette_main'
|
||||
data-testid="CommandPalette_main"
|
||||
>
|
||||
<div
|
||||
class="overlay"
|
||||
<div
|
||||
class="overlay"
|
||||
on:click={() => {
|
||||
$visibleCommandPalette = null;
|
||||
}}
|
||||
@@ -180,7 +181,7 @@
|
||||
domInput.focus();
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon menu" /> Commands
|
||||
<FontIcon icon="icon menu" /> {_t('commandPalette.commands', { defaultMessage: 'Commands' })}
|
||||
</div>
|
||||
<div
|
||||
class="page"
|
||||
@@ -190,7 +191,7 @@
|
||||
domInput.focus();
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon database" /> Database
|
||||
<FontIcon icon="icon database" /> {_t('common.database', { defaultMessage: 'Database' })}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mainInner">
|
||||
@@ -200,8 +201,8 @@
|
||||
bind:this={domInput}
|
||||
bind:value={filter}
|
||||
on:keydown={handleKeyDown}
|
||||
placeholder={parentCommand?.text ||
|
||||
($visibleCommandPalette == 'database' ? 'Search in database' : 'Search in commands')}
|
||||
placeholder={_tval(parentCommand?.text) ||
|
||||
($visibleCommandPalette == 'database' ? _t('commandPalette.searchInDatabase', { defaultMessage: 'Search in database' }) : _t('commandPalette.searchInCommands', { defaultMessage: 'Search in commands' }))}
|
||||
/>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import _ from 'lodash';
|
||||
import { currentDatabase, getCurrentDatabase } from '../stores';
|
||||
import { currentDatabase, getCurrentDatabase, getExtensions } from '../stores';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import registerCommand from './registerCommand';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { switchCurrentDatabase } from '../utility/common';
|
||||
import { getDatabasStatusMenu, switchCurrentDatabase } from '../utility/common';
|
||||
import { __t } from '../translations';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
|
||||
registerCommand({
|
||||
id: 'database.changeState',
|
||||
category: 'Database',
|
||||
name: 'Change status',
|
||||
category: __t('command.database', { defaultMessage: 'Database' }),
|
||||
name: __t('command.database.changeStatus', { defaultMessage: 'Change status' }),
|
||||
getSubCommands: () => {
|
||||
const current = getCurrentDatabase();
|
||||
if (!current) return [];
|
||||
@@ -17,33 +19,8 @@ registerCommand({
|
||||
conid: connection._id,
|
||||
database: name,
|
||||
};
|
||||
return [
|
||||
{
|
||||
text: 'Sync model (incremental)',
|
||||
onClick: () => {
|
||||
apiCall('database-connections/sync-model', dbid);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Sync model (full)',
|
||||
onClick: () => {
|
||||
apiCall('database-connections/sync-model', { ...dbid, isFullRefresh: true });
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Reopen',
|
||||
onClick: () => {
|
||||
apiCall('database-connections/refresh', dbid);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Disconnect',
|
||||
onClick: () => {
|
||||
const electron = getElectron();
|
||||
if (electron) apiCall('database-connections/disconnect', dbid);
|
||||
switchCurrentDatabase(null);
|
||||
},
|
||||
},
|
||||
];
|
||||
const driver = findEngineDriver(connection, getExtensions());
|
||||
|
||||
return getDatabasStatusMenu(dbid, driver);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { recentDatabases, currentDatabase, getRecentDatabases } from '../stores'
|
||||
import registerCommand from './registerCommand';
|
||||
import { getConnectionLabel } from 'dbgate-tools';
|
||||
import { switchCurrentDatabase } from '../utility/common';
|
||||
import { __t } from '../translations';
|
||||
|
||||
currentDatabase.subscribe(value => {
|
||||
if (!value) return;
|
||||
@@ -24,9 +25,9 @@ function switchDatabaseCommand(db) {
|
||||
|
||||
registerCommand({
|
||||
id: 'database.switch',
|
||||
category: 'Database',
|
||||
name: 'Change to recent',
|
||||
menuName: 'Switch recent database',
|
||||
category: __t('command.database', { defaultMessage: 'Database' }),
|
||||
name: __t('command.database.changeRecent', { defaultMessage: 'Change to recent' }),
|
||||
menuName: __t('command.database.switchRecent', { defaultMessage: 'Switch recent database' }),
|
||||
keyText: 'CtrlOrCommand+D',
|
||||
getSubCommands: () => getRecentDatabases().map(switchDatabaseCommand),
|
||||
});
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { commands } from '../stores';
|
||||
import { invalidateCommandDefinitions } from './invalidateCommands';
|
||||
import _ from 'lodash';
|
||||
import { _tval, DefferedTranslationResult, isDefferedTranslationResult } from '../translations';
|
||||
|
||||
export interface SubCommand {
|
||||
text: string;
|
||||
@@ -8,10 +10,10 @@ export interface SubCommand {
|
||||
|
||||
export interface GlobalCommand {
|
||||
id: string;
|
||||
category: string; // null for group commands
|
||||
category: string | DefferedTranslationResult; // null for group commands
|
||||
isGroupCommand?: boolean;
|
||||
name: string;
|
||||
text?: string /* category: name */;
|
||||
name: string | DefferedTranslationResult;
|
||||
text?: string | DefferedTranslationResult;
|
||||
keyText?: string;
|
||||
keyTextFromGroup?: string; // automatically filled from group
|
||||
group?: string;
|
||||
@@ -23,11 +25,11 @@ export interface GlobalCommand {
|
||||
toolbar?: boolean;
|
||||
enabled?: boolean;
|
||||
showDisabled?: boolean;
|
||||
toolbarName?: string;
|
||||
menuName?: string;
|
||||
toolbarName?: string | DefferedTranslationResult;
|
||||
menuName?: string | DefferedTranslationResult;
|
||||
toolbarOrder?: number;
|
||||
disableHandleKeyText?: string;
|
||||
isRelatedToTab?: boolean,
|
||||
isRelatedToTab?: boolean;
|
||||
systemCommand?: boolean;
|
||||
}
|
||||
|
||||
@@ -41,7 +43,12 @@ export default function registerCommand(command: GlobalCommand) {
|
||||
return {
|
||||
...x,
|
||||
[command.id]: {
|
||||
text: `${command.category}: ${command.name}`,
|
||||
text:
|
||||
isDefferedTranslationResult(command.category) || isDefferedTranslationResult(command.name)
|
||||
? {
|
||||
_transCallback: () => `${_tval(command.category)}: ${_tval(command.name)}`,
|
||||
}
|
||||
: `${command.category}: ${command.name}`,
|
||||
...command,
|
||||
enabled: !testEnabled,
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
import { getStringSettingsValue } from '../settings/settingsTools';
|
||||
import { stringifyCellValue } from 'dbgate-tools';
|
||||
|
||||
export let rowData;
|
||||
@@ -13,7 +13,7 @@
|
||||
value,
|
||||
'gridCellIntent',
|
||||
editorTypes,
|
||||
{ useThousandsSeparator: getBoolSettingsValue('dataGrid.thousandsSeparator', false) },
|
||||
{ thousandsSeparator: getStringSettingsValue('dataGrid.thousandsSeparatorChar', 'none') },
|
||||
jsonParsedValue
|
||||
);
|
||||
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionDataGrid.openQuery',
|
||||
category: 'Data grid',
|
||||
name: 'Open query',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.openQuery', { defaultMessage: 'Open query' }),
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().openQuery(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionDataGrid.export',
|
||||
category: 'Data grid',
|
||||
name: 'Export',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
|
||||
keyText: 'CtrlOrCommand+E',
|
||||
icon: 'icon export',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
@@ -140,6 +140,7 @@
|
||||
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
|
||||
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import { __t } from '../translations';
|
||||
|
||||
export let conid;
|
||||
export let display;
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import DefineDictionaryDescriptionModal from '../modals/DefineDictionaryDescriptionModal.svelte';
|
||||
import { sleep } from '../utility/common';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
|
||||
export let column;
|
||||
export let conid = undefined;
|
||||
@@ -24,6 +27,7 @@
|
||||
export let allowDefineVirtualReferences = false;
|
||||
export let setGrouping;
|
||||
export let seachInColumns = '';
|
||||
export let onReload = undefined;
|
||||
|
||||
const openReferencedTable = () => {
|
||||
openDatabaseObjectDetail('TableDataTab', null, {
|
||||
@@ -45,6 +49,19 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleCustomizeDescriptions = () => {
|
||||
showModal(DefineDictionaryDescriptionModal, {
|
||||
conid,
|
||||
database,
|
||||
schemaName: column.foreignKey.refSchemaName,
|
||||
pureName: column.foreignKey.refTableName,
|
||||
onConfirm: async () => {
|
||||
await sleep(100);
|
||||
onReload?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function getMenu() {
|
||||
return [
|
||||
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
@@ -56,26 +73,35 @@
|
||||
|
||||
column.foreignKey && [{ divider: true }, { onClick: openReferencedTable, text: column.foreignKey.refTableName }],
|
||||
|
||||
setGrouping && { divider: true },
|
||||
setGrouping && { onClick: () => setGrouping('GROUP'), text: 'Group by' },
|
||||
setGrouping && { onClick: () => setGrouping('MAX'), text: 'MAX' },
|
||||
setGrouping && { onClick: () => setGrouping('MIN'), text: 'MIN' },
|
||||
setGrouping && { onClick: () => setGrouping('SUM'), text: 'SUM' },
|
||||
setGrouping && { onClick: () => setGrouping('AVG'), text: 'AVG' },
|
||||
setGrouping && { onClick: () => setGrouping('COUNT'), text: 'COUNT' },
|
||||
setGrouping && { onClick: () => setGrouping('COUNT DISTINCT'), text: 'COUNT DISTINCT' },
|
||||
isProApp() &&
|
||||
setGrouping && [
|
||||
{ divider: true },
|
||||
{ onClick: () => setGrouping('GROUP'), text: 'Group by' },
|
||||
{ onClick: () => setGrouping('MAX'), text: 'MAX' },
|
||||
{ onClick: () => setGrouping('MIN'), text: 'MIN' },
|
||||
{ onClick: () => setGrouping('SUM'), text: 'SUM' },
|
||||
{ onClick: () => setGrouping('AVG'), text: 'AVG' },
|
||||
{ onClick: () => setGrouping('COUNT'), text: 'COUNT' },
|
||||
{ onClick: () => setGrouping('COUNT DISTINCT'), text: 'COUNT DISTINCT' },
|
||||
],
|
||||
|
||||
isTypeDateTime(column.dataType) && [
|
||||
{ divider: true },
|
||||
{ onClick: () => setGrouping('GROUP:YEAR'), text: 'Group by YEAR' },
|
||||
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
|
||||
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
||||
],
|
||||
isProApp() &&
|
||||
isTypeDateTime(column.dataType) && [
|
||||
{ divider: true },
|
||||
{ onClick: () => setGrouping('GROUP:YEAR'), text: 'Group by YEAR' },
|
||||
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
|
||||
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
||||
],
|
||||
|
||||
allowDefineVirtualReferences && [
|
||||
{ divider: true },
|
||||
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||
],
|
||||
{ divider: true },
|
||||
|
||||
isProApp() &&
|
||||
allowDefineVirtualReferences && { onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||
column.foreignKey &&
|
||||
isProApp() && {
|
||||
onClick: handleCustomizeDescriptions,
|
||||
text: 'Customize description',
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
@@ -111,7 +137,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
<DropDownButton menu={getMenu} narrow />
|
||||
<DropDownButton menu={getMenu} narrow data-testid={`ColumnHeaderControl_dropdown_${column?.uniqueName}`} />
|
||||
<div class="horizontal-split-handle resizeHandleControl" use:splitterDrag={'clientX'} on:resizeSplitter />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import ColumnEditorModal from '../tableeditor/ColumnEditorModal.svelte';
|
||||
import { tick } from 'svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let managerSize;
|
||||
export let display: GridDisplay;
|
||||
@@ -154,8 +155,8 @@
|
||||
class="colmode"
|
||||
value={isDynamicStructure ? 'variable' : 'fixed'}
|
||||
options={[
|
||||
{ label: 'Fixed columns (like SQL)', value: 'fixed' },
|
||||
{ label: 'Variable columns (like MongoDB)', value: 'variable' },
|
||||
{ label: _t('column.fixed', {defaultMessage: 'Fixed columns (like SQL)'}), value: 'fixed' },
|
||||
{ label: _t('column.variable', {defaultMessage: 'Variable columns (like MongoDB)'}) , value: 'variable' },
|
||||
]}
|
||||
on:change={e => {
|
||||
dispatchChangeSet({
|
||||
@@ -175,7 +176,7 @@
|
||||
{/if}
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput
|
||||
placeholder="Search columns"
|
||||
placeholder={_t('column.search', {defaultMessage: 'Search columns'})}
|
||||
value={currentFilter}
|
||||
onChange={value => display.setSearchInColumns(value)}
|
||||
data-testid="ColumnManager_searchColumns"
|
||||
@@ -186,8 +187,8 @@
|
||||
on:click={() => {
|
||||
showModal(InputTextModal, {
|
||||
value: '',
|
||||
label: 'Column name',
|
||||
header: 'Add new column',
|
||||
label: _t('column.name', {defaultMessage: 'Column name'}),
|
||||
header: _t('column.addNew', {defaultMessage: 'Add new column'}),
|
||||
onConfirm: name => {
|
||||
display.addDynamicColumn(name);
|
||||
tick().then(() => {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ColumnEditorModal from '../tableeditor/ColumnEditorModal.svelte';
|
||||
import { editorDeleteColumn } from 'dbgate-tools';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
|
||||
export let column;
|
||||
export let display;
|
||||
@@ -59,13 +60,17 @@
|
||||
on:mouseup
|
||||
>
|
||||
<div>
|
||||
<span class="expandColumnIcon" style={`margin-right: ${5 + (column.uniquePath.length - 1) * 10}px`}>
|
||||
<FontIcon
|
||||
icon={column.isExpandable ? plusExpandIcon(display.isExpandedColumn(column.uniqueName)) : 'icon invisible-box'}
|
||||
on:click={() => display.toggleExpandedColumn(column.uniqueName)}
|
||||
data-testid="ColumnManagerRow_expand_{column.uniqueName}"
|
||||
/>
|
||||
</span>
|
||||
{#if isProApp()}
|
||||
<span class="expandColumnIcon" style={`margin-right: ${5 + (column.uniquePath.length - 1) * 10}px`}>
|
||||
<FontIcon
|
||||
icon={column.isExpandable
|
||||
? plusExpandIcon(display.isExpandedColumn(column.uniqueName))
|
||||
: 'icon invisible-box'}
|
||||
on:click={() => display.toggleExpandedColumn(column.uniqueName)}
|
||||
data-testid="ColumnManagerRow_expand_{column.uniqueName}"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if isJsonView}
|
||||
<FontIcon icon="img column" />
|
||||
{:else}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import DictionaryLookupModal from '../modals/DictionaryLookupModal.svelte';
|
||||
import ValueLookupModal from '../modals/ValueLookupModal.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let isReadOnly = false;
|
||||
export let filterBehaviour;
|
||||
@@ -24,6 +25,7 @@
|
||||
export let setFilter;
|
||||
export let showResizeSplitter = false;
|
||||
export let onFocusGrid = null;
|
||||
export let onFocusGridHeader = null;
|
||||
export let onGetReference = null;
|
||||
export let foreignKey = null;
|
||||
export let conid = null;
|
||||
@@ -34,6 +36,7 @@
|
||||
export let onCustomCommand = null;
|
||||
export let customCommandTooltip = null;
|
||||
export let formatterFunction = null;
|
||||
export let filterDisabled = false;
|
||||
|
||||
export let pureName = null;
|
||||
export let schemaName = null;
|
||||
@@ -47,6 +50,7 @@
|
||||
let isError;
|
||||
let isOk;
|
||||
let domInput;
|
||||
let isDisabled;
|
||||
|
||||
$: if (onGetReference && domInput) onGetReference(domInput);
|
||||
|
||||
@@ -62,43 +66,43 @@
|
||||
|
||||
function createMenu() {
|
||||
const res = [
|
||||
{ onClick: () => setFilter(''), text: 'Clear Filter' },
|
||||
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
|
||||
{ onClick: () => setFilter(''), text: _t('filter.clear', { defaultMessage: 'Clear Filter' }) },
|
||||
{ onClick: () => filterMultipleValues(), text: _t('filter.multipleValues', { defaultMessage: 'Filter multiple values' }) },
|
||||
];
|
||||
|
||||
if (filterBehaviour.supportEquals) {
|
||||
res.push(
|
||||
{ onClick: () => openFilterWindow('='), text: 'Equals...' },
|
||||
{ onClick: () => openFilterWindow('<>'), text: 'Does Not Equal...' }
|
||||
{ onClick: () => openFilterWindow('='), text: _t('filter.equals', { defaultMessage: 'Equals...' }) },
|
||||
{ onClick: () => openFilterWindow('<>'), text: _t('filter.doesNotEqual', { defaultMessage: 'Does Not Equal...' }) }
|
||||
);
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportExistsTesting) {
|
||||
res.push(
|
||||
{ onClick: () => setFilter('EXISTS'), text: 'Field exists' },
|
||||
{ onClick: () => setFilter('NOT EXISTS'), text: 'Field does not exist' }
|
||||
{ onClick: () => setFilter('EXISTS'), text: _t('filter.fieldExists', { defaultMessage: 'Field exists' }) },
|
||||
{ onClick: () => setFilter('NOT EXISTS'), text: _t('filter.fieldDoesNotExist', { defaultMessage: 'Field does not exist' }) }
|
||||
);
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportNotEmptyArrayTesting) {
|
||||
res.push({ onClick: () => setFilter('NOT EMPTY ARRAY'), text: 'Array is not empty' });
|
||||
res.push({ onClick: () => setFilter('NOT EMPTY ARRAY'), text: _t('filter.arrayIsNotEmpty', { defaultMessage: 'Array is not empty' }) });
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportEmptyArrayTesting) {
|
||||
res.push({ onClick: () => setFilter('EMPTY ARRAY'), text: 'Array is empty' });
|
||||
res.push({ onClick: () => setFilter('EMPTY ARRAY'), text: _t('filter.arrayIsEmpty', { defaultMessage: 'Array is empty' }) });
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportNullTesting) {
|
||||
res.push(
|
||||
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
|
||||
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' }
|
||||
{ onClick: () => setFilter('NULL'), text: _t('filter.isNull', { defaultMessage: 'Is Null' }) },
|
||||
{ onClick: () => setFilter('NOT NULL'), text: _t('filter.isNotNull', { defaultMessage: 'Is Not Null' }) }
|
||||
);
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportEmpty) {
|
||||
res.push(
|
||||
{ onClick: () => setFilter('EMPTY, NULL'), text: 'Is Empty Or Null' },
|
||||
{ onClick: () => setFilter('NOT EMPTY NOT NULL'), text: 'Has Not Empty Value' }
|
||||
{ onClick: () => setFilter('EMPTY, NULL'), text: _t('filter.isEmptyOrNull', { defaultMessage: 'Is Empty Or Null' }) },
|
||||
{ onClick: () => setFilter('NOT EMPTY NOT NULL'), text: _t('filter.hasNotEmptyValue', { defaultMessage: 'Has Not Empty Value' }) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,10 +110,10 @@
|
||||
res.push(
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => openFilterWindow('>'), text: 'Greater Than...' },
|
||||
{ onClick: () => openFilterWindow('>='), text: 'Greater Than Or Equal To...' },
|
||||
{ onClick: () => openFilterWindow('<'), text: 'Less Than...' },
|
||||
{ onClick: () => openFilterWindow('<='), text: 'Less Than Or Equal To...' }
|
||||
{ onClick: () => openFilterWindow('>'), text: _t('filter.greaterThan', { defaultMessage: 'Greater Than...' }) },
|
||||
{ onClick: () => openFilterWindow('>='), text: _t('filter.greaterThanOrEqualTo', { defaultMessage: 'Greater Than Or Equal To...' }) },
|
||||
{ onClick: () => openFilterWindow('<'), text: _t('filter.lessThan', { defaultMessage: 'Less Than...' }) },
|
||||
{ onClick: () => openFilterWindow('<='), text: _t('filter.lessThanOrEqualTo', { defaultMessage: 'Less Than Or Equal To...' }) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,26 +121,26 @@
|
||||
res.push(
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => openFilterWindow('+'), text: 'Contains...' },
|
||||
{ onClick: () => openFilterWindow('~'), text: 'Does Not Contain...' },
|
||||
{ onClick: () => openFilterWindow('^'), text: 'Begins With...' },
|
||||
{ onClick: () => openFilterWindow('!^'), text: 'Does Not Begin With...' },
|
||||
{ onClick: () => openFilterWindow('$'), text: 'Ends With...' },
|
||||
{ onClick: () => openFilterWindow('!$'), text: 'Does Not End With...' }
|
||||
{ onClick: () => openFilterWindow('+'), text: _t('filter.contains', { defaultMessage: 'Contains...' }) },
|
||||
{ onClick: () => openFilterWindow('~'), text: _t('filter.doesNotContain', { defaultMessage: 'Does Not Contain...' }) },
|
||||
{ onClick: () => openFilterWindow('^'), text: _t('filter.beginsWith', { defaultMessage: 'Begins With...' }) },
|
||||
{ onClick: () => openFilterWindow('!^'), text: _t('filter.doesNotBeginWith', { defaultMessage: 'Does Not Begin With...' }) },
|
||||
{ onClick: () => openFilterWindow('$'), text: _t('filter.endsWith', { defaultMessage: 'Ends With...' }) },
|
||||
{ onClick: () => openFilterWindow('!$'), text: _t('filter.doesNotEndWith', { defaultMessage: 'Does Not End With...' }) }
|
||||
);
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportBooleanValues) {
|
||||
res.push(
|
||||
{ onClick: () => setFilter('TRUE'), text: 'Is True' },
|
||||
{ onClick: () => setFilter('FALSE'), text: 'Is False' }
|
||||
{ onClick: () => setFilter('TRUE'), text: _t('filter.isTrue', { defaultMessage: 'Is True' }) },
|
||||
{ onClick: () => setFilter('FALSE'), text: _t('filter.isFalse', { defaultMessage: 'Is False' }) }
|
||||
);
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportBooleanOrNull) {
|
||||
res.push(
|
||||
{ onClick: () => setFilter('TRUE, NULL'), text: 'Is True or NULL' },
|
||||
{ onClick: () => setFilter('FALSE, NULL'), text: 'Is False or NULL' }
|
||||
{ onClick: () => setFilter('TRUE, NULL'), text: _t('filter.isTrueOrNull', { defaultMessage: 'Is True or NULL' }) },
|
||||
{ onClick: () => setFilter('FALSE, NULL'), text: _t('filter.isFalseOrNull', { defaultMessage: 'Is False or NULL' }) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,44 +148,44 @@
|
||||
res.push(
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('TOMORROW'), text: 'Tomorrow' },
|
||||
{ onClick: () => setFilter('TODAY'), text: 'Today' },
|
||||
{ onClick: () => setFilter('YESTERDAY'), text: 'Yesterday' },
|
||||
{ onClick: () => setFilter('TOMORROW'), text: _t('filter.tomorrow', { defaultMessage: 'Tomorrow' }) },
|
||||
{ onClick: () => setFilter('TODAY'), text: _t('filter.today', { defaultMessage: 'Today' }) },
|
||||
{ onClick: () => setFilter('YESTERDAY'), text: _t('filter.yesterday', { defaultMessage: 'Yesterday' }) },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('NEXT WEEK'), text: 'Next Week' },
|
||||
{ onClick: () => setFilter('THIS WEEK'), text: 'This Week' },
|
||||
{ onClick: () => setFilter('LAST WEEK'), text: 'Last Week' },
|
||||
{ onClick: () => setFilter('NEXT WEEK'), text: _t('filter.nextWeek', { defaultMessage: 'Next Week' }) },
|
||||
{ onClick: () => setFilter('THIS WEEK'), text: _t('filter.thisWeek', { defaultMessage: 'This Week' }) },
|
||||
{ onClick: () => setFilter('LAST WEEK'), text: _t('filter.lastWeek', { defaultMessage: 'Last Week' }) },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('NEXT MONTH'), text: 'Next Month' },
|
||||
{ onClick: () => setFilter('THIS MONTH'), text: 'This Month' },
|
||||
{ onClick: () => setFilter('LAST MONTH'), text: 'Last Month' },
|
||||
{ onClick: () => setFilter('NEXT MONTH'), text: _t('filter.nextMonth', { defaultMessage: 'Next Month' }) },
|
||||
{ onClick: () => setFilter('THIS MONTH'), text: _t('filter.thisMonth', { defaultMessage: 'This Month' }) },
|
||||
{ onClick: () => setFilter('LAST MONTH'), text: _t('filter.lastMonth', { defaultMessage: 'Last Month' }) },
|
||||
|
||||
{ divider: true },
|
||||
|
||||
{ onClick: () => setFilter('NEXT YEAR'), text: 'Next Year' },
|
||||
{ onClick: () => setFilter('THIS YEAR'), text: 'This Year' },
|
||||
{ onClick: () => setFilter('LAST YEAR'), text: 'Last Year' }
|
||||
{ onClick: () => setFilter('NEXT YEAR'), text: _t('filter.nextYear', { defaultMessage: 'Next Year' }) },
|
||||
{ onClick: () => setFilter('THIS YEAR'), text: _t('filter.thisYear', { defaultMessage: 'This Year' }) },
|
||||
{ onClick: () => setFilter('LAST YEAR'), text: _t('filter.lastYear', { defaultMessage: 'Last Year' }) }
|
||||
);
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportDatetimeComparison) {
|
||||
res.push(
|
||||
{ divider: true },
|
||||
{ onClick: () => openFilterWindow('<='), text: 'Before...' },
|
||||
{ onClick: () => openFilterWindow('>='), text: 'After...' },
|
||||
{ onClick: () => openFilterWindow('>=;<='), text: 'Between...' }
|
||||
{ onClick: () => openFilterWindow('<='), text: _t('filter.before', { defaultMessage: 'Before...' }) },
|
||||
{ onClick: () => openFilterWindow('>='), text: _t('filter.after', { defaultMessage: 'After...' }) },
|
||||
{ onClick: () => openFilterWindow('>=;<='), text: _t('filter.between', { defaultMessage: 'Between...' }) }
|
||||
);
|
||||
}
|
||||
|
||||
if (filterBehaviour.supportSqlCondition) {
|
||||
res.push(
|
||||
{ divider: true },
|
||||
{ onClick: () => openFilterWindow('sql'), text: 'SQL condition ...' },
|
||||
{ onClick: () => openFilterWindow('sqlRight'), text: 'SQL condition - right side ...' }
|
||||
{ onClick: () => openFilterWindow('sql'), text: _t('filter.sqlCondition', { defaultMessage: 'SQL condition ...' }) },
|
||||
{ onClick: () => openFilterWindow('sqlRight'), text: _t('filter.sqlConditionRight', { defaultMessage: 'SQL condition - right side ...' }) }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,6 +205,11 @@
|
||||
// ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
if (ev.keyCode == keycodes.upArrow) {
|
||||
if (onFocusGridHeader) onFocusGridHeader();
|
||||
// ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
}
|
||||
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
|
||||
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
|
||||
// }
|
||||
@@ -257,6 +266,7 @@
|
||||
try {
|
||||
isOk = false;
|
||||
isError = false;
|
||||
isDisabled = filterDisabled;
|
||||
if (value) {
|
||||
parseFilter(value, filterBehaviour);
|
||||
isOk = true;
|
||||
@@ -287,6 +297,7 @@
|
||||
on:paste={handlePaste}
|
||||
class:isError
|
||||
class:isOk
|
||||
class:isDisabled
|
||||
{placeholder}
|
||||
data-testid={`DataFilterControl_input_${uniqueName}`}
|
||||
/>
|
||||
@@ -345,4 +356,8 @@
|
||||
input.isOk {
|
||||
background-color: var(--theme-bg-green);
|
||||
}
|
||||
|
||||
input.isDisabled {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.switchToForm',
|
||||
category: 'Data grid',
|
||||
name: 'Switch to form',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.switchToform', { defaultMessage: 'Switch to form' }),
|
||||
icon: 'icon form',
|
||||
keyText: 'F4',
|
||||
testEnabled: () => getCurrentEditor()?.switchViewEnabled('form'),
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.switchToJson',
|
||||
category: 'Data grid',
|
||||
name: 'Switch to JSON',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.switchToJSON', { defaultMessage: 'Switch to JSON' }),
|
||||
icon: 'icon json',
|
||||
keyText: 'F4',
|
||||
testEnabled: () => getCurrentEditor()?.switchViewEnabled('json'),
|
||||
@@ -23,8 +23,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.switchToTable',
|
||||
category: 'Data grid',
|
||||
name: 'Switch to table',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.witchToTable', { defaultMessage: 'Switch to table' }),
|
||||
icon: 'icon table',
|
||||
keyText: 'F4',
|
||||
testEnabled: () => getCurrentEditor()?.switchViewEnabled('table'),
|
||||
@@ -33,13 +33,24 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.toggleLeftPanel',
|
||||
category: 'Data grid',
|
||||
name: 'Toggle left panel',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.toggleLeftPanel', { defaultMessage: 'Toggle left panel' }),
|
||||
keyText: 'CtrlOrCommand+L',
|
||||
testEnabled: () => getCurrentEditor()?.canShowLeftPanel(),
|
||||
onClick: () => getCurrentEditor().toggleLeftPanel(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.toggleCellDataView',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.toggleCellDataView', { defaultMessage: 'Toggle cell data view' }),
|
||||
toolbarName: __t('command.datagrid.toggleCellDataView.toolbar', { defaultMessage: 'Cell Data' }),
|
||||
menuName: __t('command.datagrid.toggleCellDataView.menu', { defaultMessage: 'Show cell data' }),
|
||||
icon: 'icon cell-data',
|
||||
testEnabled: () => !!getCurrentEditor(),
|
||||
onClick: () => getCurrentEditor().toggleCellDataView(),
|
||||
});
|
||||
|
||||
function extractMacroValuesForMacro(macroValues, macro) {
|
||||
// return {};
|
||||
if (!macro) return {};
|
||||
@@ -68,6 +79,9 @@
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import { __t, _t } from '../translations';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import CellDataWidget from '../widgets/CellDataWidget.svelte';
|
||||
|
||||
export let config;
|
||||
export let setConfig;
|
||||
@@ -89,6 +103,7 @@
|
||||
export let hasMultiColumnFilter = false;
|
||||
export let setLoadedRows = null;
|
||||
export let hideGridLeftColumn = false;
|
||||
export let cellDataViewVisible = false;
|
||||
|
||||
export let onPublishedCellsChanged;
|
||||
|
||||
@@ -105,6 +120,7 @@
|
||||
setContext('macroValues', macroValues);
|
||||
|
||||
let managerSize;
|
||||
let cellViewWidth;
|
||||
const collapsedLeftColumnStore =
|
||||
getContext('collapsedLeftColumnStore') || writable(getLocalStorage('dataGrid_collapsedLeftColumn', false));
|
||||
|
||||
@@ -147,6 +163,10 @@
|
||||
collapsedLeftColumnStore.update(x => !x);
|
||||
}
|
||||
|
||||
export function toggleCellDataView() {
|
||||
cellDataViewVisible = !cellDataViewVisible;
|
||||
}
|
||||
|
||||
registerMenu(
|
||||
{ command: 'dataGrid.switchToForm', tag: 'switch', hideDisabled: true },
|
||||
{ command: 'dataGrid.switchToTable', tag: 'switch', hideDisabled: true },
|
||||
@@ -155,6 +175,7 @@
|
||||
);
|
||||
|
||||
$: if (managerSize) setLocalStorage('dataGridManagerWidth', managerSize);
|
||||
$: if (cellViewWidth) setLocalStorage('dataGridCellViewWidth', cellViewWidth);
|
||||
|
||||
function getInitialManagerSize() {
|
||||
const width = getLocalStorage('dataGridManagerWidth');
|
||||
@@ -163,6 +184,14 @@
|
||||
}
|
||||
return '300px';
|
||||
}
|
||||
|
||||
function getInitialCellViewWidth() {
|
||||
const width = getLocalStorage('dataGridCellViewWidth');
|
||||
if (_.isNumber(width) && width > 30 && width < 500) {
|
||||
return width;
|
||||
}
|
||||
return 300;
|
||||
}
|
||||
</script>
|
||||
|
||||
<HorizontalSplitter
|
||||
@@ -173,7 +202,7 @@
|
||||
<div class="left" slot="1">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem
|
||||
title="Columns"
|
||||
title={_t('dataGrid.columns', { defaultMessage: 'Columns' })}
|
||||
name="columns"
|
||||
height="45%"
|
||||
skip={isFormView}
|
||||
@@ -183,7 +212,7 @@
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title="Filters"
|
||||
title={_t('dataGrid.filters', { defaultMessage: 'Filters' })}
|
||||
name="filters"
|
||||
height={showReferences && display?.hasReferences && !isFormView ? '15%' : '30%'}
|
||||
skip={!display?.filterable}
|
||||
@@ -201,22 +230,23 @@
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title="References"
|
||||
title={_t('dataGrid.references', { defaultMessage: 'References' })}
|
||||
name="references"
|
||||
height="30%"
|
||||
collapsed={isDetailView}
|
||||
skip={!(showReferences && display?.hasReferences)}
|
||||
skip={!(showReferences && display?.hasReferences && isProApp())}
|
||||
data-testid="DataGrid_itemReferences"
|
||||
>
|
||||
<ReferenceManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title="Macros"
|
||||
title={_t('dataGrid.macros', { defaultMessage: 'Macros' })}
|
||||
name="macros"
|
||||
skip={!showMacros}
|
||||
skip={!(showMacros && isProApp())}
|
||||
collapsed={!expandMacros}
|
||||
data-testid="DataGrid_itemMacros"
|
||||
height="20%"
|
||||
>
|
||||
<MacroManager {...$$props} {managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
@@ -225,30 +255,49 @@
|
||||
<svelte:fragment slot="2">
|
||||
<VerticalSplitter initialValue="70%" isSplitter={!!$selectedMacro && !isFormView && showMacros}>
|
||||
<svelte:fragment slot="1">
|
||||
{#if isFormView}
|
||||
<svelte:component this={formViewComponent} {...$$props} />
|
||||
{:else if isJsonView}
|
||||
<svelte:component this={jsonViewComponent} {...$$props} {setLoadedRows} />
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={gridCoreComponent}
|
||||
{...$$props}
|
||||
{collapsedLeftColumnStore}
|
||||
formViewAvailable={!!formViewComponent}
|
||||
macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)}
|
||||
macroPreview={$selectedMacro}
|
||||
{setLoadedRows}
|
||||
onPublishedCellsChanged={value => {
|
||||
publishedCells = value;
|
||||
if (onPublishedCellsChanged) {
|
||||
onPublishedCellsChanged(value);
|
||||
}
|
||||
}}
|
||||
onChangeSelectedColumns={cols => {
|
||||
if (domColumnManager) domColumnManager.setSelectedColumns(cols);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<HorizontalSplitter
|
||||
initialSizeRight={getInitialCellViewWidth()}
|
||||
onChangeSize={value => (cellViewWidth = value)}
|
||||
isSplitter={cellDataViewVisible && !isFormView}
|
||||
>
|
||||
<svelte:fragment slot="1">
|
||||
{#if isFormView}
|
||||
<svelte:component this={formViewComponent} {...$$props} />
|
||||
{:else if isJsonView}
|
||||
<svelte:component this={jsonViewComponent} {...$$props} {setLoadedRows} />
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={gridCoreComponent}
|
||||
{...$$props}
|
||||
{collapsedLeftColumnStore}
|
||||
formViewAvailable={!!formViewComponent}
|
||||
macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)}
|
||||
macroPreview={$selectedMacro}
|
||||
{setLoadedRows}
|
||||
onPublishedCellsChanged={value => {
|
||||
publishedCells = value;
|
||||
if (onPublishedCellsChanged) {
|
||||
onPublishedCellsChanged(value);
|
||||
}
|
||||
if (value[0]?.isSelectedFullRow && !isFormView) {
|
||||
cellDataViewVisible = true;
|
||||
}
|
||||
}}
|
||||
onChangeSelectedColumns={cols => {
|
||||
if (domColumnManager) domColumnManager.setSelectedColumns(cols);
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2">
|
||||
<CellDataWidget
|
||||
onClose={() => {
|
||||
cellDataViewVisible = false;
|
||||
}}
|
||||
selection={publishedCells}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</HorizontalSplitter>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="2">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import _, { isPlainObject } from 'lodash';
|
||||
import ShowFormButton from '../formview/ShowFormButton.svelte';
|
||||
import { detectTypeIcon, getConvertValueMenu, isJsonLikeLongString, safeJsonParse } from 'dbgate-tools';
|
||||
import { detectTypeIcon, getConvertValueMenu, isJsonLikeLongString, safeJsonParse, isTypeNumber } from 'dbgate-tools';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import CellValue from './CellValue.svelte';
|
||||
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
||||
@@ -38,7 +38,9 @@
|
||||
export let overlayValue = null;
|
||||
export let isMissingOverlayField = false;
|
||||
|
||||
$: value = col.isStructured ? _.get(rowData || {}, col.uniquePath) : (rowData || {})[col.uniqueName];
|
||||
$: value = col.isStructured
|
||||
? _.get(rowData || {}, col.uniquePath)
|
||||
: (rowData || {})[col.uniqueNameShorten ?? col.uniqueName];
|
||||
|
||||
function computeStyle(maxWidth, col) {
|
||||
let res = '';
|
||||
@@ -55,7 +57,11 @@
|
||||
$: style = computeStyle(maxWidth, col);
|
||||
|
||||
$: isJson =
|
||||
_.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint;
|
||||
_.isPlainObject(value) &&
|
||||
!(value?.type == 'Buffer' && _.isArray(value.data)) &&
|
||||
!value.$oid &&
|
||||
!value.$bigint &&
|
||||
!value.$decimal;
|
||||
|
||||
// don't parse JSON for explicit data types
|
||||
$: jsonParsedValue = !editorTypes?.explicitDataType && isJsonLikeLongString(value) ? safeJsonParse(value) : null;
|
||||
@@ -78,7 +84,7 @@
|
||||
class:isFocusedColumn
|
||||
class:hasOverlayValue
|
||||
class:isMissingOverlayField
|
||||
class:alignRight={_.isNumber(value) && !showHint}
|
||||
class:alignRight={(_.isNumber(value) || isTypeNumber(col.dataType)) && !showHint && !isModifiedCell}
|
||||
{style}
|
||||
>
|
||||
{#if hasOverlayValue}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.refresh',
|
||||
category: 'Data grid',
|
||||
name: _t('common.refresh', { defaultMessage: 'Refresh' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('common.refresh', { defaultMessage: 'Refresh' }),
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -15,8 +15,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.deepRefresh',
|
||||
category: 'Data grid',
|
||||
name: 'Refresh with structure',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('common.datagrid.deepRefresh', { defaultMessage: 'Refresh with structure' }),
|
||||
keyText: 'Ctrl+F5',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -27,8 +27,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.revertRowChanges',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.revertRowChanges', { defaultMessage: 'Revert row changes' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.revertRowChanges', { defaultMessage: 'Revert row changes' }),
|
||||
keyText: 'CtrlOrCommand+U',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges,
|
||||
onClick: () => getCurrentDataGrid().revertRowChanges(),
|
||||
@@ -36,9 +36,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.revertAllChanges',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.revertAllChanges', { defaultMessage: 'Revert all changes' }),
|
||||
toolbarName: _t('command.datagrid.revertAllChanges.toolbar', { defaultMessage: 'Revert all' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.revertAllChanges', { defaultMessage: 'Revert all changes' }),
|
||||
toolbarName: __t('command.datagrid.revertAllChanges.toolbar', { defaultMessage: 'Revert all' }),
|
||||
icon: 'icon undo',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges,
|
||||
onClick: () => getCurrentDataGrid().revertAllChanges(),
|
||||
@@ -46,9 +46,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.deleteSelectedRows',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.deleteSelectedRows', { defaultMessage: 'Delete selected rows' }),
|
||||
toolbarName: _t('command.datagrid.deleteSelectedRows.toolbar', { defaultMessage: 'Delete row(s)' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.deleteSelectedRows', { defaultMessage: 'Delete selected rows' }),
|
||||
toolbarName: __t('command.datagrid.deleteSelectedRows.toolbar', { defaultMessage: 'Delete row(s)' }),
|
||||
keyText: isMac() ? 'Command+Backspace' : 'CtrlOrCommand+Delete',
|
||||
icon: 'icon minus',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
@@ -57,9 +57,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.insertNewRow',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.insertNewRow', { defaultMessage: 'Insert new row' }),
|
||||
toolbarName: _t('command.datagrid.insertNewRow.toolbar', { defaultMessage: 'New row' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.insertNewRow', { defaultMessage: 'Insert new row' }),
|
||||
toolbarName: __t('command.datagrid.insertNewRow.toolbar', { defaultMessage: 'New row' }),
|
||||
icon: 'icon add',
|
||||
keyText: isMac() ? 'Command+I' : 'Insert',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
@@ -68,9 +68,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.addNewColumn',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.addNewColumn', { defaultMessage: 'Add new column' }),
|
||||
toolbarName: _t('command.datagrid.addNewColumn.toolbar', { defaultMessage: 'New column' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.addNewColumn', { defaultMessage: 'Add new column' }),
|
||||
toolbarName: __t('command.datagrid.addNewColumn.toolbar', { defaultMessage: 'New column' }),
|
||||
icon: 'icon add-column',
|
||||
testEnabled: () => getCurrentDataGrid()?.addNewColumnEnabled(),
|
||||
onClick: () => getCurrentDataGrid().addNewColumn(),
|
||||
@@ -78,9 +78,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.cloneRows',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.cloneRows', { defaultMessage: 'Clone rows' }),
|
||||
toolbarName: _t('command.datagrid.cloneRows.toolbar', { defaultMessage: 'Clone row(s)' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.cloneRows', { defaultMessage: 'Clone rows' }),
|
||||
toolbarName: __t('command.datagrid.cloneRows.toolbar', { defaultMessage: 'Clone row(s)' }),
|
||||
keyText: 'CtrlOrCommand+Shift+C',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
onClick: () => getCurrentDataGrid().cloneRows(),
|
||||
@@ -88,8 +88,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.setNull',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.setNull', { defaultMessage: 'Set NULL' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.setNull', { defaultMessage: 'Set NULL' }),
|
||||
keyText: 'CtrlOrCommand+0',
|
||||
testEnabled: () =>
|
||||
getCurrentDataGrid()?.getGrider()?.editable && !getCurrentDataGrid()?.getEditorTypes()?.supportFieldRemoval,
|
||||
@@ -98,8 +98,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.removeField',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.removeField', { defaultMessage: 'Remove field' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.removeField', { defaultMessage: 'Remove field' }),
|
||||
keyText: 'CtrlOrCommand+0',
|
||||
testEnabled: () =>
|
||||
getCurrentDataGrid()?.getGrider()?.editable && getCurrentDataGrid()?.getEditorTypes()?.supportFieldRemoval,
|
||||
@@ -108,8 +108,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.undo',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.undo', { defaultMessage: 'Undo' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.undo', { defaultMessage: 'Undo' }),
|
||||
group: 'undo',
|
||||
icon: 'icon undo',
|
||||
toolbar: true,
|
||||
@@ -120,8 +120,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.redo',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.redo', { defaultMessage: 'Redo' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.redo', { defaultMessage: 'Redo' }),
|
||||
group: 'redo',
|
||||
icon: 'icon redo',
|
||||
toolbar: true,
|
||||
@@ -132,16 +132,16 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.reconnect',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.reconnect', { defaultMessage: 'Reconnect' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.reconnect', { defaultMessage: 'Reconnect' }),
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().reconnect(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.copyToClipboard',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.copyToClipboard', { defaultMessage: 'Copy to clipboard' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.copyToClipboard', { defaultMessage: 'Copy to clipboard' }),
|
||||
keyText: 'CtrlOrCommand+C',
|
||||
disableHandleKeyText: 'CtrlOrCommand+C',
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
@@ -150,57 +150,57 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.editJsonDocument',
|
||||
category: 'Data grid',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
keyText: 'CtrlOrCommand+J',
|
||||
name: _t('command.datagrid.editJsonDocument', { defaultMessage: 'Edit row as JSON document' }),
|
||||
name: __t('command.datagrid.editJsonDocument', { defaultMessage: 'Edit row as JSON document' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.editJsonEnabled(),
|
||||
onClick: () => getCurrentDataGrid().editJsonDocument(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.openSelectionInMap',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.openSelectionInMap', { defaultMessage: 'Open selection in map' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.openSelectionInMap', { defaultMessage: 'Open selection in map' }),
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().openSelectionInMap(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.viewJsonDocument',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.viewJsonDocument', { defaultMessage: 'View row as JSON document' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.viewJsonDocument', { defaultMessage: 'View row as JSON document' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.viewJsonDocumentEnabled(),
|
||||
onClick: () => getCurrentDataGrid().viewJsonDocument(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.viewJsonValue',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.viewJsonValue', { defaultMessage: 'View cell as JSON document' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.viewJsonValue', { defaultMessage: 'View cell as JSON document' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.viewJsonValueEnabled(),
|
||||
onClick: () => getCurrentDataGrid().viewJsonValue(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.openJsonArrayInSheet',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.openJsonArrayInSheet', { defaultMessage: 'Open array as table' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.openJsonArrayInSheet', { defaultMessage: 'Open array as table' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.openJsonArrayInSheetEnabled(),
|
||||
onClick: () => getCurrentDataGrid().openJsonArrayInSheet(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.saveCellToFile',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.saveCellToFile', { defaultMessage: 'Save cell to file' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.saveCellToFile', { defaultMessage: 'Save cell to file' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.saveCellToFileEnabled(),
|
||||
onClick: () => getCurrentDataGrid().saveCellToFile(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.loadCellFromFile',
|
||||
category: 'Data grid',
|
||||
name: _t('command.datagrid.loadCellFromFile', { defaultMessage: 'Load cell from file' }),
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.loadCellFromFile', { defaultMessage: 'Load cell from file' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.loadCellFromFileEnabled(),
|
||||
onClick: () => getCurrentDataGrid().loadCellFromFile(),
|
||||
});
|
||||
@@ -216,62 +216,62 @@
|
||||
//
|
||||
registerCommand({
|
||||
id: 'dataGrid.filterSelected',
|
||||
category: 'Data grid',
|
||||
name: 'Filter selected value',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.filterSelected', { defaultMessage: 'Filter selected value' }),
|
||||
keyText: 'CtrlOrCommand+Shift+F',
|
||||
testEnabled: () => getCurrentDataGrid()?.getDisplay().filterable,
|
||||
onClick: () => getCurrentDataGrid().filterSelectedValue(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.findColumn',
|
||||
category: 'Data grid',
|
||||
name: 'Find column',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.findColumn', { defaultMessage: 'Find column' }),
|
||||
keyText: 'CtrlOrCommand+F',
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
getSubCommands: () => getCurrentDataGrid().buildFindMenu(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.hideColumn',
|
||||
category: 'Data grid',
|
||||
name: 'Hide column',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datgrid.hideColumn', { defaultMessage: 'Hide column' }),
|
||||
keyText: isMac() ? 'Alt+Command+F' : 'CtrlOrCommand+H',
|
||||
testEnabled: () => getCurrentDataGrid()?.canShowLeftPanel(),
|
||||
onClick: () => getCurrentDataGrid().hideColumn(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.clearFilter',
|
||||
category: 'Data grid',
|
||||
name: 'Clear filter',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.clearFilter', { defaultMessage: 'Clear filter' }),
|
||||
keyText: 'CtrlOrCommand+Shift+E',
|
||||
testEnabled: () => getCurrentDataGrid()?.clearFilterEnabled(),
|
||||
onClick: () => getCurrentDataGrid().clearFilter(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.generateSqlFromData',
|
||||
category: 'Data grid',
|
||||
name: 'Generate SQL',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.generateSql', { defaultMessage: 'Generate SQL' }),
|
||||
keyText: 'CtrlOrCommand+G',
|
||||
testEnabled: () => getCurrentDataGrid()?.generateSqlFromDataEnabled(),
|
||||
onClick: () => getCurrentDataGrid().generateSqlFromData(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.openFreeTable',
|
||||
category: 'Data grid',
|
||||
name: 'Edit selection as table',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.editSelection', { defaultMessage: 'Edit selection as table' }),
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().openFreeTable(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.newJson',
|
||||
category: 'Data grid',
|
||||
name: 'Add JSON document',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.addJsonDocument', { defaultMessage: 'Add JSON document' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.addJsonDocumentEnabled(),
|
||||
onClick: () => getCurrentDataGrid().addJsonDocument(),
|
||||
});
|
||||
registerCommand({
|
||||
id: 'dataGrid.editCellValue',
|
||||
category: 'Data grid',
|
||||
name: 'Edit cell value',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.editCell', { defaultMessage: 'Edit cell value' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.editCellValueEnabled(),
|
||||
onClick: () => getCurrentDataGrid().editCellValue(),
|
||||
});
|
||||
@@ -279,8 +279,8 @@
|
||||
if (isProApp()) {
|
||||
registerCommand({
|
||||
id: 'dataGrid.sendToDataDeploy',
|
||||
category: 'Data grid',
|
||||
name: 'Send to data deployer',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.sendToDataDeployer', { defaultMessage: 'Send to data deployer' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.sendToDataDeployEnabled(),
|
||||
onClick: () => getCurrentDataGrid().sendToDataDeploy(),
|
||||
});
|
||||
@@ -354,15 +354,17 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { GridDisplay } from 'dbgate-datalib';
|
||||
import { GridDisplay, MacroDefinition } from 'dbgate-datalib';
|
||||
import {
|
||||
driverBase,
|
||||
parseCellValue,
|
||||
detectSqlFilterBehaviour,
|
||||
stringifyCellValue,
|
||||
shouldOpenMultilineDialog,
|
||||
base64ToHex,
|
||||
} from 'dbgate-tools';
|
||||
import { getContext, onDestroy } from 'svelte';
|
||||
import { type Writable } from 'svelte/store';
|
||||
import _, { map } from 'lodash';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import ColumnHeaderControl from './ColumnHeaderControl.svelte';
|
||||
@@ -379,7 +381,17 @@
|
||||
filterCellsForRow,
|
||||
} from './gridutil';
|
||||
import HorizontalScrollBar from './HorizontalScrollBar.svelte';
|
||||
import { cellFromEvent, emptyCellArray, getCellRange, isRegularCell, nullCell, topLeftCell } from './selection';
|
||||
import {
|
||||
cellFromEvent,
|
||||
emptyCellArray,
|
||||
getCellRange,
|
||||
isColumnHeaderCell,
|
||||
isRegularCell,
|
||||
isRowHeaderCell,
|
||||
isTableHeaderCell,
|
||||
nullCell,
|
||||
topLeftCell,
|
||||
} from './selection';
|
||||
import VerticalScrollBar from './VerticalScrollBar.svelte';
|
||||
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
@@ -387,7 +399,7 @@
|
||||
import DataFilterControl from './DataFilterControl.svelte';
|
||||
import createReducer from '../utility/createReducer';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { copyRowsFormat, currentArchive, selectedCellsCallback } from '../stores';
|
||||
import { copyRowsFormat, currentArchive } from '../stores';
|
||||
import {
|
||||
copyRowsFormatDefs,
|
||||
copyRowsToClipboard,
|
||||
@@ -421,10 +433,11 @@
|
||||
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
||||
import contextMenuActivator from '../utility/contextMenuActivator';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { _t } from '../translations';
|
||||
import { __t, _t, _tval } from '../translations';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import SaveArchiveModal from '../modals/SaveArchiveModal.svelte';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import macros from '../macro/macros';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
@@ -464,6 +477,7 @@
|
||||
export let overlayDefinition = null;
|
||||
export let onGetSelectionMenu = null;
|
||||
export let onOpenChart = null;
|
||||
export let macroCondition = null;
|
||||
|
||||
export const activator = createActivator('DataGridCore', false);
|
||||
|
||||
@@ -495,6 +509,7 @@
|
||||
let selectionMenu = null;
|
||||
|
||||
const tabid = getContext('tabid');
|
||||
const selectedMacro = getContext('selectedMacro') as Writable<MacroDefinition>;
|
||||
|
||||
let unsubscribeDbRefresh;
|
||||
|
||||
@@ -758,7 +773,7 @@
|
||||
|
||||
export function saveCellToFileEnabled() {
|
||||
const value = getSelectedExportableCell();
|
||||
return _.isString(value) || (value?.type == 'Buffer' && _.isArray(value?.data));
|
||||
return _.isString(value) || (value?.type == 'Buffer' && _.isArray(value?.data)) || value?.$binary?.base64;
|
||||
}
|
||||
|
||||
export async function saveCellToFile() {
|
||||
@@ -771,6 +786,8 @@
|
||||
fs.promises.writeFile(file, value);
|
||||
} else if (value?.type == 'Buffer' && _.isArray(value?.data)) {
|
||||
fs.promises.writeFile(file, window['Buffer'].from(value.data));
|
||||
} else if (value?.$binary?.base64) {
|
||||
fs.promises.writeFile(file, window['Buffer'].from(value.$binary.base64, 'base64'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -796,8 +813,9 @@
|
||||
isText
|
||||
? data
|
||||
: {
|
||||
type: 'Buffer',
|
||||
data: [...data],
|
||||
$binary: {
|
||||
base64: data.toString('base64'),
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1199,7 +1217,6 @@
|
||||
if (rowIndexes.every(x => grider.getRowData(x))) {
|
||||
lastPublishledSelectedCellsRef.set(stringified);
|
||||
changeSetValueRef.set($changeSetStore?.value);
|
||||
$selectedCellsCallback = () => getCellsPublished(selectedCells);
|
||||
|
||||
if (onChangeSelectedColumns) {
|
||||
onChangeSelectedColumns(getSelectedColumns().map(x => x.columnName));
|
||||
@@ -1240,30 +1257,59 @@
|
||||
|
||||
function getCellsPublished(cells) {
|
||||
const regular = cellsToRegularCells(cells);
|
||||
|
||||
const commonInfo = {
|
||||
engine: display?.driver,
|
||||
editable: grider.editable,
|
||||
editorTypes: display?.driver?.dataEditorTypesBehaviour,
|
||||
displayColumns: columns,
|
||||
realColumnUniqueNames,
|
||||
grider,
|
||||
};
|
||||
|
||||
const rowIndexes = _.sortBy(_.uniq(regular.map(x => x[0])));
|
||||
const fullRowIndexes = new Set(cells.filter(x => x[1] == 'header').map(x => x[0]));
|
||||
const rowInfos = rowIndexes.map(row => {
|
||||
const rowData = grider.getRowData(row);
|
||||
|
||||
return {
|
||||
row,
|
||||
rowData,
|
||||
condition: display?.getChangeSetCondition(rowData),
|
||||
insertedRowIndex: grider?.getInsertedRowIndex(row),
|
||||
rowStatus: grider.getRowStatus(row),
|
||||
isSelectedFullRow: fullRowIndexes.has(row),
|
||||
};
|
||||
});
|
||||
|
||||
const rowInfoByIndex = _.zipObject(
|
||||
rowIndexes.map(x => x.toString()),
|
||||
rowInfos
|
||||
);
|
||||
|
||||
const res = regular
|
||||
.map(cell => {
|
||||
const row = cell[0];
|
||||
const rowData = grider.getRowData(row);
|
||||
const column = realColumnUniqueNames[cell[1]];
|
||||
const rowData = rowInfoByIndex[row].rowData;
|
||||
|
||||
return {
|
||||
row,
|
||||
rowData,
|
||||
...commonInfo,
|
||||
...rowInfoByIndex[row],
|
||||
column,
|
||||
value: rowData && rowData[column],
|
||||
engine: display?.driver,
|
||||
condition: display?.getChangeSetCondition(rowData),
|
||||
insertedRowIndex: grider?.getInsertedRowIndex(row),
|
||||
rowStatus: grider.getRowStatus(row),
|
||||
onSetValue: value => grider.setCellValue(row, column, value),
|
||||
};
|
||||
})
|
||||
.filter(x => x.column);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function scrollIntoView(cell) {
|
||||
const [row, col] = cell;
|
||||
|
||||
if (row != null) {
|
||||
if (_.isNumber(row)) {
|
||||
let newRow = null;
|
||||
const rowCount = grider.rowCount;
|
||||
if (rowCount == 0) return;
|
||||
@@ -1281,7 +1327,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (col != null) {
|
||||
if (_.isNumber(col)) {
|
||||
if (col >= columnSizes.frozenCount) {
|
||||
let newColumn = columnSizes.scrollInView(
|
||||
firstVisibleColumnScrollIndex,
|
||||
@@ -1413,7 +1459,11 @@
|
||||
|
||||
function handleGridWheel(event) {
|
||||
if (event.shiftKey) {
|
||||
scrollHorizontal(event.deltaY, event.deltaX);
|
||||
if (isMac()) {
|
||||
scrollHorizontal(event.deltaX, event.deltaY);
|
||||
} else {
|
||||
scrollHorizontal(event.deltaY, event.deltaX);
|
||||
}
|
||||
} else {
|
||||
scrollHorizontal(event.deltaX, event.deltaY);
|
||||
scrollVertical(event.deltaX, event.deltaY);
|
||||
@@ -1507,7 +1557,11 @@
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
if (!isRegularCell(shiftDragStartCell)) {
|
||||
if (
|
||||
!isRegularCell(shiftDragStartCell) &&
|
||||
!isColumnHeaderCell(shiftDragStartCell) &&
|
||||
!isRowHeaderCell(shiftDragStartCell)
|
||||
) {
|
||||
shiftDragStartCell = currentCell;
|
||||
}
|
||||
} else {
|
||||
@@ -1535,7 +1589,13 @@
|
||||
}
|
||||
|
||||
function handleCursorMove(event) {
|
||||
if (!isRegularCell(currentCell)) return null;
|
||||
if (
|
||||
!isRegularCell(currentCell) &&
|
||||
!isColumnHeaderCell(currentCell) &&
|
||||
!isRowHeaderCell(currentCell) &&
|
||||
!isTableHeaderCell(currentCell)
|
||||
)
|
||||
return null;
|
||||
let rowCount = grider.rowCount;
|
||||
if (isCtrlOrCommandKey(event)) {
|
||||
switch (event.keyCode) {
|
||||
@@ -1562,24 +1622,36 @@
|
||||
switch (event.keyCode) {
|
||||
case keycodes.upArrow:
|
||||
if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
|
||||
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
|
||||
return _.isNumber(currentCell[0]) ? moveCurrentCell(currentCell[0] - 1, currentCell[1], event) : null;
|
||||
case keycodes.downArrow:
|
||||
return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
|
||||
if (currentCell[0] == 'header') return focusFilterEditor(currentCell[1]);
|
||||
return _.isNumber(currentCell[0]) ? moveCurrentCell(currentCell[0] + 1, currentCell[1], event) : null;
|
||||
case keycodes.enter:
|
||||
if (!grider.editable) return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
|
||||
if (!grider.editable)
|
||||
return _.isNumber(currentCell[0]) ? moveCurrentCell(currentCell[0] + 1, currentCell[1], event) : null;
|
||||
break;
|
||||
case keycodes.leftArrow:
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
|
||||
return _.isNumber(currentCell[1])
|
||||
? moveCurrentCell(currentCell[0], currentCell[1] == 0 ? 'header' : currentCell[1] - 1, event)
|
||||
: null;
|
||||
case keycodes.rightArrow:
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
|
||||
return currentCell[1] == 'header'
|
||||
? moveCurrentCell(currentCell[0], 0, event)
|
||||
: _.isNumber(currentCell[1])
|
||||
? moveCurrentCell(currentCell[0], currentCell[1] + 1, event)
|
||||
: null;
|
||||
case keycodes.home:
|
||||
return moveCurrentCell(currentCell[0], 0, event);
|
||||
case keycodes.end:
|
||||
return moveCurrentCell(currentCell[0], columnSizes.realCount - 1, event);
|
||||
case keycodes.pageUp:
|
||||
return moveCurrentCell(currentCell[0] - visibleRowCountLowerBound, currentCell[1], event);
|
||||
return _.isNumber(currentCell[0])
|
||||
? moveCurrentCell(currentCell[0] - visibleRowCountLowerBound, currentCell[1], event)
|
||||
: null;
|
||||
case keycodes.pageDown:
|
||||
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
|
||||
return _.isNumber(currentCell[0])
|
||||
? moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event)
|
||||
: null;
|
||||
case keycodes.tab: {
|
||||
return moveCurrentCellWithTabKey(event.shiftKey);
|
||||
}
|
||||
@@ -1613,10 +1685,14 @@
|
||||
function moveCurrentCell(row, col, event = null) {
|
||||
const rowCount = grider.rowCount;
|
||||
|
||||
if (row < 0) row = 0;
|
||||
if (row >= rowCount) row = rowCount - 1;
|
||||
if (col < 0) col = 0;
|
||||
if (col >= columnSizes.realCount) col = columnSizes.realCount - 1;
|
||||
if (_.isNumber(row)) {
|
||||
if (row < 0) row = 0;
|
||||
if (row >= rowCount) row = rowCount - 1;
|
||||
}
|
||||
if (_.isNumber(col)) {
|
||||
if (col < 0) col = 0;
|
||||
if (col >= columnSizes.realCount) col = columnSizes.realCount - 1;
|
||||
}
|
||||
currentCell = [row, col];
|
||||
// setSelectedCells([...(event.ctrlKey ? selectedCells : []), [row, col]]);
|
||||
selectedCells = [[row, col]];
|
||||
@@ -1736,6 +1812,17 @@
|
||||
if (domFocusField) domFocusField.focus();
|
||||
};
|
||||
|
||||
const selectColumnHeaderCell = uniquePath => {
|
||||
const modelIndex = columns.findIndex(x => x.uniquePath == uniquePath);
|
||||
const realIndex = columnSizes.modelToReal(modelIndex);
|
||||
let cell = ['header', realIndex];
|
||||
// @ts-ignore
|
||||
currentCell = cell;
|
||||
// @ts-ignore
|
||||
selectedCells = [cell];
|
||||
if (domFocusField) domFocusField.focus();
|
||||
};
|
||||
|
||||
const [inplaceEditorState, dispatchInsplaceEditor] = createReducer((state, action) => {
|
||||
switch (action.type) {
|
||||
case 'show':
|
||||
@@ -1788,15 +1875,15 @@
|
||||
{ command: 'dataGrid.refresh' },
|
||||
{ placeTag: 'copy' },
|
||||
{
|
||||
text: 'Copy advanced',
|
||||
text: _t('datagrid.copyAdvanced', { defaultMessage: 'Copy advanced' }),
|
||||
submenu: [
|
||||
_.keys(copyRowsFormatDefs).map(format => ({
|
||||
text: copyRowsFormatDefs[format].label,
|
||||
text: _tval(copyRowsFormatDefs[format].label),
|
||||
onClick: () => copyToClipboardCore(format),
|
||||
})),
|
||||
{ divider: true },
|
||||
_.keys(copyRowsFormatDefs).map(format => ({
|
||||
text: `Set format: ${copyRowsFormatDefs[format].name}`,
|
||||
text: _t('datagrid.setFormat', { defaultMessage: 'Set format: ' }) + _tval(copyRowsFormatDefs[format].name),
|
||||
onClick: () => ($copyRowsFormat = format),
|
||||
})),
|
||||
|
||||
@@ -1833,6 +1920,18 @@
|
||||
{ command: 'dataGrid.openJsonArrayInSheet', hideDisabled: true },
|
||||
{ command: 'dataGrid.saveCellToFile', hideDisabled: true },
|
||||
{ command: 'dataGrid.loadCellFromFile', hideDisabled: true },
|
||||
{ command: 'dataGrid.toggleCellDataView', hideDisabled: true },
|
||||
isProApp() && {
|
||||
text: _t('datagrid.useMacro', { defaultMessage: 'Use macro' }),
|
||||
submenu: macros
|
||||
.filter(macro => !macroCondition || macroCondition(macro))
|
||||
.map(macro => ({
|
||||
text: _tval(macro.title),
|
||||
onClick: () => {
|
||||
selectedMacro.set(macro);
|
||||
},
|
||||
})),
|
||||
},
|
||||
// { command: 'dataGrid.copyJsonDocument', hideDisabled: true },
|
||||
{ divider: true },
|
||||
{ placeTag: 'export' },
|
||||
@@ -1866,7 +1965,7 @@
|
||||
return [
|
||||
menu,
|
||||
{
|
||||
text: copyRowsFormatDefs[$copyRowsFormat].label,
|
||||
text: _tval(copyRowsFormatDefs[$copyRowsFormat].label),
|
||||
onClick: () => copyToClipboardCore($copyRowsFormat),
|
||||
keyText: 'CtrlOrCommand+C',
|
||||
tag: 'copy',
|
||||
@@ -1984,6 +2083,7 @@
|
||||
data-row="header"
|
||||
data-col={col.colIndex}
|
||||
style={`width:${col.width}px; min-width:${col.width}px; max-width:${col.width}px`}
|
||||
class:active-header-cell={currentCell && currentCell[0] == 'header' && currentCell[1] == col.colIndex}
|
||||
>
|
||||
<ColumnHeaderControl
|
||||
column={col}
|
||||
@@ -2003,6 +2103,7 @@
|
||||
grouping={display.getGrouping(col.uniqueName)}
|
||||
{allowDefineVirtualReferences}
|
||||
seachInColumns={display.config?.searchInColumns}
|
||||
onReload={refresh}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
@@ -2057,7 +2158,11 @@
|
||||
onFocusGrid={() => {
|
||||
selectTopmostCell(col.uniqueName);
|
||||
}}
|
||||
onFocusGridHeader={() => {
|
||||
selectColumnHeaderCell(col.uniqueName);
|
||||
}}
|
||||
dataType={col.dataType}
|
||||
filterDisabled={display.isFilterDisabled(col.uniqueName)}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
@@ -2182,6 +2287,9 @@
|
||||
background-color: var(--theme-bg-1);
|
||||
overflow: hidden;
|
||||
}
|
||||
:global(.data-grid-focused) .active-header-cell {
|
||||
background-color: var(--theme-bg-selected);
|
||||
}
|
||||
.filter-cell {
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
onShowForm={onSetFormView && !overlayDefinition ? () => onSetFormView(rowData, null) : null}
|
||||
extraIcon={overlayDefinition ? OVERLAY_STATUS_ICONS[rowStatus.status] : null}
|
||||
extraIconTooltip={overlayDefinition ? OVERLAY_STATUS_TOOLTIPS[rowStatus.status] : null}
|
||||
isSelected={frameSelection ? false : !!selectedCells?.find(cell => cell[0] == rowIndex && cell[1] == 'header')}
|
||||
/>
|
||||
{#each visibleRealColumns as col (col.uniqueName)}
|
||||
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'jslTableGrid.export',
|
||||
category: 'Data grid',
|
||||
name: 'Export',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
|
||||
icon: 'icon export',
|
||||
keyText: 'CtrlOrCommand+E',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
@@ -56,6 +56,7 @@
|
||||
|
||||
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import { __t } from '../translations';
|
||||
|
||||
export let jslid;
|
||||
export let display;
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
const nextRows = await loadDataPage(
|
||||
$$props,
|
||||
loadedRows.length,
|
||||
getIntSettingsValue('dataGrid.pageSize', 100, 5, 1000)
|
||||
getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)
|
||||
);
|
||||
if (loadedTimeRef.get() !== loadStart) {
|
||||
// new load was dispatched
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import TokenizedFilteredText from '../widgets/TokenizedFilteredText.svelte';
|
||||
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let managerSize;
|
||||
export let display: GridDisplay;
|
||||
export let onReferenceClick = ref => {};
|
||||
@@ -24,12 +26,12 @@
|
||||
</script>
|
||||
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search references" bind:value={filter} />
|
||||
<SearchInput placeholder={_t('dataGrid.searchReferences', { defaultMessage: 'Search references' })} bind:value={filter} />
|
||||
<CloseSearchButton bind:filter />
|
||||
</SearchBoxWrapper>
|
||||
<ManagerInnerContainer width={managerSize}>
|
||||
{#if foreignKeys.length > 0}
|
||||
<div class="bold nowrap ml-1">References tables ({foreignKeys.length})</div>
|
||||
<div class="bold nowrap ml-1">{_t('dataGrid.referencesTables', { defaultMessage: 'References tables' })} ({foreignKeys.length})</div>
|
||||
{#each foreignKeys.filter(fk => filterName(filter, fk.refTableName)) as fk}
|
||||
<div
|
||||
class="link"
|
||||
@@ -53,7 +55,7 @@
|
||||
{/if}
|
||||
|
||||
{#if dependencies.length > 0}
|
||||
<div class="bold nowrap ml-1">Dependend tables ({dependencies.length})</div>
|
||||
<div class="bold nowrap ml-1">{_t('dataGrid.dependentTables', { defaultMessage: 'Dependent tables' })} ({dependencies.length})</div>
|
||||
{#each dependencies.filter(fk => filterName(filter, fk.pureName)) as fk}
|
||||
<div
|
||||
class="link"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
export let extraIcon = null;
|
||||
export let extraIconTooltip = null;
|
||||
export let isSelected = false;
|
||||
|
||||
let mouseIn = false;
|
||||
</script>
|
||||
@@ -14,6 +15,7 @@
|
||||
<td
|
||||
data-row={rowIndex}
|
||||
data-col="header"
|
||||
class:selected={isSelected}
|
||||
on:mouseenter={() => (mouseIn = true)}
|
||||
on:mouseleave={() => (mouseIn = false)}
|
||||
>
|
||||
@@ -43,4 +45,7 @@
|
||||
right: 0px;
|
||||
top: 1px;
|
||||
}
|
||||
:global(.data-grid-focused) td.selected {
|
||||
background-color: var(--theme-bg-selected);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
<script context="module" lang="ts">
|
||||
import { __t, _t } from '../translations'
|
||||
const getCurrentEditor = () => getActiveComponent('SqlDataGridCore');
|
||||
|
||||
registerCommand({
|
||||
id: 'sqlDataGrid.openQuery',
|
||||
category: 'Data grid',
|
||||
name: 'Open query',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.openQuery', { defaultMessage : 'Open query' }),
|
||||
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
|
||||
onClick: () => getCurrentEditor().openQuery(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'sqlDataGrid.export',
|
||||
category: 'Data grid',
|
||||
name: 'Export',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('common.export', { defaultMessage : 'Export' }),
|
||||
icon: 'icon export',
|
||||
keyText: 'CtrlOrCommand+E',
|
||||
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
|
||||
@@ -126,7 +127,7 @@
|
||||
export function openQuery(sql?) {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query #',
|
||||
title: _t('common.queryNumber', { defaultMessage: 'Query #' }),
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'QueryTab',
|
||||
focused: true,
|
||||
@@ -144,7 +145,7 @@
|
||||
}
|
||||
|
||||
function openQueryOnError() {
|
||||
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 1000)));
|
||||
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
|
||||
}
|
||||
|
||||
const quickExportHandler = fmt => async () => {
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
import {
|
||||
useAllApps,
|
||||
useConnectionInfo,
|
||||
useConnectionList,
|
||||
useDatabaseInfo,
|
||||
useDatabaseServerVersion,
|
||||
useServerVersion,
|
||||
useSettings,
|
||||
useUsedApps,
|
||||
} from '../utility/metadataLoaders';
|
||||
|
||||
import DataGrid from './DataGrid.svelte';
|
||||
@@ -30,6 +30,7 @@
|
||||
import SqlFormView from '../formview/SqlFormView.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -53,7 +54,7 @@
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
|
||||
$: connections = useConnectionList();
|
||||
const settingsValue = useSettings();
|
||||
@@ -77,9 +78,13 @@
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion,
|
||||
table => getDictionaryDescription(table, conid, database, $apps, $connections),
|
||||
forceReadOnly || $connection?.isReadOnly,
|
||||
forceReadOnly ||
|
||||
$connection?.isReadOnly ||
|
||||
extendedDbInfo?.tables?.find(x => x.pureName == pureName && x.schemaName == schemaName)
|
||||
?.tablePermissionRole == 'read',
|
||||
isRawMode,
|
||||
$settingsValue
|
||||
$settingsValue,
|
||||
isProApp()
|
||||
)
|
||||
: null;
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ export function countColumnSizes(grider: Grider, columns, containerWidth, displa
|
||||
if (_.isArray(value)) text = `[${value.length} items]`;
|
||||
else if (value?.$oid) text = `ObjectId("${value.$oid}")`;
|
||||
else if (value?.$bigint) text = value.$bigint;
|
||||
else if (value?.$decimal) text = value.$decimal;
|
||||
else if (isJsonLikeLongString(value) && safeJsonParse(value)) text = '(JSON)';
|
||||
const width = context.measureText(typeof text == 'string' ? text.slice(0, MAX_GRID_TEXT_LENGTH) : text).width + 8;
|
||||
// console.log('colName', colName, text, width);
|
||||
|
||||
@@ -13,6 +13,24 @@ export function isRegularCell(cell: CellAddress): cell is RegularCellAddress {
|
||||
return _.isNumber(row) && _.isNumber(col);
|
||||
}
|
||||
|
||||
export function isRowHeaderCell(cell: CellAddress): boolean {
|
||||
if (!cell) return false;
|
||||
const [row, col] = cell;
|
||||
return col === 'header' && _.isNumber(row);
|
||||
}
|
||||
|
||||
export function isColumnHeaderCell(cell: CellAddress): boolean {
|
||||
if (!cell) return false;
|
||||
const [row, col] = cell;
|
||||
return row === 'header' && _.isNumber(col);
|
||||
}
|
||||
|
||||
export function isTableHeaderCell(cell: CellAddress): boolean {
|
||||
if (!cell) return false;
|
||||
const [row, col] = cell;
|
||||
return row === 'header' && col === 'header';
|
||||
}
|
||||
|
||||
function normalizeHeaderForSelection(addr: CellAddress): CellAddress {
|
||||
if (addr[0] == 'filter') return ['header', addr[1]];
|
||||
return addr;
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'designer.arrange',
|
||||
category: 'Designer',
|
||||
category: __t('command.designer', { defaultMessage: 'Designer' }),
|
||||
icon: 'icon arrange',
|
||||
name: 'Arrange',
|
||||
name: __t('command.designer.arrange', { defaultMessage: 'Arrange' }),
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => getCurrentEditor()?.canArrange(),
|
||||
@@ -15,9 +15,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'diagram.export',
|
||||
category: 'Designer',
|
||||
toolbarName: 'Export diagram',
|
||||
name: 'Export diagram',
|
||||
category: __t('command.designer', { defaultMessage: 'Designer' }),
|
||||
toolbarName: __t('command.designer.exportDiagram', { defaultMessage: 'Export diagram' }),
|
||||
name: __t('command.designer.exportDiagram', { defaultMessage: 'Export diagram' }),
|
||||
icon: 'icon report',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -27,9 +27,9 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'diagram.deleteSelectedTables',
|
||||
category: 'Designer',
|
||||
toolbarName: 'Remove',
|
||||
name: 'Remove selected tables',
|
||||
category: __t('command.designer', { defaultMessage: 'Designer' }),
|
||||
toolbarName: __t('command.designer.remove', { defaultMessage: 'Remove' }),
|
||||
name: __t('command.designer.removeSelectedTables', { defaultMessage: 'Remove selected tables' }),
|
||||
icon: 'icon delete',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -42,7 +42,7 @@
|
||||
import DesignerTable from './DesignerTable.svelte';
|
||||
import { isConnectedByReference } from './designerTools';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getTableInfo, useAllApps, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||
import _ from 'lodash';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -67,6 +67,7 @@
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import dragScroll from '../utility/dragScroll';
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import { __t, _t } from '../translations';
|
||||
|
||||
export let value;
|
||||
export let onChange;
|
||||
@@ -108,7 +109,7 @@
|
||||
ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId)
|
||||
) as any[];
|
||||
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
|
||||
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
||||
|
||||
@@ -848,45 +849,45 @@
|
||||
settings?.customizeStyle && [
|
||||
{ divider: true },
|
||||
isProApp() && {
|
||||
text: 'Column properties',
|
||||
text: _t('designer.columnProperties', { defaultMessage: 'Column properties' }),
|
||||
submenu: [
|
||||
{
|
||||
text: `Nullability: ${value?.style?.showNullability ? 'YES' : 'NO'}`,
|
||||
text: _t('designer.nullabilityYesNo', { defaultMessage: 'Nullability: {show}', values: { show: value?.style?.showNullability ? 'YES' : 'NO' } }),
|
||||
onClick: changeStyleFunc('showNullability', !value?.style?.showNullability),
|
||||
},
|
||||
{
|
||||
text: `Data type: ${value?.style?.showDataType ? 'YES' : 'NO'}`,
|
||||
text: _t('designer.dataTypeYesNo', { defaultMessage: 'Data type: {show}', values: { show: value?.style?.showDataType ? 'YES' : 'NO' } }),
|
||||
onClick: changeStyleFunc('showDataType', !value?.style?.showDataType),
|
||||
},
|
||||
],
|
||||
},
|
||||
isProApp() && {
|
||||
text: `Columns - ${_.startCase(value?.style?.filterColumns || 'all')}`,
|
||||
text: _t('designer.columns', { defaultMessage: 'Columns - { filterColumns }', values: { filterColumns: _.startCase(value?.style?.filterColumns || 'all') } }),
|
||||
submenu: [
|
||||
{
|
||||
text: 'All',
|
||||
text: _t('designer.all', { defaultMessage: 'All' }),
|
||||
onClick: changeStyleFunc('filterColumns', ''),
|
||||
},
|
||||
{
|
||||
text: 'Primary Key',
|
||||
text: _t('designer.primaryKey', { defaultMessage: 'Primary Key' }),
|
||||
onClick: changeStyleFunc('filterColumns', 'primaryKey'),
|
||||
},
|
||||
{
|
||||
text: 'All Keys',
|
||||
text: _t('designer.allKeys', { defaultMessage: 'All Keys' }),
|
||||
onClick: changeStyleFunc('filterColumns', 'allKeys'),
|
||||
},
|
||||
{
|
||||
text: 'Not Null',
|
||||
text: _t('designer.notNull', { defaultMessage: 'Not Null' }),
|
||||
onClick: changeStyleFunc('filterColumns', 'notNull'),
|
||||
},
|
||||
{
|
||||
text: 'Keys And Not Null',
|
||||
text: _t('designer.keysAndNotNull', { defaultMessage: 'Keys And Not Null' }),
|
||||
onClick: changeStyleFunc('filterColumns', 'keysAndNotNull'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
text: `Zoom - ${(value?.style?.zoomKoef || 1) * 100}%`,
|
||||
text: _t('designer.zoom', { defaultMessage: 'Zoom - {zoom}%', values: { zoom: ((value?.style?.zoomKoef || 1) * 100) } }),
|
||||
submenu: DIAGRAM_ZOOMS.map(koef => ({
|
||||
text: `${koef * 100} %`,
|
||||
onClick: changeStyleFunc('zoomKoef', koef.toString()),
|
||||
@@ -1015,11 +1016,11 @@
|
||||
use:dragScroll={handleDragScroll}
|
||||
>
|
||||
{#if !(tables?.length > 0)}
|
||||
<div class="empty">Drag & drop tables or views from left panel here</div>
|
||||
<div class="empty">{_t('designer.dragDropTables', { defaultMessage: 'Drag & drop tables or views from left panel here' })}</div>
|
||||
|
||||
{#if allowAddTablesButton}
|
||||
<div class="addAllTables">
|
||||
<FormStyledButton value="Add all tables" on:click={handleAddAllTables} />
|
||||
<FormStyledButton value={_t('designer.addAllTables', { defaultMessage: 'Add all tables' })} on:click={handleAddAllTables} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1118,7 +1119,7 @@
|
||||
<div class="panel">
|
||||
<DragColumnMemory {settings} {sourceDragColumn$} {targetDragColumn$} />
|
||||
<div class="searchbox">
|
||||
<SearchInput bind:value={columnFilter} placeholder="Filter columns" />
|
||||
<SearchInput bind:value={columnFilter} placeholder={_t('designer.filterColumns', { defaultMessage: 'Filter columns' })} />
|
||||
<CloseSearchButton bind:filter={columnFilter} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import moveDrag from '../utility/moveDrag';
|
||||
import ColumnLine from './ColumnLine.svelte';
|
||||
import DomTableRef from './DomTableRef';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -185,8 +186,8 @@
|
||||
const handleSetTableAlias = () => {
|
||||
showModal(InputTextModal, {
|
||||
value: alias || '',
|
||||
label: 'New alias',
|
||||
header: 'Set table alias',
|
||||
label: _t('designerTable.newAlias', { defaultMessage: 'New alias' }),
|
||||
header: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }),
|
||||
onConfirm: newAlias => {
|
||||
onChangeTable({
|
||||
...table,
|
||||
@@ -210,13 +211,13 @@
|
||||
return settings?.tableMenu({ designer, designerId, onRemoveTable });
|
||||
}
|
||||
return [
|
||||
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
|
||||
{ text: _t('common.remove', { defaultMessage: 'Remove' }), onClick: () => onRemoveTable({ designerId }) },
|
||||
{ divider: true },
|
||||
settings?.allowTableAlias &&
|
||||
!isMultipleTableSelection && [
|
||||
{ text: 'Set table alias', onClick: handleSetTableAlias },
|
||||
{ text: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }), onClick: handleSetTableAlias },
|
||||
alias && {
|
||||
text: 'Remove table alias',
|
||||
text: _t('designerTable.removeTableAlias', { defaultMessage: 'Remove table alias' }),
|
||||
onClick: () =>
|
||||
onChangeTable({
|
||||
...table,
|
||||
@@ -225,11 +226,11 @@
|
||||
},
|
||||
],
|
||||
settings?.allowAddAllReferences &&
|
||||
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
|
||||
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
|
||||
!isMultipleTableSelection && { text: _t('designerTable.addReferences', { defaultMessage: 'Add references' }), onClick: () => onAddAllReferences(table) },
|
||||
settings?.allowChangeColor && { text: _t('designerTable.changeColor', { defaultMessage: 'Change color' }), onClick: () => onChangeTableColor(table) },
|
||||
settings?.allowDefineVirtualReferences &&
|
||||
!isMultipleTableSelection && {
|
||||
text: 'Define virtual foreign key',
|
||||
text: _t('designerTable.defineVirtualForeignKey', { defaultMessage: 'Define virtual foreign key' }),
|
||||
onClick: () => handleDefineVirtualForeignKey(table),
|
||||
},
|
||||
settings?.appendTableSystemMenu &&
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import ObjectListControl from '../elements/ObjectListControl.svelte';
|
||||
import Link from './Link.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let collection;
|
||||
export let title;
|
||||
@@ -24,18 +25,18 @@
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'baseColumns',
|
||||
header: 'Base columns',
|
||||
header: _t('foreignKey.baseColumns', { defaultMessage: 'Base columns' }),
|
||||
slot: 0,
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'refTableName',
|
||||
header: 'Referenced table',
|
||||
header: _t('foreignKey.refTableName', { defaultMessage: 'Referenced table' }),
|
||||
sortable: true,
|
||||
},
|
||||
{
|
||||
fieldName: 'refColumns',
|
||||
header: 'Referenced columns',
|
||||
header: _t('foreignKey.refColumns', { defaultMessage: 'Referenced columns' }),
|
||||
slot: 1,
|
||||
sortable: true,
|
||||
},
|
||||
@@ -60,5 +61,5 @@
|
||||
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
|
||||
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>{row?.columns.map(x => x.refColumnName).join(', ')}</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row><Link onClick={() => onRemove(row)}>Remove</Link></svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row><Link onClick={() => onRemove(row)}>{_t('common.remove', { defaultMessage: 'Remove' })}</Link></svelte:fragment>
|
||||
</ObjectListControl>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import Link from './Link.svelte';
|
||||
import TableControl from './TableControl.svelte';
|
||||
import { writable } from 'svelte/store';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let title;
|
||||
export let collection;
|
||||
@@ -39,7 +40,7 @@
|
||||
</span>
|
||||
<span class="title mr-1">{title}</span>
|
||||
{#if onAddNew}
|
||||
<Link onClick={onAddNew}><FontIcon icon="icon add" /> Add new</Link>
|
||||
<Link onClick={onAddNew}><FontIcon icon="icon add" />{_t('common.addNew', { defaultMessage: 'Add new' })}</Link>
|
||||
{/if}
|
||||
{#if multipleItemsActions && activeMultipleSelection && activeMultipleSelection?.length > 0}
|
||||
{#each multipleItemsActions as item}
|
||||
@@ -65,7 +66,7 @@
|
||||
columns={_.compact([
|
||||
!hideDisplayName && {
|
||||
fieldName: displayNameFieldName || 'displayName',
|
||||
header: 'Name',
|
||||
header: _t('common.name', { defaultMessage: 'Name' }),
|
||||
slot: -1,
|
||||
sortable: true,
|
||||
filterable: !!displayNameFieldName,
|
||||
|
||||
@@ -1,4 +1,44 @@
|
||||
<script lang="ts" context="module">
|
||||
const LAT_PRIORITY_PATTERNS = [
|
||||
/^lat$/i,
|
||||
/^latitude$/i,
|
||||
/latitude$/i,
|
||||
/lat$/i,
|
||||
/latitude/i,
|
||||
/lat/i,
|
||||
];
|
||||
|
||||
const LON_PRIORITY_PATTERNS = [
|
||||
/^lon$/i,
|
||||
/^lng$/i,
|
||||
/^longitude$/i,
|
||||
/longitude$/i,
|
||||
/lon$/i,
|
||||
/lng$/i,
|
||||
/longitude/i,
|
||||
/lon|lng/i,
|
||||
];
|
||||
|
||||
function getFieldName(fieldPath) {
|
||||
return fieldPath.split('.').pop() || fieldPath;
|
||||
}
|
||||
|
||||
function getFieldPriority(fieldPath, patterns) {
|
||||
const name = getFieldName(fieldPath);
|
||||
for (let i = 0; i < patterns.length; i++) {
|
||||
if (patterns[i].test(name)) return i;
|
||||
}
|
||||
return patterns.length;
|
||||
}
|
||||
|
||||
function sortByPriorityThenLength(paths, patterns) {
|
||||
return paths.sort((a, b) => {
|
||||
const priorityDiff = getFieldPriority(a, patterns) - getFieldPriority(b, patterns);
|
||||
if (priorityDiff !== 0) return priorityDiff;
|
||||
return getFieldName(a).length - getFieldName(b).length;
|
||||
});
|
||||
}
|
||||
|
||||
function findLatLonPaths(obj, attrTest, res = [], prefix = '') {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (attrTest(key, obj[key])) {
|
||||
@@ -10,11 +50,15 @@
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export function findLatPaths(obj) {
|
||||
return findLatLonPaths(obj, x => x.toLowerCase()?.includes('lat'));
|
||||
const paths = findLatLonPaths(obj, x => x.toLowerCase()?.includes('lat'));
|
||||
return sortByPriorityThenLength(paths, LAT_PRIORITY_PATTERNS);
|
||||
}
|
||||
|
||||
export function findLonPaths(obj) {
|
||||
return findLatLonPaths(obj, x => x.toLowerCase()?.includes('lon') || x.toLowerCase()?.includes('lng'));
|
||||
const paths = findLatLonPaths(obj, x => x.toLowerCase()?.includes('lon') || x.toLowerCase()?.includes('lng'));
|
||||
return sortByPriorityThenLength(paths, LON_PRIORITY_PATTERNS);
|
||||
}
|
||||
export function findAllObjectPaths(obj) {
|
||||
return findLatLonPaths(obj, (_k, v) => v != null && !_.isNaN(Number(v)));
|
||||
|
||||
170
packages/web/src/elements/SettingsMenuControl.svelte
Normal file
170
packages/web/src/elements/SettingsMenuControl.svelte
Normal file
@@ -0,0 +1,170 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import HorizontalSplitter from './HorizontalSplitter.svelte';
|
||||
|
||||
interface MenuItemDef {
|
||||
label: string;
|
||||
slot?: number;
|
||||
component?: any;
|
||||
props?: any;
|
||||
testid?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
export let items: MenuItemDef[];
|
||||
export let value: string | number = 0;
|
||||
export let containerMaxWidth = undefined;
|
||||
export let containerMaxHeight = undefined;
|
||||
export let flex1 = true;
|
||||
export let flexColContainer = false;
|
||||
export let maxHeight100 = false;
|
||||
export let scrollableContentContainer = false;
|
||||
export let contentTestId = undefined;
|
||||
export let onUserChange = null;
|
||||
|
||||
export function setValue(index) {
|
||||
value = index;
|
||||
}
|
||||
export function getValue() {
|
||||
return value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="main" class:maxHeight100 class:flex1>
|
||||
<HorizontalSplitter initialValue="170px">
|
||||
<svelte:fragment slot="1">
|
||||
<div class="menu">
|
||||
{#each _.compact(items) as item, index}
|
||||
<div
|
||||
class="menu-item"
|
||||
class:selected={value == (item.identifier ?? index)}
|
||||
on:click={() => {
|
||||
value = item.identifier ?? index;
|
||||
onUserChange?.(item.identifier ?? index);
|
||||
}}
|
||||
data-testid={item.testid}
|
||||
>
|
||||
<span class="ml-2 noselect">
|
||||
{item.label}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="2">
|
||||
<div
|
||||
class="content-container"
|
||||
class:scrollableContentContainer
|
||||
style:max-height={containerMaxHeight}
|
||||
data-testid={contentTestId}
|
||||
>
|
||||
{#each _.compact(items) as item, index}
|
||||
<div
|
||||
class="container"
|
||||
class:flexColContainer
|
||||
class:maxHeight100
|
||||
class:itemVisible={(item.identifier ?? index) == value}
|
||||
style:max-width={containerMaxWidth}
|
||||
>
|
||||
<svelte:component
|
||||
this={item.component}
|
||||
{...item.props}
|
||||
itemVisible={(item.identifier ?? index) == value}
|
||||
menuControlHiddenItem={(item.identifier ?? index) != value}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</HorizontalSplitter>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.main.flex1 {
|
||||
flex: 1;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.main.maxHeight100 {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--theme-bg-2);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.menu::-webkit-scrollbar {
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
white-space: nowrap;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-item:first-child {
|
||||
border-top: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
.menu-item.selected {
|
||||
background-color: var(--theme-bg-1);
|
||||
font-weight: 600;
|
||||
border-left: 3px solid var(--theme-font-link);
|
||||
}
|
||||
|
||||
.content-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
background-color: var(--theme-bg-0);
|
||||
}
|
||||
|
||||
.scrollableContentContainer {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.container.maxHeight100 {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.container.flexColContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container:not(.itemVisible) {
|
||||
visibility: hidden;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,8 @@
|
||||
import { onMount, afterUpdate } from 'svelte';
|
||||
|
||||
export let code = '';
|
||||
export let inline = false;
|
||||
export let onClick = null;
|
||||
|
||||
let domCode;
|
||||
|
||||
@@ -29,7 +31,11 @@
|
||||
The `sql` class hints the language; highlight.js will
|
||||
read it even though we register the grammar explicitly.
|
||||
-->
|
||||
<pre bind:this={domCode} class="sql">{code}</pre>
|
||||
{#if inline}
|
||||
<span bind:this={domCode} class="sql" class:clickable={!!onClick} on:click={onClick}>{code}</span>
|
||||
{:else}
|
||||
<pre bind:this={domCode} class="sql" class:clickable={!!onClick} on:click={onClick}>{code}</pre>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
@@ -38,4 +44,8 @@
|
||||
padding: 0;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import DropDownButton from '../buttons/DropDownButton.svelte';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
interface TabDef {
|
||||
label: string;
|
||||
@@ -8,14 +9,19 @@
|
||||
component?: any;
|
||||
props?: any;
|
||||
testid?: string;
|
||||
identifier?: string;
|
||||
}
|
||||
|
||||
export let tabs: TabDef[];
|
||||
export let value = 0;
|
||||
export let value: string | number = 0;
|
||||
export let menu = null;
|
||||
export let isInline = false;
|
||||
export let containerMaxWidth = undefined;
|
||||
export let containerMaxHeight = undefined;
|
||||
export let flex1 = true;
|
||||
export let flexColContainer = false;
|
||||
export let maxHeight100 = false;
|
||||
export let scrollableContentContainer = false;
|
||||
export let contentTestId = undefined;
|
||||
export let inlineTabs = false;
|
||||
export let onUserChange = null;
|
||||
@@ -28,20 +34,20 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="main" class:flex1>
|
||||
<div class="main" class:maxHeight100 class:flex1>
|
||||
<div class="tabs" class:inlineTabs>
|
||||
{#each _.compact(tabs) as tab, index}
|
||||
<div
|
||||
class="tab-item"
|
||||
class:selected={value == index}
|
||||
class:selected={value == (tab.identifier ?? index)}
|
||||
on:click={() => {
|
||||
value = index;
|
||||
onUserChange?.(index);
|
||||
value = tab.identifier ?? index;
|
||||
onUserChange?.(tab.identifier ?? index);
|
||||
}}
|
||||
data-testid={tab.testid}
|
||||
>
|
||||
<span class="ml-2 noselect">
|
||||
{tab.label}
|
||||
{_tval(tab.label)}
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -50,10 +56,27 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content-container" data-testid={contentTestId}>
|
||||
<div
|
||||
class="content-container"
|
||||
class:scrollableContentContainer
|
||||
style:max-height={containerMaxHeight}
|
||||
data-testid={contentTestId}
|
||||
>
|
||||
{#each _.compact(tabs) as tab, index}
|
||||
<div class="container" class:isInline class:tabVisible={index == value} style:max-width={containerMaxWidth}>
|
||||
<svelte:component this={tab.component} {...tab.props} tabControlHiddenTab={index != value} />
|
||||
<div
|
||||
class="container"
|
||||
class:flexColContainer
|
||||
class:maxHeight100
|
||||
class:isInline
|
||||
class:tabVisible={(tab.identifier ?? index) == value}
|
||||
style:max-width={containerMaxWidth}
|
||||
>
|
||||
<svelte:component
|
||||
this={tab.component}
|
||||
{...tab.props}
|
||||
tabVisible={(tab.identifier ?? index) == value}
|
||||
tabControlHiddenTab={(tab.identifier ?? index) != value}
|
||||
/>
|
||||
{#if tab.slot != null}
|
||||
{#if tab.slot == 0}<slot name="0" />
|
||||
{:else if tab.slot == 1}<slot name="1" />
|
||||
@@ -83,6 +106,10 @@
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.main.maxHeight100 {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
height: var(--dim-tabs-height);
|
||||
@@ -109,6 +136,7 @@
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
white-space: nowrap;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
display: flex;
|
||||
@@ -132,6 +160,19 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.scrollableContentContainer {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.container.maxHeight100 {
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.container.flexColContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container:not(.isInline) {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import { evaluateCondition } from 'dbgate-sqltree';
|
||||
import { compileCompoudEvalCondition } from 'dbgate-filterparser';
|
||||
import { chevronExpandIcon } from '../icons/expandIcons';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let columns: (TableControlColumn | false)[];
|
||||
export let rows = null;
|
||||
@@ -199,6 +200,7 @@
|
||||
tabindex={selectable ? -1 : undefined}
|
||||
on:keydown={handleKeyDown}
|
||||
class:stickyHeader
|
||||
data-testid={$$props['data-testid']}
|
||||
>
|
||||
<thead class:stickyHeader>
|
||||
<tr>
|
||||
@@ -350,7 +352,7 @@
|
||||
{#if col.component}
|
||||
<svelte:component this={col.component} {...rowProps} />
|
||||
{:else if col.formatter}
|
||||
{col.formatter(row)}
|
||||
{col.formatter(row, col)}
|
||||
{:else if col.slot != null}
|
||||
{#key row[col.slotKey] || 'key'}
|
||||
{#if col.slot == -1}<slot name="-1" {row} {col} {index} />
|
||||
@@ -367,7 +369,7 @@
|
||||
{/if}
|
||||
{/key}
|
||||
{:else}
|
||||
{row[col.fieldName] || ''}
|
||||
{ _tval(row[col.fieldName]) || '' }
|
||||
{/if}
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
export let isSplitter = true;
|
||||
export let initialValue = undefined;
|
||||
export let hideFirst = false;
|
||||
|
||||
export let allowCollapseChild1 = false;
|
||||
export let allowCollapseChild2 = false;
|
||||
@@ -22,28 +23,32 @@
|
||||
</script>
|
||||
|
||||
<div class="container" bind:clientHeight>
|
||||
<div
|
||||
class="child1"
|
||||
style={isSplitter
|
||||
? collapsed1
|
||||
? 'display:none'
|
||||
: collapsed2
|
||||
? 'flex:1'
|
||||
: `height:${size}px; min-height:${size}px; max-height:${size}px}`
|
||||
: `flex:1`}
|
||||
>
|
||||
<slot name="1" />
|
||||
</div>
|
||||
{#if isSplitter}
|
||||
{#if !hideFirst}
|
||||
<div
|
||||
class={'vertical-split-handle'}
|
||||
style={collapsed1 || collapsed2 ? 'display:none' : ''}
|
||||
use:splitterDrag={'clientY'}
|
||||
on:resizeSplitter={e => {
|
||||
size += e.detail;
|
||||
if (clientHeight > 0) customRatio = size / clientHeight;
|
||||
}}
|
||||
/>
|
||||
class="child1"
|
||||
style={isSplitter
|
||||
? collapsed1
|
||||
? 'display:none'
|
||||
: collapsed2
|
||||
? 'flex:1'
|
||||
: `height:${size}px; min-height:${size}px; max-height:${size}px}`
|
||||
: `flex:1`}
|
||||
>
|
||||
<slot name="1" />
|
||||
</div>
|
||||
{/if}
|
||||
{#if isSplitter}
|
||||
{#if !hideFirst}
|
||||
<div
|
||||
class={'vertical-split-handle'}
|
||||
style={collapsed1 || collapsed2 ? 'display:none' : ''}
|
||||
use:splitterDrag={'clientY'}
|
||||
on:resizeSplitter={e => {
|
||||
size += e.detail;
|
||||
if (clientHeight > 0) customRatio = size / clientHeight;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div
|
||||
class={collapsed1 ? 'child1' : 'child2'}
|
||||
style={collapsed2 ? 'display:none' : collapsed1 ? 'flex:1' : 'child2'}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
export let label;
|
||||
|
||||
export let disabled = false;
|
||||
|
||||
$: renderedValue = value ?? inheritedValue;
|
||||
$: isInherited = inheritedValue != null && value == null;
|
||||
|
||||
@@ -30,11 +32,12 @@
|
||||
<div
|
||||
class="wrapper"
|
||||
on:click|preventDefault|stopPropagation={() => {
|
||||
if (disabled) return;
|
||||
onChange(getNextValue());
|
||||
}}
|
||||
>
|
||||
<div class="checkbox" {...$$restProps} class:checked={!!renderedValue} class:isInherited />
|
||||
<div class="label">
|
||||
<div class="checkbox" {...$$restProps} class:checked={!!renderedValue} class:isInherited class:disabled />
|
||||
<div class="label" class:disabled>
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
@@ -51,6 +54,11 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.label.disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--theme-font-3);
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
@@ -76,4 +84,9 @@
|
||||
.isInherited {
|
||||
background: var(--theme-bg-2) !important;
|
||||
}
|
||||
|
||||
.checkbox.disabled {
|
||||
background: var(--theme-bg-2) !important;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
|
||||
import FormSelectField from './FormSelectField.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let folderName;
|
||||
export let name;
|
||||
@@ -28,10 +29,10 @@
|
||||
<div>
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value="All files"
|
||||
value={_t('common.allFiles', { defaultMessage: "All files" })}
|
||||
on:click={() => setFieldValue(name, _.uniq([...($values[name] || []), ...($files && $files.map(x => x.name))]))}
|
||||
/>
|
||||
<FormStyledButton type="button" value="Remove all" on:click={() => setFieldValue(name, [])} />
|
||||
<FormStyledButton type="button" value={_t('common.removeAll', { defaultMessage: "Remove all" })} on:click={() => setFieldValue(name, [])} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
|
||||
import FormSelectField from './FormSelectField.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let additionalFolders = [];
|
||||
export let name;
|
||||
@@ -35,7 +36,7 @@
|
||||
label: folder,
|
||||
})),
|
||||
allowCreateNew && {
|
||||
label: '(Create new)',
|
||||
label: _t('archiveFolder.createNew', { defaultMessage: '(Create new)' }),
|
||||
value: '@create',
|
||||
},
|
||||
];
|
||||
@@ -48,8 +49,8 @@
|
||||
function handleChange(e) {
|
||||
if (e.detail == '@create') {
|
||||
showModal(InputTextModal, {
|
||||
header: 'Archive',
|
||||
label: 'Name of new archive folder',
|
||||
header: _t('archiveFolder.archive', { defaultMessage: 'Archive' }),
|
||||
label: _t('archiveFolder.nameOfNewArchiveFolder', { defaultMessage: 'Name of new archive folder' }),
|
||||
onConfirm: createOption,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
import FormStringList from './FormStringList.svelte';
|
||||
import FormDropDownTextField from './FormDropDownTextField.svelte';
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let arg;
|
||||
export let namePrefix;
|
||||
export let isReadOnly = false;
|
||||
|
||||
$: name = `${namePrefix}${arg.name}`;
|
||||
|
||||
@@ -18,46 +20,52 @@
|
||||
|
||||
{#if arg.type == 'text'}
|
||||
<FormTextField
|
||||
label={arg.label}
|
||||
label={_tval(arg.label)}
|
||||
{name}
|
||||
defaultValue={arg.default}
|
||||
focused={arg.focused}
|
||||
placeholder={arg.placeholder}
|
||||
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
|
||||
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
|
||||
/>
|
||||
{:else if arg.type == 'stringlist'}
|
||||
<FormStringList label={arg.label} addButtonLabel={arg.addButtonLabel} {name} placeholder={arg.placeholder} />
|
||||
<FormStringList
|
||||
label={_tval(arg.label)}
|
||||
addButtonLabel={_tval(arg.addButtonLabel)}
|
||||
{name}
|
||||
placeholder={arg.placeholder}
|
||||
isReadOnly={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
|
||||
/>
|
||||
{:else if arg.type == 'number'}
|
||||
<FormTextField
|
||||
label={arg.label}
|
||||
label={_tval(arg.label)}
|
||||
type="number"
|
||||
{name}
|
||||
defaultValue={arg.default}
|
||||
focused={arg.focused}
|
||||
placeholder={arg.placeholder}
|
||||
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
|
||||
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
|
||||
/>
|
||||
{:else if arg.type == 'checkbox'}
|
||||
<FormCheckboxField
|
||||
label={arg.label}
|
||||
label={_tval(arg.label)}
|
||||
{name}
|
||||
defaultValue={arg.default}
|
||||
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
|
||||
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
|
||||
/>
|
||||
{:else if arg.type == 'select'}
|
||||
<FormSelectField
|
||||
label={arg.label}
|
||||
label={_tval(arg.label)}
|
||||
isNative
|
||||
{name}
|
||||
defaultValue={arg.default}
|
||||
options={arg.options.map(opt =>
|
||||
_.isString(opt) ? { label: opt, value: opt } : { label: opt.name, value: opt.value }
|
||||
)}
|
||||
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
|
||||
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
|
||||
/>
|
||||
{:else if arg.type == 'dropdowntext'}
|
||||
<FormDropDownTextField
|
||||
label={arg.label}
|
||||
label={_tval(arg.label)}
|
||||
{name}
|
||||
defaultValue={arg.default}
|
||||
menu={() => {
|
||||
@@ -66,6 +74,6 @@
|
||||
onClick: () => setFieldValue(name, _.isString(opt) ? opt : opt.value),
|
||||
}));
|
||||
}}
|
||||
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
|
||||
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
|
||||
export let namePrefix = '';
|
||||
export let args: any[];
|
||||
export let isReadOnly = false;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#each args as arg (arg.name)}
|
||||
<FormArgument {arg} {namePrefix} />
|
||||
<FormArgument {arg} {namePrefix} {isReadOnly} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
1
packages/web/src/forms/FormClusterNodesField.svelte
Normal file
1
packages/web/src/forms/FormClusterNodesField.svelte
Normal file
@@ -0,0 +1 @@
|
||||
This file is part of DbGate Premium
|
||||
@@ -1,9 +1,12 @@
|
||||
<script lang="ts">
|
||||
import FontIcon from "../icons/FontIcon.svelte";
|
||||
|
||||
export let type;
|
||||
export let label;
|
||||
export let noMargin = false;
|
||||
export let disabled = false;
|
||||
export let labelProps: any = {};
|
||||
export let labelIcon = null;
|
||||
</script>
|
||||
|
||||
<div class="largeFormMarker" class:noMargin>
|
||||
@@ -12,6 +15,9 @@
|
||||
<span {...labelProps} on:click={labelProps.onClick} class:disabled class='checkLabel'>{label}</span>
|
||||
{:else}
|
||||
<div class="label" {...labelProps} on:click={labelProps.onClick}>
|
||||
{#if labelIcon}
|
||||
<FontIcon icon={labelIcon} padRight />
|
||||
{/if}
|
||||
<span {...labelProps} on:click={labelProps.onClick} class:disabled>{label}</span>
|
||||
</div>
|
||||
<slot />
|
||||
|
||||
444
packages/web/src/forms/FormIconField.svelte
Normal file
444
packages/web/src/forms/FormIconField.svelte
Normal file
@@ -0,0 +1,444 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
|
||||
export let name;
|
||||
export let label;
|
||||
export let defaultIcon;
|
||||
|
||||
export let templateProps = {};
|
||||
const { template, values, setFieldValue } = getFormContext();
|
||||
|
||||
let showPicker = false;
|
||||
|
||||
// Real-world subject icons for application identification
|
||||
const ICONS = [
|
||||
{ icon: defaultIcon, name: '(Default icon)' },
|
||||
|
||||
// Applications & Tools
|
||||
{ icon: 'mdi mdi-application', name: 'Application' },
|
||||
{ icon: 'mdi mdi-cog', name: 'Settings' },
|
||||
{ icon: 'mdi mdi-tools', name: 'Tools' },
|
||||
{ icon: 'mdi mdi-wrench', name: 'Wrench' },
|
||||
{ icon: 'mdi mdi-hammer', name: 'Hammer' },
|
||||
{ icon: 'mdi mdi-screwdriver', name: 'Screwdriver' },
|
||||
{ icon: 'mdi mdi-palette', name: 'Palette' },
|
||||
{ icon: 'mdi mdi-brush', name: 'Brush' },
|
||||
{ icon: 'mdi mdi-calculator', name: 'Calculator' },
|
||||
|
||||
// Files & Folders
|
||||
{ icon: 'mdi mdi-file', name: 'File' },
|
||||
{ icon: 'mdi mdi-folder', name: 'Folder' },
|
||||
{ icon: 'mdi mdi-folder-open', name: 'Folder Open' },
|
||||
{ icon: 'mdi mdi-file-document', name: 'Document' },
|
||||
{ icon: 'mdi mdi-file-image', name: 'Image File' },
|
||||
{ icon: 'mdi mdi-file-video', name: 'Video File' },
|
||||
{ icon: 'mdi mdi-file-music', name: 'Music File' },
|
||||
{ icon: 'mdi mdi-archive', name: 'Archive' },
|
||||
|
||||
|
||||
// Core Applications
|
||||
{ icon: 'mdi mdi-database', name: 'Database' },
|
||||
{ icon: 'mdi mdi-server', name: 'Server' },
|
||||
{ icon: 'mdi mdi-web', name: 'Web' },
|
||||
{ icon: 'mdi mdi-cloud', name: 'Cloud' },
|
||||
{ icon: 'mdi mdi-monitor', name: 'Monitor' },
|
||||
{ icon: 'mdi mdi-laptop', name: 'Laptop' },
|
||||
{ icon: 'mdi mdi-cellphone', name: 'Mobile' },
|
||||
|
||||
// Business & Finance
|
||||
{ icon: 'mdi mdi-briefcase', name: 'Business' },
|
||||
{ icon: 'mdi mdi-bank', name: 'Banking' },
|
||||
{ icon: 'mdi mdi-currency-usd', name: 'Finance' },
|
||||
{ icon: 'mdi mdi-chart-line', name: 'Analytics' },
|
||||
{ icon: 'mdi mdi-chart-bar', name: 'Reports' },
|
||||
{ icon: 'mdi mdi-chart-pie', name: 'Statistics' },
|
||||
{ icon: 'mdi mdi-calculator', name: 'Calculator' },
|
||||
{ icon: 'mdi mdi-cash-register', name: 'Sales' },
|
||||
{ icon: 'mdi mdi-credit-card', name: 'Payments' },
|
||||
{ icon: 'mdi mdi-receipt', name: 'Invoicing' },
|
||||
|
||||
// Communication & Social
|
||||
{ icon: 'mdi mdi-email', name: 'Email' },
|
||||
{ icon: 'mdi mdi-phone', name: 'Phone' },
|
||||
{ icon: 'mdi mdi-message', name: 'Messaging' },
|
||||
{ icon: 'mdi mdi-chat', name: 'Chat' },
|
||||
{ icon: 'mdi mdi-forum', name: 'Forum' },
|
||||
{ icon: 'mdi mdi-account-group', name: 'Team' },
|
||||
{ icon: 'mdi mdi-bullhorn', name: 'Marketing' },
|
||||
{ icon: 'mdi mdi-newspaper', name: 'News' },
|
||||
|
||||
// Education & Knowledge
|
||||
{ icon: 'mdi mdi-school', name: 'Education' },
|
||||
{ icon: 'mdi mdi-book', name: 'Library' },
|
||||
{ icon: 'mdi mdi-book-open', name: 'Learning' },
|
||||
{ icon: 'mdi mdi-certificate', name: 'Certification' },
|
||||
{ icon: 'mdi mdi-graduation-cap', name: 'Academic' },
|
||||
{ icon: 'mdi mdi-microscope', name: 'Research' },
|
||||
{ icon: 'mdi mdi-flask', name: 'Laboratory' },
|
||||
{ icon: 'mdi mdi-library', name: 'Archive' },
|
||||
|
||||
// Healthcare & Medical
|
||||
{ icon: 'mdi mdi-hospital-building', name: 'Hospital' },
|
||||
{ icon: 'mdi mdi-medical-bag', name: 'Medical' },
|
||||
{ icon: 'mdi mdi-heart-pulse', name: 'Health' },
|
||||
{ icon: 'mdi mdi-pill', name: 'Pharmacy' },
|
||||
{ icon: 'mdi mdi-tooth', name: 'Dental' },
|
||||
{ icon: 'mdi mdi-eye', name: 'Vision' },
|
||||
{ icon: 'mdi mdi-stethoscope', name: 'Clinic' },
|
||||
|
||||
// Transportation & Logistics
|
||||
{ icon: 'mdi mdi-truck', name: 'Logistics' },
|
||||
{ icon: 'mdi mdi-car', name: 'Automotive' },
|
||||
{ icon: 'mdi mdi-airplane', name: 'Aviation' },
|
||||
{ icon: 'mdi mdi-ship-wheel', name: 'Maritime' },
|
||||
{ icon: 'mdi mdi-train', name: 'Railway' },
|
||||
{ icon: 'mdi mdi-bus', name: 'Transit' },
|
||||
{ icon: 'mdi mdi-bike', name: 'Cycling' },
|
||||
{ icon: 'mdi mdi-map', name: 'Navigation' },
|
||||
{ icon: 'mdi mdi-gas-station', name: 'Fuel' },
|
||||
|
||||
// Real Estate & Construction
|
||||
{ icon: 'mdi mdi-home', name: 'Real Estate' },
|
||||
{ icon: 'mdi mdi-office-building', name: 'Commercial' },
|
||||
{ icon: 'mdi mdi-factory', name: 'Industrial' },
|
||||
{ icon: 'mdi mdi-hammer', name: 'Construction' },
|
||||
{ icon: 'mdi mdi-wrench', name: 'Maintenance' },
|
||||
{ icon: 'mdi mdi-tools', name: 'Tools' },
|
||||
{ icon: 'mdi mdi-city', name: 'Urban Planning' },
|
||||
|
||||
// Retail & E-commerce
|
||||
{ icon: 'mdi mdi-store', name: 'Retail' },
|
||||
{ icon: 'mdi mdi-shopping', name: 'Shopping' },
|
||||
{ icon: 'mdi mdi-cart', name: 'E-commerce' },
|
||||
{ icon: 'mdi mdi-barcode', name: 'Inventory' },
|
||||
{ icon: 'mdi mdi-package-variant', name: 'Shipping' },
|
||||
{ icon: 'mdi mdi-gift', name: 'Gifts' },
|
||||
|
||||
// Entertainment & Media
|
||||
{ icon: 'mdi mdi-camera', name: 'Photography' },
|
||||
{ icon: 'mdi mdi-video', name: 'Video' },
|
||||
{ icon: 'mdi mdi-music', name: 'Music' },
|
||||
{ icon: 'mdi mdi-gamepad-variant', name: 'Gaming' },
|
||||
{ icon: 'mdi mdi-movie', name: 'Cinema' },
|
||||
{ icon: 'mdi mdi-television', name: 'Broadcasting' },
|
||||
{ icon: 'mdi mdi-radio', name: 'Radio' },
|
||||
{ icon: 'mdi mdi-theater', name: 'Theater' },
|
||||
|
||||
// Food & Hospitality
|
||||
{ icon: 'mdi mdi-food', name: 'Food Service' },
|
||||
{ icon: 'mdi mdi-coffee', name: 'Cafe' },
|
||||
{ icon: 'mdi mdi-silverware-fork-knife', name: 'Restaurant' },
|
||||
{ icon: 'mdi mdi-pizza', name: 'Pizza' },
|
||||
{ icon: 'mdi mdi-cake', name: 'Bakery' },
|
||||
{ icon: 'mdi mdi-glass-wine', name: 'Bar' },
|
||||
{ icon: 'mdi mdi-bed', name: 'Hotel' },
|
||||
|
||||
// Sports & Fitness
|
||||
{ icon: 'mdi mdi-dumbbell', name: 'Fitness' },
|
||||
{ icon: 'mdi mdi-basketball', name: 'Basketball' },
|
||||
{ icon: 'mdi mdi-soccer', name: 'Soccer' },
|
||||
{ icon: 'mdi mdi-tennis', name: 'Tennis' },
|
||||
{ icon: 'mdi mdi-golf', name: 'Golf' },
|
||||
{ icon: 'mdi mdi-run', name: 'Running' },
|
||||
{ icon: 'mdi mdi-swim', name: 'Swimming' },
|
||||
{ icon: 'mdi mdi-yoga', name: 'Yoga' },
|
||||
|
||||
// Nature & Environment
|
||||
{ icon: 'mdi mdi-tree', name: 'Forestry' },
|
||||
{ icon: 'mdi mdi-flower', name: 'Gardening' },
|
||||
{ icon: 'mdi mdi-leaf', name: 'Environment' },
|
||||
{ icon: 'mdi mdi-weather-sunny', name: 'Weather' },
|
||||
{ icon: 'mdi mdi-earth', name: 'Geography' },
|
||||
{ icon: 'mdi mdi-water', name: 'Water' },
|
||||
{ icon: 'mdi mdi-fire', name: 'Energy' },
|
||||
{ icon: 'mdi mdi-lightning-bolt', name: 'Power' },
|
||||
|
||||
// Science & Technology
|
||||
{ icon: 'mdi mdi-rocket', name: 'Aerospace' },
|
||||
{ icon: 'mdi mdi-atom', name: 'Physics' },
|
||||
{ icon: 'mdi mdi-dna', name: 'Genetics' },
|
||||
{ icon: 'mdi mdi-telescope', name: 'Astronomy' },
|
||||
{ icon: 'mdi mdi-robot', name: 'Robotics' },
|
||||
{ icon: 'mdi mdi-chip', name: 'Electronics' },
|
||||
|
||||
// Security & Safety
|
||||
{ icon: 'mdi mdi-shield', name: 'Security' },
|
||||
{ icon: 'mdi mdi-lock', name: 'Access Control' },
|
||||
{ icon: 'mdi mdi-key', name: 'Authentication' },
|
||||
{ icon: 'mdi mdi-fire-truck', name: 'Emergency' },
|
||||
{ icon: 'mdi mdi-police-badge', name: 'Law Enforcement' },
|
||||
|
||||
// Time & Scheduling
|
||||
{ icon: 'mdi mdi-calendar', name: 'Calendar' },
|
||||
{ icon: 'mdi mdi-clock', name: 'Time Tracking' },
|
||||
{ icon: 'mdi mdi-timer', name: 'Timer' },
|
||||
{ icon: 'mdi mdi-alarm', name: 'Reminders' },
|
||||
|
||||
// Creative & Design
|
||||
{ icon: 'mdi mdi-palette', name: 'Design' },
|
||||
{ icon: 'mdi mdi-brush', name: 'Art' },
|
||||
{ icon: 'mdi mdi-draw', name: 'Drawing' },
|
||||
{ icon: 'mdi mdi-image', name: 'Graphics' },
|
||||
{ icon: 'mdi mdi-format-paint', name: 'Painting' },
|
||||
|
||||
// Alpha Icons
|
||||
{ icon: 'mdi mdi-alpha-a-circle', name: 'A' },
|
||||
{ icon: 'mdi mdi-alpha-b-circle', name: 'B' },
|
||||
{ icon: 'mdi mdi-alpha-c-circle', name: 'C' },
|
||||
{ icon: 'mdi mdi-alpha-d-circle', name: 'D' },
|
||||
{ icon: 'mdi mdi-alpha-e-circle', name: 'E' },
|
||||
{ icon: 'mdi mdi-alpha-f-circle', name: 'F' },
|
||||
{ icon: 'mdi mdi-alpha-g-circle', name: 'G' },
|
||||
{ icon: 'mdi mdi-alpha-h-circle', name: 'H' },
|
||||
{ icon: 'mdi mdi-alpha-i-circle', name: 'I' },
|
||||
{ icon: 'mdi mdi-alpha-j-circle', name: 'J' },
|
||||
{ icon: 'mdi mdi-alpha-k-circle', name: 'K' },
|
||||
{ icon: 'mdi mdi-alpha-l-circle', name: 'L' },
|
||||
{ icon: 'mdi mdi-alpha-m-circle', name: 'M' },
|
||||
{ icon: 'mdi mdi-alpha-n-circle', name: 'N' },
|
||||
{ icon: 'mdi mdi-alpha-o-circle', name: 'O' },
|
||||
{ icon: 'mdi mdi-alpha-p-circle', name: 'P' },
|
||||
{ icon: 'mdi mdi-alpha-q-circle', name: 'Q' },
|
||||
{ icon: 'mdi mdi-alpha-r-circle', name: 'R' },
|
||||
{ icon: 'mdi mdi-alpha-s-circle', name: 'S' },
|
||||
{ icon: 'mdi mdi-alpha-t-circle', name: 'T' },
|
||||
{ icon: 'mdi mdi-alpha-u-circle', name: 'U' },
|
||||
{ icon: 'mdi mdi-alpha-v-circle', name: 'V' },
|
||||
{ icon: 'mdi mdi-alpha-w-circle', name: 'W' },
|
||||
{ icon: 'mdi mdi-alpha-x-circle', name: 'X' },
|
||||
{ icon: 'mdi mdi-alpha-y-circle', name: 'Y' },
|
||||
{ icon: 'mdi mdi-alpha-z-circle', name: 'Z' },
|
||||
|
||||
// Numeric Icons
|
||||
{ icon: 'mdi mdi-numeric-0-circle', name: '0' },
|
||||
{ icon: 'mdi mdi-numeric-1-circle', name: '1' },
|
||||
{ icon: 'mdi mdi-numeric-2-circle', name: '2' },
|
||||
{ icon: 'mdi mdi-numeric-3-circle', name: '3' },
|
||||
{ icon: 'mdi mdi-numeric-4-circle', name: '4' },
|
||||
{ icon: 'mdi mdi-numeric-5-circle', name: '5' },
|
||||
{ icon: 'mdi mdi-numeric-6-circle', name: '6' },
|
||||
{ icon: 'mdi mdi-numeric-7-circle', name: '7' },
|
||||
{ icon: 'mdi mdi-numeric-8-circle', name: '8' },
|
||||
{ icon: 'mdi mdi-numeric-9-circle', name: '9' },
|
||||
{ icon: 'mdi mdi-numeric-10-circle', name: '10' },
|
||||
|
||||
// Alpha Outline Icons
|
||||
{ icon: 'mdi mdi-alpha-a-circle-outline', name: 'A Outline' },
|
||||
{ icon: 'mdi mdi-alpha-b-circle-outline', name: 'B Outline' },
|
||||
{ icon: 'mdi mdi-alpha-c-circle-outline', name: 'C Outline' },
|
||||
{ icon: 'mdi mdi-alpha-d-circle-outline', name: 'D Outline' },
|
||||
{ icon: 'mdi mdi-alpha-e-circle-outline', name: 'E Outline' },
|
||||
{ icon: 'mdi mdi-alpha-f-circle-outline', name: 'F Outline' },
|
||||
{ icon: 'mdi mdi-alpha-g-circle-outline', name: 'G Outline' },
|
||||
{ icon: 'mdi mdi-alpha-h-circle-outline', name: 'H Outline' },
|
||||
{ icon: 'mdi mdi-alpha-i-circle-outline', name: 'I Outline' },
|
||||
{ icon: 'mdi mdi-alpha-j-circle-outline', name: 'J Outline' },
|
||||
{ icon: 'mdi mdi-alpha-k-circle-outline', name: 'K Outline' },
|
||||
{ icon: 'mdi mdi-alpha-l-circle-outline', name: 'L Outline' },
|
||||
{ icon: 'mdi mdi-alpha-m-circle-outline', name: 'M Outline' },
|
||||
{ icon: 'mdi mdi-alpha-n-circle-outline', name: 'N Outline' },
|
||||
{ icon: 'mdi mdi-alpha-o-circle-outline', name: 'O Outline' },
|
||||
{ icon: 'mdi mdi-alpha-p-circle-outline', name: 'P Outline' },
|
||||
{ icon: 'mdi mdi-alpha-q-circle-outline', name: 'Q Outline' },
|
||||
{ icon: 'mdi mdi-alpha-r-circle-outline', name: 'R Outline' },
|
||||
{ icon: 'mdi mdi-alpha-s-circle-outline', name: 'S Outline' },
|
||||
{ icon: 'mdi mdi-alpha-t-circle-outline', name: 'T Outline' },
|
||||
{ icon: 'mdi mdi-alpha-u-circle-outline', name: 'U Outline' },
|
||||
{ icon: 'mdi mdi-alpha-v-circle-outline', name: 'V Outline' },
|
||||
{ icon: 'mdi mdi-alpha-w-circle-outline', name: 'W Outline' },
|
||||
{ icon: 'mdi mdi-alpha-x-circle-outline', name: 'X Outline' },
|
||||
{ icon: 'mdi mdi-alpha-y-circle-outline', name: 'Y Outline' },
|
||||
{ icon: 'mdi mdi-alpha-z-circle-outline', name: 'Z Outline' },
|
||||
|
||||
// Numeric Outline Icons
|
||||
{ icon: 'mdi mdi-numeric-0-circle-outline', name: '0 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-1-circle-outline', name: '1 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-2-circle-outline', name: '2 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-3-circle-outline', name: '3 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-4-circle-outline', name: '4 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-5-circle-outline', name: '5 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-6-circle-outline', name: '6 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-7-circle-outline', name: '7 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-8-circle-outline', name: '8 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-9-circle-outline', name: '9 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-10-circle-outline', name: '10 Outline' },
|
||||
];
|
||||
|
||||
function selectIcon(iconName) {
|
||||
setFieldValue(name, iconName);
|
||||
showPicker = false;
|
||||
}
|
||||
|
||||
function togglePicker() {
|
||||
showPicker = !showPicker;
|
||||
}
|
||||
|
||||
function handleKeydown(event, action) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
$: iconValue = $values?.[name];
|
||||
</script>
|
||||
|
||||
<svelte:component this={template} type="select" {label} {...templateProps}>
|
||||
<div class="icon-field-container">
|
||||
<div
|
||||
class="selected-icon"
|
||||
on:click={togglePicker}
|
||||
on:keydown={e => handleKeydown(e, togglePicker)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<FontIcon icon={iconValue || defaultIcon} />
|
||||
<span class="icon-name">{ICONS.find(icon => icon.icon === iconValue)?.name || '(Default icon)'}</span>
|
||||
<FontIcon icon="icon chevron-down" />
|
||||
</div>
|
||||
|
||||
{#if showPicker}
|
||||
<div class="icon-picker">
|
||||
<div class="icon-picker-header">
|
||||
<span>Choose an icon</span>
|
||||
<InlineButton on:click={togglePicker}>
|
||||
<FontIcon icon="icon close" />
|
||||
</InlineButton>
|
||||
</div>
|
||||
|
||||
<div class="icon-grid">
|
||||
{#each ICONS as { icon, name: iconDisplayName }}
|
||||
<div
|
||||
class="icon-option"
|
||||
class:selected={iconValue === icon}
|
||||
on:click={() => selectIcon(icon)}
|
||||
on:keydown={e => handleKeydown(e, () => selectIcon(icon))}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title={iconDisplayName}
|
||||
>
|
||||
<FontIcon {icon} />
|
||||
<span class="icon-label">{iconDisplayName}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:component>
|
||||
|
||||
<style>
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--theme-font-1);
|
||||
}
|
||||
.icon-field-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
background: var(--theme-bg-0);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.selected-icon:hover {
|
||||
border-color: var(--theme-border-hover);
|
||||
}
|
||||
|
||||
.selected-icon:focus {
|
||||
outline: none;
|
||||
border-color: var(--theme-font-link);
|
||||
box-shadow: 0 0 0 2px var(--theme-font-link-opacity);
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
flex: 1;
|
||||
color: var(--theme-font-1);
|
||||
}
|
||||
|
||||
.icon-picker {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: var(--theme-bg-0);
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.icon-picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
background: var(--theme-bg-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 1px;
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.icon-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-option:hover {
|
||||
background: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
.icon-option.selected {
|
||||
background: var(--theme-bg-selected);
|
||||
color: var(--theme-font-link);
|
||||
}
|
||||
|
||||
.icon-option:focus {
|
||||
outline: none;
|
||||
background: var(--theme-bg-hover);
|
||||
box-shadow: 0 0 0 2px var(--theme-font-link-opacity);
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-font-2);
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.icon-option.selected .icon-label {
|
||||
color: var(--theme-font-link);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
import TextField from './TextField.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let name;
|
||||
export let disabled = false;
|
||||
@@ -29,7 +30,7 @@
|
||||
setFieldValue(name, e.target['value']);
|
||||
}
|
||||
}}
|
||||
placeholder={isCrypted ? '(Password is encrypted)' : undefined}
|
||||
placeholder={isCrypted ? _t('common.passwordEncrypted', { defaultMessage: 'Password is encrypted' }) : undefined}
|
||||
type={isCrypted || showPassword ? 'text' : 'password'}
|
||||
/>
|
||||
{#if !isCrypted}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
export let addButtonLabel;
|
||||
export let placeholder;
|
||||
export let templateProps;
|
||||
export let isReadOnly = false;
|
||||
|
||||
const { template, values, setFieldValue } = getFormContext();
|
||||
|
||||
@@ -20,7 +21,7 @@
|
||||
|
||||
<svelte:component this={template} type="text" {label} {...templateProps}>
|
||||
{#each stringList as value, index}
|
||||
<div class='input-line-flex'>
|
||||
<div class="input-line-flex">
|
||||
<TextField
|
||||
{value}
|
||||
{placeholder}
|
||||
@@ -28,12 +29,14 @@
|
||||
const newValues = stringList.map((v, i) => (i === index ? e.target['value'] : v));
|
||||
setFieldValue(name, newValues);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
|
||||
<InlineButton
|
||||
on:click={() => {
|
||||
setFieldValue(name, [...stringList.slice(0, index), ...stringList.slice(index + 1)]);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
>
|
||||
<FontIcon icon="icon delete" />
|
||||
</InlineButton>
|
||||
@@ -45,11 +48,12 @@
|
||||
on:click={() => {
|
||||
setFieldValue(name, [...stringList, '']);
|
||||
}}
|
||||
disabled={isReadOnly}
|
||||
/>
|
||||
</svelte:component>
|
||||
|
||||
<style>
|
||||
.input-line-flex {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
.input-line-flex {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
import TextField from './TextField.svelte';
|
||||
import { _tval } from '../translations';
|
||||
|
||||
export let name;
|
||||
export let defaultValue;
|
||||
@@ -11,7 +12,7 @@
|
||||
|
||||
<TextField
|
||||
{...$$restProps}
|
||||
value={$values[name] ?? defaultValue}
|
||||
value={$values?.[name] ? _tval($values[name]) : defaultValue}
|
||||
on:input={e => setFieldValue(name, e.target['value'])}
|
||||
on:input={e => {
|
||||
if (saveOnInput) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let options = [];
|
||||
export let options: Array<{ label: string; value: any }> = [];
|
||||
export let value;
|
||||
export let isNative = false;
|
||||
export let isMulti = false;
|
||||
|
||||
@@ -1,48 +1,79 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import { currentDatabase } from '../stores';
|
||||
import { filterAppsForDatabase } from '../utility/appTools';
|
||||
import { useAppFolders, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getConnectionInfo, useAllApps, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let value = '#new';
|
||||
export let disableInitialize = false;
|
||||
export let value = '';
|
||||
export let conid;
|
||||
export let database;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let selectFieldKey = 0;
|
||||
|
||||
$: appFolders = useAppFolders();
|
||||
$: usedApps = useUsedApps();
|
||||
$: dbInfo = useDatabaseInfo({ conid, database });
|
||||
$: connectionInfo = useConnectionInfo({ conid });
|
||||
|
||||
$: {
|
||||
if (!disableInitialize && value == '#new' && $currentDatabase) {
|
||||
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $usedApps || []);
|
||||
const common = _.intersection(
|
||||
($appFolders || []).map(x => x.name),
|
||||
filtered.map(x => x.name)
|
||||
);
|
||||
if (common.length > 0) {
|
||||
value = common[0] as string;
|
||||
$: allApps = useAllApps();
|
||||
$: apps = filterAppsForDatabase($connectionInfo, database, $allApps || [], $dbInfo);
|
||||
|
||||
$: if (apps?.length == 1) {
|
||||
value = apps[0].appid;
|
||||
selectFieldKey++;
|
||||
dispatch('change', value);
|
||||
}
|
||||
|
||||
async function handleAddNewApplication() {
|
||||
showModal(InputTextModal, {
|
||||
header: _t('database.newApplication', { defaultMessage: 'New application' }),
|
||||
label: _t('database.applicationName', { defaultMessage: 'Application name' }),
|
||||
value: _.startCase(database),
|
||||
onConfirm: async appName => {
|
||||
const newAppId = await apiCall('apps/create-app-from-db', {
|
||||
appName,
|
||||
server: $connectionInfo?.server,
|
||||
database,
|
||||
});
|
||||
await tick();
|
||||
value = newAppId;
|
||||
dispatch('change', value);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SelectField
|
||||
isNative
|
||||
{...$$restProps}
|
||||
{value}
|
||||
on:change={e => {
|
||||
value = e.detail;
|
||||
dispatch('change', value);
|
||||
}}
|
||||
options={[
|
||||
{ label: '(New application linked to current DB)', value: '#new' },
|
||||
...($appFolders || []).map(app => ({
|
||||
label: app.name,
|
||||
value: app.name,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<div class="flex">
|
||||
{#key selectFieldKey}
|
||||
<SelectField
|
||||
isNative
|
||||
{...$$restProps}
|
||||
{value}
|
||||
on:change={e => {
|
||||
value = e.detail;
|
||||
dispatch('change', value);
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: '(not selected)',
|
||||
value: '',
|
||||
},
|
||||
...(apps || []).map(app => ({
|
||||
label: app.applicationName,
|
||||
value: app.appid,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
{/key}
|
||||
<InlineButton on:click={handleAddNewApplication} square>
|
||||
<FontIcon icon="icon plus-thick" padLeft padRight />
|
||||
</InlineButton>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionJsonView.expandAll',
|
||||
category: 'Collection data',
|
||||
name: 'Expand all',
|
||||
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
|
||||
name: __t('command.collectionData.expandAll', { defaultMessage: 'Expand all' }),
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon expand-all',
|
||||
onClick: () => getCurrentEditor().handleExpandAll(),
|
||||
@@ -12,8 +12,8 @@
|
||||
});
|
||||
registerCommand({
|
||||
id: 'collectionJsonView.collapseAll',
|
||||
category: 'Collection data',
|
||||
name: 'Collapse all',
|
||||
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
|
||||
name: __t('command.collectionData.collapseAll', { defaultMessage: 'Collapse all' }),
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon collapse-all',
|
||||
onClick: () => getCurrentEditor().handleCollapseAll(),
|
||||
@@ -37,6 +37,7 @@
|
||||
import CollectionJsonRow from './CollectionJsonRow.svelte';
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import { __t } from '../translations';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.refresh',
|
||||
category: 'Data form',
|
||||
name: _t('common.refresh', { defaultMessage: 'Refresh' }),
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('common.refresh', { defaultMessage: 'Refresh' }),
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -26,8 +26,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.copyToClipboard',
|
||||
category: 'Data form',
|
||||
name: 'Copy to clipboard',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.copyToClipboard', { defaultMessage: 'Copy to clipboard' }),
|
||||
keyText: 'CtrlOrCommand+C',
|
||||
disableHandleKeyText: 'CtrlOrCommand+C',
|
||||
testEnabled: () => getCurrentDataForm() != null,
|
||||
@@ -36,8 +36,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.revertRowChanges',
|
||||
category: 'Data form',
|
||||
name: 'Revert row changes',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.revertRowChanges', { defaultMessage: 'Revert row changes' }),
|
||||
keyText: 'CtrlOrCommand+U',
|
||||
testEnabled: () => getCurrentDataForm()?.getGrider()?.containsChanges,
|
||||
onClick: () => getCurrentDataForm().getGrider().revertRowChanges(0),
|
||||
@@ -45,8 +45,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.setNull',
|
||||
category: 'Data form',
|
||||
name: 'Set NULL',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.setNull', { defaultMessage: 'Set NULL' }),
|
||||
keyText: 'CtrlOrCommand+0',
|
||||
testEnabled: () => getCurrentDataForm() != null && !getCurrentDataForm()?.getEditorTypes()?.supportFieldRemoval,
|
||||
onClick: () => getCurrentDataForm().setFixedValue(null),
|
||||
@@ -54,8 +54,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.removeField',
|
||||
category: 'Data form',
|
||||
name: 'Remove field',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.removeField', { defaultMessage: 'Remove field' }),
|
||||
keyText: 'CtrlOrCommand+0',
|
||||
testEnabled: () => getCurrentDataForm() != null && getCurrentDataForm()?.getEditorTypes()?.supportFieldRemoval,
|
||||
onClick: () => getCurrentDataForm().setFixedValue(undefined),
|
||||
@@ -63,8 +63,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.undo',
|
||||
category: 'Data form',
|
||||
name: 'Undo',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.undo', { defaultMessage: 'Undo' }),
|
||||
group: 'undo',
|
||||
icon: 'icon undo',
|
||||
toolbar: true,
|
||||
@@ -75,8 +75,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.redo',
|
||||
category: 'Data form',
|
||||
name: 'Redo',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.redo', { defaultMessage: 'Redo' }),
|
||||
group: 'redo',
|
||||
icon: 'icon redo',
|
||||
toolbar: true,
|
||||
@@ -87,16 +87,16 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.reconnect',
|
||||
category: 'Data grid',
|
||||
name: 'Reconnect',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.reconnect', { defaultMessage: 'Reconnect' }),
|
||||
testEnabled: () => getCurrentDataForm() != null,
|
||||
onClick: () => getCurrentDataForm().reconnect(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.filterSelected',
|
||||
category: 'Data form',
|
||||
name: 'Filter this value',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.filterSelected', { defaultMessage: 'Filter this value' }),
|
||||
keyText: 'CtrlOrCommand+Shift+F',
|
||||
testEnabled: () => getCurrentDataForm() != null,
|
||||
onClick: () => getCurrentDataForm().filterSelectedValue(),
|
||||
@@ -104,16 +104,16 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.addToFilter',
|
||||
category: 'Data form',
|
||||
name: 'Add to filter',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.addToFilter', { defaultMessage: 'Add to filter' }),
|
||||
testEnabled: () => getCurrentDataForm() != null,
|
||||
onClick: () => getCurrentDataForm().addToFilter(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.goToFirst',
|
||||
category: 'Data form',
|
||||
name: 'First',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.goToFirst', { defaultMessage: 'First' }),
|
||||
keyText: 'CtrlOrCommand+Home',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -124,8 +124,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.goToPrevious',
|
||||
category: 'Data form',
|
||||
name: 'Previous',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.goToPrevious', { defaultMessage: 'Previous' }),
|
||||
keyText: 'CtrlOrCommand+ArrowUp',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -136,8 +136,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.goToNext',
|
||||
category: 'Data form',
|
||||
name: 'Next',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.goToNext', { defaultMessage: 'Next' }),
|
||||
keyText: 'CtrlOrCommand+ArrowDown',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -148,8 +148,8 @@
|
||||
|
||||
registerCommand({
|
||||
id: 'dataForm.goToLast',
|
||||
category: 'Data form',
|
||||
name: 'Last',
|
||||
category: __t('command.dataForm', { defaultMessage: 'Data form' }),
|
||||
name: __t('command.dataForm.goToLast', { defaultMessage: 'Last' }),
|
||||
keyText: 'CtrlOrCommand+End',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -197,7 +197,7 @@
|
||||
import resizeObserver from '../utility/resizeObserver';
|
||||
import openReferenceForm from './openReferenceForm';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
import { _t, __t } from '../translations';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -243,20 +243,16 @@
|
||||
|
||||
function getRowCountInfo(allRowCount) {
|
||||
if (rowCountNotAvailable) {
|
||||
return `Row: ${((display.config.formViewRecordNumber || 0) + 1).toLocaleString()} / ???`;
|
||||
return _t('dataForm.rowCount', { defaultMessage: 'Row: {rowCount} / ???', values: { rowCount: ((display.config.formViewRecordNumber || 0) + 1).toLocaleString() } });
|
||||
}
|
||||
if (rowData == null) {
|
||||
if (allRowCount != null) {
|
||||
return `Out of bounds: ${(
|
||||
(display.config.formViewRecordNumber || 0) + 1
|
||||
).toLocaleString()} / ${allRowCount.toLocaleString()}`;
|
||||
return _t('dataForm.outOfBounds', { defaultMessage: 'Out of bounds: {current} / {total}', values: { current: ((display.config.formViewRecordNumber || 0) + 1).toLocaleString(), total: allRowCount.toLocaleString() } });
|
||||
}
|
||||
return 'No data';
|
||||
return _t('dataForm.noData', { defaultMessage: 'No data' });
|
||||
}
|
||||
if (allRowCount == null || display == null) return 'Loading row count...';
|
||||
return `Row: ${(
|
||||
(display.config.formViewRecordNumber || 0) + 1
|
||||
).toLocaleString()} / ${allRowCount.toLocaleString()}`;
|
||||
if (allRowCount == null || display == null) return _t('dataForm.loadingRowCount', { defaultMessage: 'Loading row count...' });
|
||||
return _t('dataForm.rowCount', { defaultMessage: 'Row: {current} / {total}', values: { current: ((display.config.formViewRecordNumber || 0) + 1).toLocaleString(), total: allRowCount.toLocaleString() } });
|
||||
}
|
||||
|
||||
export function getGrider() {
|
||||
@@ -720,7 +716,7 @@
|
||||
</div>
|
||||
|
||||
{#if isLoading}
|
||||
<LoadingInfo wrapper message="Loading data" />
|
||||
<LoadingInfo wrapper message={_t('common.loadingData', { defaultMessage: 'Loading data' })} />
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import ColumnLabel from '../elements/ColumnLabel.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
|
||||
export let uniqueName;
|
||||
export let display;
|
||||
@@ -42,15 +43,23 @@
|
||||
{:else}
|
||||
{uniqueName}
|
||||
{/if}
|
||||
<InlineButton
|
||||
square
|
||||
narrow
|
||||
on:click={() => {
|
||||
display.removeFilter(uniqueName);
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon close" />
|
||||
</InlineButton>
|
||||
<div class="flex items-center gap-2">
|
||||
<CheckboxField
|
||||
checked={!display.isFilterDisabled(uniqueName)}
|
||||
on:change={() => {
|
||||
display.toggleFilterEnabled(uniqueName);
|
||||
}}
|
||||
/>
|
||||
<InlineButton
|
||||
square
|
||||
narrow
|
||||
on:click={() => {
|
||||
display.removeFilter(uniqueName);
|
||||
}}
|
||||
>
|
||||
<FontIcon icon="icon close" />
|
||||
</InlineButton>
|
||||
</div>
|
||||
</div>
|
||||
<DataFilterControl
|
||||
filterBehaviour={computeFilterBehavoir(column, display, isDynamicStructure)}
|
||||
@@ -64,6 +73,7 @@
|
||||
columnName={column ? (column.uniquePath.length == 1 ? column.uniquePath[0] : null) : uniqueName}
|
||||
foreignKey={column?.foreignKey}
|
||||
dataType={column?.dataType}
|
||||
filterDisabled={display.isFilterDisabled(uniqueName)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import keycodes from '../utility/keycodes';
|
||||
import FormViewFilterColumn from './FormViewFilterColumn.svelte';
|
||||
import { stringFilterBehaviour } from 'dbgate-tools';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
import { _t } from '../translations';
|
||||
// import PrimaryKeyFilterEditor from './PrimaryKeyFilterEditor.svelte';
|
||||
|
||||
export let managerSize;
|
||||
@@ -35,7 +37,7 @@
|
||||
|
||||
{#if isFormView}
|
||||
<div class="m-1">
|
||||
<div>Column name filter</div>
|
||||
<div>{_t('datagrid.columnNameFilter', { defaultMessage: 'Column name filter' })}</div>
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
@@ -62,8 +64,15 @@
|
||||
{#if hasMultiColumnFilter}
|
||||
<div class="m-1">
|
||||
<div class="space-between">
|
||||
<span>Multi column filter</span>
|
||||
<span>{_t('dataGrid.multiColumnFilter', { defaultMessage: 'Multi column filter' })}</span>
|
||||
{#if multiColumnFilter}
|
||||
<div class="flex items-center gap-2">
|
||||
<CheckboxField
|
||||
checked={!display.isMultiColumnFilterDisabled()}
|
||||
on:change={() => {
|
||||
display.toggleMultiColumnFilterEnabled();
|
||||
}}
|
||||
/>
|
||||
<InlineButton
|
||||
square
|
||||
narrow
|
||||
@@ -73,6 +82,7 @@
|
||||
>
|
||||
<FontIcon icon="icon close" />
|
||||
</InlineButton>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -85,6 +95,7 @@
|
||||
{database}
|
||||
{schemaName}
|
||||
{pureName}
|
||||
filterDisabled={display.isMultiColumnFilterDisabled()}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -74,6 +74,9 @@
|
||||
'icon arrow-link': 'mdi mdi-arrow-top-right-thick',
|
||||
'icon reset': 'mdi mdi-cancel',
|
||||
'icon send': 'mdi mdi-send',
|
||||
'icon regex': 'mdi mdi-regex',
|
||||
'icon list': 'mdi mdi-format-list-bulleted-triangle',
|
||||
'icon help': 'mdi mdi-help',
|
||||
|
||||
'icon window-restore': 'mdi mdi-window-restore',
|
||||
'icon window-maximize': 'mdi mdi-window-maximize',
|
||||
@@ -117,6 +120,7 @@
|
||||
'icon structure': 'mdi mdi-tools',
|
||||
'icon square': 'mdi mdi-square',
|
||||
'icon data-deploy': 'mdi mdi-database-settings',
|
||||
'icon team-file': 'mdi mdi-account-file',
|
||||
|
||||
'icon cloud-account': 'mdi mdi-account-remove-outline',
|
||||
'icon cloud-account-connected': 'mdi mdi-account-check-outline',
|
||||
@@ -160,6 +164,7 @@
|
||||
'icon parent-filter-outline': 'mdi mdi-home-alert-outline',
|
||||
'icon download': 'mdi mdi-download',
|
||||
'icon text': 'mdi mdi-text',
|
||||
'icon ai-provider': 'mdi mdi-cloud-cog',
|
||||
'icon ai': 'mdi mdi-head-lightbulb',
|
||||
'icon wait': 'mdi mdi-timer-sand',
|
||||
'icon more': 'mdi mdi-more',
|
||||
@@ -224,6 +229,7 @@
|
||||
'icon type-unknown': 'mdi mdi-help-box',
|
||||
'icon equal': 'mdi mdi-equal',
|
||||
'icon not-equal': 'mdi mdi-not-equal-variant',
|
||||
'icon warn': 'mdi mdi-alert',
|
||||
|
||||
'icon at': 'mdi mdi-at',
|
||||
'icon expand-all': 'mdi mdi-expand-all',
|
||||
@@ -284,6 +290,7 @@
|
||||
'img auth': 'mdi mdi-account-key color-icon-blue',
|
||||
'img cloud-connection': 'mdi mdi-cloud-lock color-icon-blue',
|
||||
'img ai': 'mdi mdi-head-lightbulb color-icon-yellow',
|
||||
'img ai-provider': 'mdi mdi-head-lightbulb color-icon-blue',
|
||||
'img run': 'mdi mdi-play color-icon-blue',
|
||||
|
||||
'img add': 'mdi mdi-plus-circle color-icon-green',
|
||||
@@ -345,6 +352,8 @@
|
||||
'img settings': 'mdi mdi-cog color-icon-blue',
|
||||
'img data-deploy': 'mdi mdi-database-settings color-icon-green',
|
||||
'img arrow-start-here': 'mdi mdi-arrow-down-bold-circle color-icon-green',
|
||||
'img team-file': 'mdi mdi-account-file color-icon-red',
|
||||
'img table-backup': 'mdi mdi-cube color-icon-yellow',
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
import ElectronFilesInput from './ElectronFilesInput.svelte';
|
||||
import { addFilesToSourceList } from './ImportExportConfigurator.svelte';
|
||||
import UploadButton from '../buttons/UploadButton.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let setPreviewSource = undefined;
|
||||
|
||||
@@ -55,10 +56,10 @@
|
||||
{:else}
|
||||
<UploadButton />
|
||||
{/if}
|
||||
<FormStyledButton value="Add web URL" on:click={handleAddUrl} />
|
||||
<FormStyledButton value={_t('importExport.addWebUrl', { defaultMessage: "Add web URL" })} on:click={handleAddUrl} />
|
||||
</div>
|
||||
|
||||
<div class="wrapper">Drag & drop imported files here</div>
|
||||
<div class="wrapper">{_t('importExport.dragDropImportedFilesHere', { defaultMessage: "Drag & drop imported files here" })}</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let conidName;
|
||||
export let databaseName;
|
||||
@@ -41,7 +42,7 @@
|
||||
{#if $dbinfo && $dbinfo[field]?.length > 0}
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value={`All ${field}`}
|
||||
value={_t('common.allFields', { defaultMessage: 'All {field}', values: { field } })}
|
||||
data-testid={`FormTablesSelect_buttonAll_${field}`}
|
||||
on:click={() =>
|
||||
setFieldValue(
|
||||
@@ -52,7 +53,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
|
||||
<FormStyledButton type="button" value="Remove all" on:click={() => setFieldValue(name, [])} />
|
||||
<FormStyledButton type="button" value={_t('common.removeAll', { defaultMessage: "Remove all" })} on:click={() => setFieldValue(name, [])} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
import createRef from '../utility/createRef';
|
||||
import DropDownButton from '../buttons/DropDownButton.svelte';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
// export let uploadedFile = undefined;
|
||||
// export let openedFile = undefined;
|
||||
@@ -210,8 +211,11 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="m-2">
|
||||
<div class="title"><FontIcon icon="icon tables" /> Map source tables/files</div>
|
||||
<div class="m-2" data-testid="ImportExportConfigurator_tableMappingSection">
|
||||
<div class="title">
|
||||
<FontIcon icon="icon tables" />
|
||||
{_t('importExport.mapSourceTablesFiles', { defaultMessage: 'Map source tables/files' })}
|
||||
</div>
|
||||
|
||||
{#key targetEditKey}
|
||||
{#key progressHolder}
|
||||
@@ -220,34 +224,34 @@
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'source',
|
||||
header: 'Source',
|
||||
header: _t('importExport.source', { defaultMessage: 'Source' }),
|
||||
component: SourceName,
|
||||
getProps: row => ({ name: row }),
|
||||
},
|
||||
{
|
||||
fieldName: 'action',
|
||||
header: 'Action',
|
||||
header: _t('importExport.action', { defaultMessage: 'Action' }),
|
||||
component: SourceAction,
|
||||
getProps: row => ({ name: row, targetDbinfo }),
|
||||
},
|
||||
{
|
||||
fieldName: 'target',
|
||||
header: 'Target',
|
||||
header: _t('importExport.target', { defaultMessage: 'Target' }),
|
||||
slot: 1,
|
||||
},
|
||||
supportsPreview && {
|
||||
fieldName: 'preview',
|
||||
header: 'Preview',
|
||||
header: _t('importExport.preview', { defaultMessage: 'Preview' }),
|
||||
slot: 0,
|
||||
},
|
||||
!!progressHolder && {
|
||||
fieldName: 'status',
|
||||
header: 'Status',
|
||||
header: _t('importExport.status', { defaultMessage: 'Status' }),
|
||||
slot: 3,
|
||||
},
|
||||
{
|
||||
fieldName: 'columns',
|
||||
header: 'Columns',
|
||||
header: _t('importExport.columns', { defaultMessage: 'Columns' }),
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
@@ -306,21 +310,21 @@
|
||||
},
|
||||
});
|
||||
}}
|
||||
>{columnCount > 0 ? `(${columnCount} columns)` : '(copy from source)'}
|
||||
>{columnCount > 0 ? _t('importExport.columnsCount', { defaultMessage: '({columnCount} columns)', values: { columnCount } }) : _t('importExport.copyFromSource', { defaultMessage: '(copy from source)' })}
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="3" let:row>
|
||||
{#if progressHolder[row]?.status == 'running' && isRunning}
|
||||
<FontIcon icon="icon loading" />
|
||||
{#if progressHolder[row]?.writtenRowCount}
|
||||
{progressHolder[row]?.writtenRowCount} rows writtem
|
||||
{progressHolder[row]?.writtenRowCount} {_t('importExport.rowsWritten', { defaultMessage: 'rows written' })}
|
||||
{:else if progressHolder[row]?.readRowCount}
|
||||
{progressHolder[row]?.readRowCount} rows read
|
||||
{progressHolder[row]?.readRowCount} {_t('importExport.rowsRead', { defaultMessage: 'rows read' })}
|
||||
{:else}
|
||||
Running
|
||||
{_t('importExport.running', { defaultMessage: 'Running' })}
|
||||
{/if}
|
||||
{:else if progressHolder[row]?.status == 'error'}
|
||||
<FontIcon icon="img error" /> Error
|
||||
<FontIcon icon="img error" /> {_t('common.error', { defaultMessage: 'Error' })}
|
||||
{#if progressHolder[row]?.errorMessage}
|
||||
<FontIcon
|
||||
icon="img info"
|
||||
@@ -333,20 +337,20 @@
|
||||
{:else if progressHolder[row]?.status == 'done'}
|
||||
<FontIcon icon="img ok" />
|
||||
{#if progressHolder[row]?.writtenRowCount}
|
||||
{progressHolder[row]?.writtenRowCount} rows written
|
||||
{progressHolder[row]?.writtenRowCount} {_t('importExport.rowsWritten', { defaultMessage: 'rows written' })}
|
||||
{:else if progressHolder[row]?.readRowCount}
|
||||
{progressHolder[row]?.readRowCount} rows written
|
||||
{progressHolder[row]?.readRowCount} {_t('importExport.rowsWritten', { defaultMessage: 'rows written' })}
|
||||
{:else}
|
||||
Done
|
||||
{_t('common.done', { defaultMessage: 'Done' })}
|
||||
{/if}
|
||||
{:else}
|
||||
<FontIcon icon="icon wait" />
|
||||
{#if progressHolder[row]?.writtenRowCount}
|
||||
{progressHolder[row]?.writtenRowCount} rows writtem
|
||||
{progressHolder[row]?.writtenRowCount} {_t('importExport.rowsWritten', { defaultMessage: 'rows written' })}
|
||||
{:else if progressHolder[row]?.readRowCount}
|
||||
{progressHolder[row]?.readRowCount} rows read
|
||||
{progressHolder[row]?.readRowCount} {_t('importExport.rowsRead', { defaultMessage: 'rows read' })}
|
||||
{:else}
|
||||
Queued
|
||||
{_t('importExport.queued', { defaultMessage: 'Queued' })}
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
@@ -73,19 +73,19 @@
|
||||
<div class="column">
|
||||
{#if direction == 'source'}
|
||||
<div class="title">
|
||||
<FontIcon icon="icon import" /> Source configuration
|
||||
<FontIcon icon="icon import" /> {_t('importExport.sourceConfiguration', { defaultMessage: 'Source configuration' })}
|
||||
</div>
|
||||
{/if}
|
||||
{#if direction == 'target'}
|
||||
<div class="title">
|
||||
<FontIcon icon="icon export" /> Target configuration
|
||||
<FontIcon icon="icon export" /> {_t('importExport.targetConfiguration', { defaultMessage: 'Target configuration' })}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="buttons">
|
||||
{#if $currentDatabase}
|
||||
<FormStyledButton
|
||||
value="Current DB"
|
||||
value={_t('importExport.currentDatabase', { defaultMessage: "Current DB" })}
|
||||
on:click={() => {
|
||||
values.update(x => ({
|
||||
...x,
|
||||
@@ -97,7 +97,7 @@
|
||||
/>
|
||||
{/if}
|
||||
<FormStyledButton
|
||||
value="Current archive"
|
||||
value={_t('importExport.currentArchive', { defaultMessage: "Current archive" })}
|
||||
data-testid={direction == 'source'
|
||||
? 'SourceTargetConfig_buttonCurrentArchive_source'
|
||||
: 'SourceTargetConfig_buttonCurrentArchive_target'}
|
||||
@@ -111,11 +111,11 @@
|
||||
/>
|
||||
{#if direction == 'target'}
|
||||
<FormStyledButton
|
||||
value="New archive"
|
||||
value={_t('importExport.newArchive', { defaultMessage: "New archive" })}
|
||||
on:click={() => {
|
||||
showModal(InputTextModal, {
|
||||
header: 'Archive',
|
||||
label: 'Name of new archive folder',
|
||||
header: _t('importExport.archive', { defaultMessage: 'Archive' }),
|
||||
label: _t('importExport.nameOfNewArchiveFolder', { defaultMessage: 'Name of new archive folder' }),
|
||||
value: `import-${moment().format('YYYY-MM-DD-hh-mm-ss')}`,
|
||||
onConfirm: value => {
|
||||
values.update(x => ({
|
||||
@@ -133,7 +133,7 @@
|
||||
<FormSelectField
|
||||
options={types.filter(x => x.directions.includes(direction))}
|
||||
name={storageTypeField}
|
||||
label="Storage type"
|
||||
label={_t('importExport.storageType', { defaultMessage: "Storage type" })}
|
||||
/>
|
||||
|
||||
{#if format && isProApp()}
|
||||
@@ -172,9 +172,9 @@
|
||||
{/if}
|
||||
|
||||
{#if storageType == 'database' || storageType == 'query'}
|
||||
<FormConnectionSelect name={connectionIdField} label="Server" {direction} />
|
||||
<FormConnectionSelect name={connectionIdField} label={_t('common.server', { defaultMessage: 'Server' })} {direction} />
|
||||
{#if !$connectionInfo?.singleDatabase}
|
||||
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" />
|
||||
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label={_t('common.database', { defaultMessage: 'Database' })} />
|
||||
{/if}
|
||||
{/if}
|
||||
{#if storageType == 'database'}
|
||||
@@ -210,7 +210,7 @@
|
||||
|
||||
{#if storageType == 'archive'}
|
||||
<FormArchiveFolderSelect
|
||||
label="Archive folder"
|
||||
label={_t('importExport.archiveFolder', { defaultMessage: "Archive folder" })}
|
||||
name={archiveFolderField}
|
||||
additionalFolders={_.compact([$values[archiveFolderField]])}
|
||||
allowCreateNew={direction == 'target'}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray;
|
||||
export let expanded = false;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
const filteredKey = new Set(['length']);
|
||||
|
||||
$: keys = Object.getOwnPropertyNames(value);
|
||||
@@ -22,8 +24,10 @@
|
||||
{keys}
|
||||
{previewKeys}
|
||||
{getValue}
|
||||
label="Array({value.length})"
|
||||
label={labelOverride || `Array(${value.length})`}
|
||||
bracketOpen="["
|
||||
bracketClose="]"
|
||||
elementValue={value}
|
||||
/>
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
import JSONNested from './JSONNested.svelte';
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray, nodeType;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
|
||||
let keys = [];
|
||||
|
||||
@@ -29,7 +31,9 @@
|
||||
{getKey}
|
||||
{getValue}
|
||||
isArray={true}
|
||||
label="{nodeType}({keys.length})"
|
||||
label={labelOverride || `${nodeType}(${keys.length})`}
|
||||
bracketOpen={'{'}
|
||||
bracketClose={'}'}
|
||||
/>
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import MapEntry from './utils/MapEntry'
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray, nodeType;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
|
||||
let keys = [];
|
||||
|
||||
@@ -28,8 +30,10 @@
|
||||
{keys}
|
||||
{getKey}
|
||||
{getValue}
|
||||
label="{nodeType}({keys.length})"
|
||||
label={labelOverride || `${nodeType}(${keys.length})`}
|
||||
colon=""
|
||||
bracketOpen={'{'}
|
||||
bracketClose={'}'}
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
export let key, value, isParentExpanded, isParentArray;
|
||||
export let expanded = false;
|
||||
export let hideKey = false;
|
||||
export let labelOverride = null;
|
||||
|
||||
const keys = ['key', 'value'];
|
||||
|
||||
@@ -17,7 +19,9 @@
|
||||
key={isParentExpanded ? String(key) : value.key}
|
||||
{keys}
|
||||
{getValue}
|
||||
label={isParentExpanded ? 'Entry ' : '=> '}
|
||||
label={labelOverride || (isParentExpanded ? 'Entry ' : '=> ')}
|
||||
bracketOpen={'{'}
|
||||
bracketClose={'}'}
|
||||
/>
|
||||
{labelOverride}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -21,13 +21,19 @@
|
||||
expandable = true;
|
||||
export let elementValue = null;
|
||||
export let onRootExpandedChanged = null;
|
||||
export let labelOverride = null;
|
||||
export let hideKey = false;
|
||||
|
||||
const context = getContext('json-tree-context-key');
|
||||
setContext('json-tree-context-key', { ...context, colon });
|
||||
const elementData = getContext('json-tree-element-data');
|
||||
const slicedKeyCount = getContext('json-tree-sliced-key-count');
|
||||
const keyLabel = labelOverride ?? key;
|
||||
const PAGE_SIZE = 100;
|
||||
let visibleKeyCount = PAGE_SIZE;
|
||||
|
||||
$: slicedKeys = expanded ? keys : previewKeys.slice(0, slicedKeyCount || 5);
|
||||
// $: slicedKeys = expanded ? keys : previewKeys.slice(0, Math.max(slicedKeyCount || 5, visibleKeyCount));
|
||||
$: slicedKeys = expanded ? keys?.slice(0, visibleKeyCount) : previewKeys.slice(0, slicedKeyCount || 5);
|
||||
|
||||
$: if (!isParentExpanded) {
|
||||
expanded = false;
|
||||
@@ -49,6 +55,12 @@
|
||||
$: if (domElement && elementData && elementValue) {
|
||||
elementData.set(domElement, elementValue);
|
||||
}
|
||||
|
||||
function showNextKeys() {
|
||||
visibleKeyCount += PAGE_SIZE;
|
||||
}
|
||||
|
||||
$: visibleShowNextKeys = expanded && slicedKeys.length < keys.length;
|
||||
</script>
|
||||
|
||||
<li class:indent={isParentExpanded} class:jsonValueHolder={!!elementValue} bind:this={domElement}>
|
||||
@@ -56,7 +68,16 @@
|
||||
{#if expandable && isParentExpanded}
|
||||
<JSONArrow on:click={toggleExpand} {expanded} />
|
||||
{/if}
|
||||
<JSONKey {key} colon={context.colon} {isParentExpanded} {isParentArray} on:click={toggleExpand} />
|
||||
{#if !hideKey}
|
||||
<JSONKey
|
||||
key={keyLabel}
|
||||
colon={context.colon}
|
||||
{isParentExpanded}
|
||||
{isParentArray}
|
||||
{hideKey}
|
||||
on:click={toggleExpand}
|
||||
/>
|
||||
{/if}
|
||||
<span on:click={toggleExpand}><span>{label}</span>{bracketOpen}</span>
|
||||
</label>
|
||||
{#if isParentExpanded}
|
||||
@@ -72,13 +93,21 @@
|
||||
<span class="comma">,</span>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if slicedKeys.length < previewKeys.length}
|
||||
{#if !visibleShowNextKeys && slicedKeys.length < previewKeys.length}
|
||||
<span>…</span>
|
||||
{/if}
|
||||
</ul>
|
||||
{:else}
|
||||
<span>…</span>
|
||||
{/if}
|
||||
|
||||
{#if visibleShowNextKeys}
|
||||
<span class="load-more">
|
||||
<!-- svelte-ignore a11y-invalid-attribute -->
|
||||
<a href="#" on:click|preventDefault={showNextKeys}>(Next 100)</a>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span>{bracketClose}</span>
|
||||
</li>
|
||||
|
||||
@@ -103,4 +132,18 @@
|
||||
/* display: contents; */
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
margin-left: 2em;
|
||||
font-style: italic;
|
||||
color: var(--theme-font-link);
|
||||
}
|
||||
.load-more a {
|
||||
color: var(--theme-font-link);
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.load-more a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
export let expanded = !!getContext('json-tree-default-expanded');
|
||||
export let labelOverride = null;
|
||||
export let onRootExpandedChanged = null;
|
||||
export let hideKey = false;
|
||||
|
||||
$: nodeType = objType(value);
|
||||
$: componentType = getComponent(nodeType);
|
||||
@@ -85,4 +86,5 @@
|
||||
{expanded}
|
||||
{labelOverride}
|
||||
{onRootExpandedChanged}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export let expanded = false;
|
||||
export let labelOverride = null;
|
||||
export let onRootExpandedChanged = null;
|
||||
export let hideKey = false;
|
||||
|
||||
$: keys = Object.getOwnPropertyNames(value);
|
||||
|
||||
@@ -26,4 +27,5 @@
|
||||
bracketClose={'}'}
|
||||
elementValue={value}
|
||||
{onRootExpandedChanged}
|
||||
{hideKey}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import JSONNode from './JSONNode.svelte';
|
||||
import { setContext } from 'svelte';
|
||||
import contextMenu, { getContextMenu } from '../utility/contextMenu';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import _ from 'lodash';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
||||
@@ -23,6 +22,7 @@
|
||||
export let isDeleted = false;
|
||||
export let isInserted = false;
|
||||
export let isModified = false;
|
||||
export let hideKey = false;
|
||||
|
||||
const settings = useSettings();
|
||||
$: wrap = $settings?.['behaviour.jsonPreviewWrap'];
|
||||
@@ -73,6 +73,7 @@
|
||||
class:wrap
|
||||
>
|
||||
<JSONNode
|
||||
{hideKey}
|
||||
{key}
|
||||
{value}
|
||||
isParentExpanded={true}
|
||||
|
||||
@@ -3,10 +3,25 @@
|
||||
|
||||
import JSONKey from './JSONKey.svelte';
|
||||
|
||||
export let key, value, valueGetter = null, isParentExpanded, isParentArray, nodeType;
|
||||
export let key,
|
||||
value,
|
||||
valueGetter = null,
|
||||
labelOverride,
|
||||
isParentExpanded,
|
||||
isParentArray,
|
||||
nodeType;
|
||||
|
||||
const label = labelOverride ?? key;
|
||||
const { colon } = getContext('json-tree-context-key');
|
||||
</script>
|
||||
|
||||
<li class:indent={isParentExpanded}>
|
||||
<JSONKey key={label} {colon} {isParentExpanded} {isParentArray} />
|
||||
<span class={nodeType}>
|
||||
{valueGetter ? valueGetter(value) : value}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
<style>
|
||||
li {
|
||||
user-select: text;
|
||||
@@ -45,9 +60,4 @@
|
||||
color: var(--symbol-color);
|
||||
}
|
||||
</style>
|
||||
<li class:indent={isParentExpanded}>
|
||||
<JSONKey {key} {colon} {isParentExpanded} {isParentArray} />
|
||||
<span class={nodeType}>
|
||||
{valueGetter ? valueGetter(value) : value}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
|
||||
31
packages/web/src/jsonui/JsonUiContentRenderer.svelte
Normal file
31
packages/web/src/jsonui/JsonUiContentRenderer.svelte
Normal file
@@ -0,0 +1,31 @@
|
||||
<script lang="ts">
|
||||
import JsonUiCountdown from './JsonUiCountdown.svelte';
|
||||
import JsonUiHeading from './JsonUiHeading.svelte';
|
||||
import JsonUiHighlight from './JsonUiHighlight.svelte';
|
||||
import JsonUiLinkButton from './JsonUiLinkButton.svelte';
|
||||
import JsonUiLinkButtonBlock from './JsonUiLinkButtonBlock.svelte';
|
||||
import JsonUiMarkdown from './JsonUiMarkdown.svelte';
|
||||
import JsonUiTextBlock from './JsonUiTextBlock.svelte';
|
||||
import JsonUiTickList from './JsonUiTickList.svelte';
|
||||
import { JsonUiBlock } from './jsonuitypes';
|
||||
|
||||
export let blocks: JsonUiBlock[] = [];
|
||||
export let passProps = {};
|
||||
|
||||
const componentMap = {
|
||||
text: JsonUiTextBlock,
|
||||
heading: JsonUiHeading,
|
||||
ticklist: JsonUiTickList,
|
||||
button: JsonUiLinkButton,
|
||||
markdown: JsonUiMarkdown,
|
||||
highlight: JsonUiHighlight,
|
||||
countdown: JsonUiCountdown,
|
||||
buttonblock: JsonUiLinkButtonBlock,
|
||||
} as const;
|
||||
</script>
|
||||
|
||||
{#each blocks as block, i}
|
||||
{#if block.type in componentMap}
|
||||
<svelte:component this={componentMap[block.type]} {...block} {...passProps} />
|
||||
{/if}
|
||||
{/each}
|
||||
87
packages/web/src/jsonui/JsonUiCountdown.svelte
Normal file
87
packages/web/src/jsonui/JsonUiCountdown.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { openWebLink } from '../utility/simpleTools';
|
||||
|
||||
export let colorClass: string = 'premium-gradient';
|
||||
export let validTo;
|
||||
export let link;
|
||||
|
||||
function formatRemaining(validTo, now) {
|
||||
let diffMs = validTo.getTime() - now.getTime();
|
||||
if (diffMs <= 0) return '0 minutes';
|
||||
|
||||
const totalMinutes = Math.floor(diffMs / 60000);
|
||||
const days = Math.floor(totalMinutes / (24 * 60));
|
||||
const hours = Math.floor((totalMinutes % (24 * 60)) / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
const parts = [];
|
||||
const en = (n, unit) => ({
|
||||
num: n,
|
||||
unit: n == 1 ? unit : unit + 's',
|
||||
});
|
||||
|
||||
if (days) parts.push(en(days, 'day'));
|
||||
if (hours) parts.push(en(hours, 'hour'));
|
||||
// Always include minutes to report down to minutes
|
||||
parts.push(en(minutes, 'minute'));
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
let currentDate = new Date();
|
||||
|
||||
onMount(() => {
|
||||
const interval = setInterval(() => {
|
||||
currentDate = new Date();
|
||||
}, 5000);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
});
|
||||
|
||||
$: parts = formatRemaining(new Date(validTo), currentDate);
|
||||
</script>
|
||||
|
||||
{#if validTo}
|
||||
<div
|
||||
class="countdown {colorClass}"
|
||||
class:isLink={!!link}
|
||||
on:click={() => {
|
||||
if (link) {
|
||||
openWebLink(link);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span class="big">Offer ends in:</span><br />
|
||||
{#each parts as part}
|
||||
<span class="part">
|
||||
<span class="big">{part.num}</span>
|
||||
{part.unit}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.countdown {
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.countdown.isLink {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.big {
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.part {
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
||||
13
packages/web/src/jsonui/JsonUiHeading.svelte
Normal file
13
packages/web/src/jsonui/JsonUiHeading.svelte
Normal file
@@ -0,0 +1,13 @@
|
||||
<script lang="ts">
|
||||
export let text: string;
|
||||
export let level: 1|2|3|4|5|6 = 2;
|
||||
const tag = `h${level}` as keyof HTMLElementTagNameMap;
|
||||
</script>
|
||||
|
||||
<svelte:element this={tag}>{text}</svelte:element>
|
||||
|
||||
<style>
|
||||
h2 {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
35
packages/web/src/jsonui/JsonUiHighlight.svelte
Normal file
35
packages/web/src/jsonui/JsonUiHighlight.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { openWebLink } from '../utility/simpleTools';
|
||||
|
||||
export let text: string;
|
||||
export let colorClass: string = 'premium-gradient';
|
||||
export let link: string;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="highlight {colorClass}"
|
||||
class:isLink={!!link}
|
||||
on:click={() => {
|
||||
if (link) {
|
||||
openWebLink(link);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{text}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.highlight {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
font-size: large;
|
||||
font-weight: bold;
|
||||
border: 1px solid;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.highlight.isLink {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
18
packages/web/src/jsonui/JsonUiLinkButton.svelte
Normal file
18
packages/web/src/jsonui/JsonUiLinkButton.svelte
Normal file
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import { openWebLink } from '../utility/simpleTools';
|
||||
|
||||
export let text: string;
|
||||
export let link: string;
|
||||
export let colorClass: string = '';
|
||||
</script>
|
||||
|
||||
<div class="center">
|
||||
<FormStyledButton on:click={() => openWebLink(link)} value={text} skipWidth {colorClass} />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
21
packages/web/src/jsonui/JsonUiLinkButtonBlock.svelte
Normal file
21
packages/web/src/jsonui/JsonUiLinkButtonBlock.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import { openWebLink } from '../utility/simpleTools';
|
||||
|
||||
export let text: string;
|
||||
export let link: string;
|
||||
export let colorClass: string = '';
|
||||
export let items: any[] = [];
|
||||
</script>
|
||||
|
||||
<div class="center">
|
||||
{#each items as item}
|
||||
<FormStyledButton on:click={() => openWebLink(item.link)} value={item.text} skipWidth {colorClass} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.center {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user