Merge branch 'master' into sqlite

This commit is contained in:
Jan Prochazka
2021-05-03 18:43:34 +02:00
41 changed files with 419 additions and 400 deletions

View File

@@ -19,6 +19,13 @@
onConfirm: () => axiosInstance.post('connections/delete', data),
});
};
const handleDuplicate = () => {
axiosInstance.post('connections/save', {
...data,
_id: undefined,
displayName: `${data.displayName || data.server} - copy`,
});
};
const handleCreateDatabase = () => {
showModal(InputTextModal, {
header: 'Create database',
@@ -54,6 +61,10 @@
text: 'Delete',
onClick: handleDelete,
},
{
text: 'Duplicate',
onClick: handleDuplicate,
},
],
!data.singleDatabase && [
!$openedConnections.includes(data._id) && {

View File

@@ -27,6 +27,7 @@ export interface GlobalCommand {
menuName?: string;
toolbarOrder?: number;
disableHandleKeyText?: string;
isRelatedToTab?: boolean,
}
export default function registerCommand(command: GlobalCommand) {

View File

@@ -251,6 +251,7 @@ export function registerFileCommands({
// keyText: 'Ctrl+S',
icon: 'icon save',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor() != null,
onClick: () => saveTabFile(getCurrentEditor(), false, folder, format, fileExtension),
});
@@ -271,6 +272,7 @@ export function registerFileCommands({
name: 'Execute',
icon: 'icon run',
toolbar: true,
isRelatedToTab: true,
keyText: 'F5 | Ctrl+Enter',
testEnabled: () => getCurrentEditor() != null && !getCurrentEditor()?.isBusy(),
onClick: () => getCurrentEditor().execute(),
@@ -281,6 +283,7 @@ export function registerFileCommands({
name: 'Kill',
icon: 'icon close',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.canKill && getCurrentEditor().canKill(),
onClick: () => getCurrentEditor().kill(),
});

View File

@@ -7,6 +7,7 @@
name: 'Refresh',
keyText: 'F5',
toolbar: true,
isRelatedToTab: true,
icon: 'icon reload',
testEnabled: () => getCurrentDataGrid()?.getDisplay()?.supportsReload,
onClick: () => getCurrentDataGrid().refresh(),
@@ -63,6 +64,7 @@
group: 'undo',
icon: 'icon undo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataGrid()?.getGrider()?.canUndo,
onClick: () => getCurrentDataGrid().undo(),
});
@@ -74,6 +76,7 @@
group: 'redo',
icon: 'icon redo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataGrid()?.getGrider()?.canRedo,
onClick: () => getCurrentDataGrid().redo(),
});

View File

@@ -54,6 +54,9 @@ export function countColumnSizes(grider: Grider, columns, containerWidth, displa
context.font = '14px Helvetica';
for (let rowIndex = 0; rowIndex < Math.min(grider.rowCount, 20); rowIndex += 1) {
const row = grider.getRowData(rowIndex);
if (!row) {
continue;
}
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
const uqName = columns[colIndex].uniqueName;

View File

@@ -18,6 +18,7 @@
name: 'Refresh',
keyText: 'F5',
toolbar: true,
isRelatedToTab: true,
icon: 'icon reload',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().refresh(),
@@ -58,6 +59,7 @@
group: 'undo',
icon: 'icon undo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataForm()?.getFormer()?.canUndo,
onClick: () => getCurrentDataForm().getFormer().undo(),
});
@@ -69,6 +71,7 @@
group: 'redo',
icon: 'icon redo',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataForm()?.getFormer()?.canRedo,
onClick: () => getCurrentDataForm().getFormer().redo(),
});
@@ -104,6 +107,7 @@
name: 'First',
keyText: 'Ctrl+Home',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-begin',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('begin'),
@@ -115,6 +119,7 @@
name: 'Previous',
keyText: 'Ctrl+ArrowUp',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-left',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('previous'),
@@ -126,6 +131,7 @@
name: 'Next',
keyText: 'Ctrl+ArrowDown',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-right',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('next'),
@@ -137,6 +143,7 @@
name: 'Last',
keyText: 'Ctrl+End',
toolbar: true,
isRelatedToTab: true,
icon: 'icon arrow-end',
testEnabled: () => getCurrentDataForm() != null,
onClick: () => getCurrentDataForm().navigate('end'),

