Merge branch 'master' into feature/svelte4

This commit is contained in:
SPRINX0\prochazka
2025-12-19 13:26:16 +01:00
464 changed files with 30266 additions and 5200 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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} />

View 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>

View File

@@ -0,0 +1 @@
This component is only for Premium edition

View File

@@ -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 },
];

View File

@@ -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}

View File

@@ -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} />

View File

@@ -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}

View File

@@ -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) : [];

View File

@@ -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: {

View File

@@ -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') } }),
},
];
}

View File

@@ -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

View File

@@ -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])}

View File

@@ -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')}

View File

@@ -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,
},
},

View File

@@ -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',
},
],
};

View 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>

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}
>

View File

@@ -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}

View File

@@ -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">

View File

@@ -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>

View File

@@ -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}

View File

@@ -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>

View 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>

View File

@@ -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);

View File

@@ -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'])}
/>

View File

@@ -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">

View File

@@ -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);
},
});

View File

@@ -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),
});

View File

@@ -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

View File

@@ -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
);

View File

@@ -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;

View File

@@ -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>

View File

@@ -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(() => {

View File

@@ -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}

View File

@@ -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>

View File

@@ -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">

View File

@@ -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}

View File

@@ -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;

View File

@@ -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]}

View File

@@ -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;

View File

@@ -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

View File

@@ -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"

View File

@@ -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>

View File

@@ -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 () => {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -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 &amp; 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>

View File

@@ -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 &&

View File

@@ -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>

View File

@@ -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,

View File

@@ -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)));

View 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>

View File

@@ -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>

View File

@@ -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;

View File

@@ -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}

View File

@@ -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'}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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,
});
}

View File

@@ -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}

View File

@@ -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>

View File

@@ -0,0 +1 @@
This file is part of DbGate Premium

View File

@@ -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 />

View 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>

View File

@@ -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}

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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 &amp; drop imported files here</div>
<div class="wrapper">{_t('importExport.dragDropImportedFilesHere', { defaultMessage: "Drag & drop imported files here" })}</div>
</div>
<style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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'}

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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>

View File

@@ -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}
/>

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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>

View 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}

View 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>

View 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>

View 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>

View 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>

View 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