View File

@@ -15,11 +15,7 @@
const { values, setFieldValue } = getFormContext();
$: dbinfo = useDatabaseInfo({ conid: $values[conidName], database: $values[databaseName] });
$: tablesOptions = [
...(($dbinfo && $dbinfo.tables) || []),
...(($dbinfo && $dbinfo.views) || []),
...(($dbinfo && $dbinfo.collections) || []),
]
$: tablesOptions = _.compact([...($dbinfo?.tables || []), ...($dbinfo?.views || []), ...($dbinfo?.collections || [])])
.filter(x => !$values[schemaName] || x.schemaName == $values[schemaName])
.map(x => ({
value: x.pureName,
@@ -31,18 +27,20 @@
<FormSelectField {...$$restProps} {name} options={tablesOptions} isMulti templateProps={{ noMargin: true }} />
<div>
<FormStyledButton
type="button"
value="All tables"
on:click={() =>
setFieldValue(name, _.uniq([...($values[name] || []), ...($dbinfo && $dbinfo.tables.map(x => x.pureName))]))}
/>
<FormStyledButton
type="button"
value="All views"
on:click={() =>
setFieldValue(name, _.uniq([...($values[name] || []), ...($dbinfo && $dbinfo.views.map(x => x.pureName))]))}
/>
{#each ['tables', 'views', 'collections'] as field}
{#if $dbinfo && $dbinfo[field]?.length > 0}
<FormStyledButton
type="button"
value={`All ${field}`}
on:click={() =>
setFieldValue(
name,
_.compact(_.uniq([...($values[name] || []), ...($dbinfo[field]?.map(x => x.pureName) || [])]))
)}
/>
{/if}
{/each}
<FormStyledButton type="button" value="Remove all" on:click={() => setFieldValue(name, [])} />
</div>
</div>

View File

@@ -17,7 +17,7 @@
}
</script>
{#each plugins as packageManifest (packageManifest.name)}
{#each plugins || [] as packageManifest (packageManifest.name)}
<div class="wrapper" on:click={() => openPlugin(packageManifest)}>
<img class="icon" src={extractPluginIcon(packageManifest)} />
<div class="ml-2">

View File

@@ -11,10 +11,15 @@
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-javascript';
import 'ace-builds/src-noconflict/mode-markdown';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-twilight';
import 'ace-builds/src-noconflict/ext-searchbox';
import 'ace-builds/src-noconflict/ext-language_tools';
import 'ace-builds/src-noconflict/theme-github';
// import 'ace-builds/src-noconflict/theme-sqlserver';
import 'ace-builds/src-noconflict/theme-twilight';
// import 'ace-builds/src-noconflict/theme-monokai';
import { currentDropDownMenu, currentThemeDefinition } from '../stores';
import _ from 'lodash';
import { handleCommandKeyDown } from '../commands/CommandListener.svelte';

View File

@@ -19,6 +19,8 @@ function getParsedLocalStorage(key) {
return null;
}
const saveHandlersList = [];
export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = null, onInitialData = null }) {
const localStorageKey = `tabdata_editor_${tabid}`;
let changeCounter = 0;
@@ -90,6 +92,11 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
}));
};
const saveToStorageIfNeeded = async () => {
if (savedCounter == changeCounter) return; // all saved
await saveToStorage();
};
const saveToStorage = async () => {
if (value == null) return;
try {
@@ -128,11 +135,13 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
onMount(() => {
window.addEventListener('beforeunload', saveToStorageSync);
initialLoad();
saveHandlersList.push(saveToStorageIfNeeded);
});
onDestroy(() => {
saveToStorage();
window.removeEventListener('beforeunload', saveToStorageSync);
_.remove(saveHandlersList, x => x == saveToStorageIfNeeded);
});
return {
@@ -144,3 +153,9 @@ export default function useEditorData({ tabid, reloadToken = 0, loadFromArgs = n
initialLoad,
};
}
export async function saveAllPendingEditorData() {
for (const item of saveHandlersList) {
await item();
}
}

View File

@@ -7,6 +7,7 @@
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import FormTextField from '../forms/FormTextField.svelte';
import FormValues from '../forms/FormValues.svelte';
import ModalBase from '../modals/ModalBase.svelte';
import { closeCurrentModal } from '../modals/modalTools';
@@ -32,17 +33,32 @@
<ModalBase {...$$restProps}>
<div slot="header">Settings</div>
<div class="heading">Appearance</div>
<FormCheckboxField name=":visibleToolbar" label="Show toolbar" defaultValue={true} />
<FormValues let:values>
<div class="heading">Appearance</div>
<FormCheckboxField name=":visibleToolbar" label="Show toolbar" defaultValue={true} />
<div class="heading">Data grid</div>
<FormCheckboxField name="dataGrid.hideLeftColumn" label="Hide left column by default" />
<FormTextField
name="dataGrid.pageSize"
label="Page size (number of rows for incremental loading, must be between 5 and 1000)"
defaultValue="100"
/>
<FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} />
<div class="heading">Data grid</div>
<FormCheckboxField name="dataGrid.hideLeftColumn" label="Hide left column by default" />
<FormTextField
name="dataGrid.pageSize"
label="Page size (number of rows for incremental loading, must be between 5 and 1000)"
defaultValue="100"
/>
<FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} />
<div class="heading">Connection</div>
<FormCheckboxField
name="connection.autoRefresh"
label="Automatic refresh of database model on background"
defaultValue={true}
/>
<FormTextField
name="connection.autoRefreshInterval"
label="Interval between automatic refreshes in seconds"
defaultValue="30"
disabled={values['connection.autoRefresh'] === false}
/>
</FormValues>
<div slot="footer">
<FormSubmit value="OK" on:click={handleOk} />

View File

@@ -11,6 +11,7 @@
name: 'Save',
// keyText: 'Ctrl+S',
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor()?.canSave(),
onClick: () => getCurrentEditor().save(),

View File

@@ -8,6 +8,7 @@
name: 'Save',
// keyText: 'Ctrl+S',
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().save(),

View File

@@ -19,6 +19,7 @@
name: 'Preview',
icon: 'icon run',
toolbar: true,
isRelatedToTab: true,
keyText: 'F5 | Ctrl+Enter',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().preview(),

View File

@@ -8,6 +8,7 @@
name: 'Save',
// keyText: 'Ctrl+S',
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor()?.canSave(),
onClick: () => getCurrentEditor().save(),

View File

@@ -13,6 +13,8 @@
console.log('CRASH DETECTED!!!');
const lastDbGateCrashJson = localStorage.getItem('lastDbGateCrash');
const lastDbGateCrash = lastDbGateCrashJson ? JSON.parse(lastDbGateCrashJson) : null;
// let detail = e?.reason?.stack || '';
// if (detail) detail = '\n\n' + detail;
if (lastDbGateCrash && new Date().getTime() - lastDbGateCrash < 30 * 1000) {
if (

View File

@@ -6,6 +6,7 @@ import tabs from '../tabs';
import { setSelectedTabFunc } from './common';
import localforage from 'localforage';
import stableStringify from 'json-stable-stringify';
import { saveAllPendingEditorData } from '../query/useEditorData';
function findFreeNumber(numbers: number[]) {
if (numbers.length == 0) return 1;
@@ -74,9 +75,9 @@ export default async function openNewTab(newTab, initialData = undefined, option
openedTabs.update(files => [
...(files || []).map(x => ({ ...x, selected: false })),
{
...newTab,
tabid,
selected: true,
...newTab,
},
]);
@@ -91,3 +92,35 @@ export default async function openNewTab(newTab, initialData = undefined, option
// },
// ]);
}
export async function duplicateTab(tab) {
await saveAllPendingEditorData();
let title = tab.title;
const mtitle = title.match(/^(.*#)[\d]+$/);
if (mtitle) title = mtitle[1];
const keyRegex = /^tabdata_([^_]+)_([^_]+)$/;
const initialData = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
const m = key.match(keyRegex);
if (m && m[2] == tab.tabid) {
initialData[m[1]] = JSON.parse(localStorage.getItem(key));
}
}
for (const key of await localforage.keys()) {
const m = key.match(keyRegex);
if (m && m[2] == tab.tabid) {
initialData[m[1]] = await localforage.getItem(key);
}
}
openNewTab(
{
..._.omit(tab, ['tabid']),
title,
},
initialData,
{ forceNewTab: true }
);
}

View File

@@ -42,7 +42,7 @@
<ErrorInfo message={$status.message} icon="img error" />
<InlineButton on:click={handleRefreshDatabase}>Refresh</InlineButton>
</WidgetsInnerContainer>
{:else if objectList.length == 0 && $status && $status.name != 'pending' && $objects}
{:else if objectList.length == 0 && $status && $status.name != 'pending' && $status.name != 'checkStructure' && $status.name != 'loadStructure' && $objects}
<WidgetsInnerContainer>
<ErrorInfo
message={`Database ${database} is empty or structure is not loaded, press Refresh button to reload structure`}
@@ -56,7 +56,7 @@
<InlineButton on:click={handleRefreshDatabase}>Refresh</InlineButton>
</SearchBoxWrapper>
<WidgetsInnerContainer>
{#if ($status && $status.name == 'pending' && $objects) || !$objects}
{#if ($status && ($status.name == 'pending' || $status.name == 'checkStructure' || $status.name == 'loadStructure') && $objects) || !$objects}
<LoadingInfo message="Loading database structure" />
{:else}
<AppObjectList

View File

@@ -49,6 +49,10 @@
<div class="item">
{#if $status.name == 'pending'}
<FontIcon icon="icon loading" /> Loading
{:else if $status.name == 'checkStructure'}
<FontIcon icon="icon loading" /> Checking model
{:else if $status.name == 'loadStructure'}
<FontIcon icon="icon loading" /> Loading model
{:else if $status.name == 'ok'}
<FontIcon icon="img ok-inv" /> Connected
{:else if $status.name == 'error'}

View File

@@ -87,9 +87,9 @@
registerCommand({
id: 'tabs.addToFavorites',
category: 'Tabs',
name: 'Favorites',
icon: 'icon favorite',
toolbar: true,
name: 'Add current tab to favorites',
// icon: 'icon favorite',
// toolbar: true,
testEnabled: () =>
getActiveTab()?.tabComponent &&
tabs[getActiveTab()?.tabComponent] &&
@@ -113,6 +113,7 @@
import { setSelectedTab } from '../utility/common';
import contextMenu from '../utility/contextMenu';
import { getConnectionInfo } from '../utility/metadataLoaders';
import { duplicateTab } from '../utility/openNewTab';
$: currentDbKey =
$currentDatabase && $currentDatabase.name && $currentDatabase.connection
@@ -146,9 +147,10 @@
}
};
const getContextMenu = (tabid, props) => () => {
const getContextMenu = tab => () => {
const { tabid, props, tabComponent } = tab;
const { conid, database } = props || {};
const res = [
return [
{
text: 'Close',
onClick: () => closeTab(tabid),
@@ -161,20 +163,33 @@
text: 'Close others',
onClick: () => closeOthers(tabid),
},
{
text: 'Duplicate',
onClick: () => duplicateTab(tab),
},
tabComponent &&
tabs[tabComponent] &&
tabs[tabComponent].allowAddToFavorites &&
tabs[tabComponent].allowAddToFavorites(props) && [
{ divider: true },
{
text: 'Add to favorites',
onClick: () => showModal(FavoriteModal, { savingTab: tab }),
},
],
conid &&
database && [
{ divider: true },
{
text: `Close with same DB - ${database}`,
onClick: () => closeWithSameDb(tabid),
},
{
text: `Close with other DB than ${database}`,
onClick: () => closeWithOtherDb(tabid),
},
],
];
if (conid && database) {
res.push(
{
text: `Close with same DB - ${database}`,
onClick: () => closeWithSameDb(tabid),
},
{
text: `Close with other DB than ${database}`,
onClick: () => closeWithOtherDb(tabid),
}
);
}
return res;
};
const handleSetDb = async props => {
@@ -216,7 +231,7 @@
class:selected={tab.selected}
on:click={e => handleTabClick(e, tab.tabid)}
on:mouseup={e => handleMouseUp(e, tab.tabid)}
use:contextMenu={getContextMenu(tab.tabid, tab.props)}
use:contextMenu={getContextMenu(tab)}
>
<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />
<span class="file-name">

View File

@@ -10,7 +10,8 @@
import _ from 'lodash';
import { openFavorite } from '../appobj/FavoriteFileAppObject.svelte';
import runCommand from '../commands/runCommand';
import { commands, commandsCustomized } from '../stores';
import FontIcon from '../icons/FontIcon.svelte';
import { activeTab, commands, commandsCustomized } from '../stores';
import getElectron from '../utility/getElectron';
import { useFavorites } from '../utility/metadataLoaders';
import ToolbarButton from './ToolbarButton.svelte';
@@ -25,26 +26,48 @@
);
</script>
<div class="container">
{#if !electron}
<ToolbarButton externalImage="logo192.png" on:click={() => runCommand('about.show')} />
{/if}
{#each ($favorites || []).filter(x => x.showInToolbar) as item}
<ToolbarButton on:click={() => openFavorite(item)} icon={item.icon || 'icon favorite'}>
{item.title}
</ToolbarButton>
{/each}
<div class="root">
<div class="container">
{#if !electron}
<ToolbarButton externalImage="logo192.png" on:click={() => runCommand('about.show')} />
{/if}
{#each ($favorites || []).filter(x => x.showInToolbar) as item}
<ToolbarButton on:click={() => openFavorite(item)} icon={item.icon || 'icon favorite'}>
{item.title}
</ToolbarButton>
{/each}
{#each list as command}
<ToolbarButton
icon={command.icon}
on:click={command.onClick}
disabled={!command.enabled}
title={getCommandTitle(command)}
>
{command.toolbarName || command.name}
</ToolbarButton>
{/each}
{#each list.filter(x => !x.isRelatedToTab) as command}
<ToolbarButton
icon={command.icon}
on:click={command.onClick}
disabled={!command.enabled}
title={getCommandTitle(command)}
>
{command.toolbarName || command.name}
</ToolbarButton>
{/each}
</div>
<div class="container">
{#if $activeTab && list.filter(x => x.isRelatedToTab).length > 0}
<div class="activeTab">
<div class="activeTabInner">
<FontIcon icon={$activeTab.icon} />
{$activeTab.title}:
</div>
</div>
{/if}
{#each list.filter(x => x.isRelatedToTab) as command}
<ToolbarButton
icon={command.icon}
on:click={command.onClick}
disabled={!command.enabled}
title={getCommandTitle(command)}
>
{command.toolbarName || command.name}
</ToolbarButton>
{/each}
</div>
</div>
<style>
@@ -54,4 +77,21 @@
align-items: stretch;
height: var(--dim-toolbar-height);
}
.root {
display: flex;
align-items: stretch;
justify-content: space-between;
}
.activeTab {
background-color: var(--theme-bg-2);
white-space: nowrap;
display: flex;
padding-left: 15px;
padding-right: 15px;
}
.activeTabInner {
align-self: center;
}
</style>