diff --git a/.gitignore b/.gitignore index 81bf4932c..b68165511 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,8 @@ node_modules build dist +app/packages/web/public + # misc .DS_Store .env.local @@ -24,3 +26,4 @@ yarn-debug.log* yarn-error.log* app/src/nativeModulesContent.js packages/api/src/nativeModulesContent.js +.VSCodeCounter \ No newline at end of file diff --git a/app/package.json b/app/package.json index b70b7e738..36b9526e9 100644 --- a/app/package.json +++ b/app/package.json @@ -68,10 +68,10 @@ "start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .", "start:local": "cross-env electron .", "dist": "electron-builder", - "build": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn dist", - "build:local": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn predist", + "build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist", + "build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist", "postinstall": "electron-builder install-app-deps", - "predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/build/*\" packages && copyfiles \"../packages/web/build/**/*\" packages" + "predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages" }, "main": "src/electron.js", "devDependencies": { diff --git a/app/src/electron.js b/app/src/electron.js index d6df7c9b1..933383cde 100644 --- a/app/src/electron.js +++ b/app/src/electron.js @@ -27,6 +27,8 @@ autoUpdater.logger = log; // TODO - create settings for this // appUpdater.channel = 'beta'; +let commands = {}; + function hideSplash() { if (splashWindow) { splashWindow.destroy(); @@ -35,61 +37,35 @@ function hideSplash() { mainWindow.show(); } +function commandItem(id) { + const command = commands[id]; + return { + id, + label: command ? command.menuName || command.toolbarName || command.name : id, + accelerator: command ? command.keyText : undefined, + enabled: command ? command.enabled : false, + click() { + mainWindow.webContents.executeJavaScript(`dbgate_runCommand('${id}')`); + }, + }; +} + function buildMenu() { const template = [ { label: 'File', submenu: [ - { - label: 'Connect to database', - click() { - mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`); - }, - }, - { - label: 'Open file', - click() { - mainWindow.webContents.executeJavaScript(`dbgate_openFile()`); - }, - }, - { - label: 'Save', - click() { - mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('save')`); - }, - accelerator: 'Ctrl+S', - id: 'save', - }, - { - label: 'Save As', - click() { - mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('saveAs')`); - }, - accelerator: 'Ctrl+Shift+S', - id: 'saveAs', - }, + commandItem('new.connection'), + commandItem('file.open'), + commandItem('group.save'), + commandItem('group.saveAs'), { type: 'separator' }, { role: 'close' }, ], }, { label: 'Window', - submenu: [ - { - label: 'New query', - click() { - mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`); - }, - }, - { type: 'separator' }, - { - label: 'Close all tabs', - click() { - mainWindow.webContents.executeJavaScript('dbgate_closeAll()'); - }, - }, - { role: 'minimize' }, - ], + submenu: [commandItem('new.query'), { type: 'separator' }, commandItem('tabs.closeAll'), { role: 'minimize' }], }, // { @@ -144,12 +120,7 @@ function buildMenu() { require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new'); }, }, - { - label: 'About', - click() { - mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`); - }, - }, + commandItem('about.show'), ], }, ]; @@ -157,10 +128,22 @@ function buildMenu() { return Menu.buildFromTemplate(template); } -ipcMain.on('update-menu', async (event, arg) => { - const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`); - mainMenu.getMenuItemById('save').enabled = !!commands.save; - mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs; +ipcMain.on('update-commands', async (event, arg) => { + commands = JSON.parse(arg); + for (const key of Object.keys(commands)) { + const menu = mainMenu.getMenuItemById(key); + if (!menu) continue; + const command = commands[key]; + + // rebuild menu + if (menu.label != command.text || menu.accelerator != command.keyText) { + mainMenu = buildMenu(); + mainWindow.setMenu(mainMenu); + return; + } + + menu.enabled = command.enabled; + } }); function createWindow() { @@ -186,7 +169,7 @@ function createWindow() { const startUrl = process.env.ELECTRON_START_URL || url.format({ - pathname: path.join(__dirname, '../packages/web/build/index.html'), + pathname: path.join(__dirname, '../packages/web/public/index.html'), protocol: 'file:', slashes: true, }); diff --git a/package.json b/package.json index 6388eab53..ad23104c9 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "start:api": "yarn workspace dbgate-api start", "start:api:portal": "yarn workspace dbgate-api start:portal", "start:api:covid": "yarn workspace dbgate-api start:covid", - "start:web": "yarn workspace dbgate-web start", + "start:web": "yarn workspace dbgate-web dev", "start:sqltree": "yarn workspace dbgate-sqltree start", "start:tools": "yarn workspace dbgate-tools start", "start:datalib": "yarn workspace dbgate-datalib start", @@ -21,7 +21,7 @@ "build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib", "build:app": "cd app && yarn install && yarn build", "build:api": "yarn workspace dbgate-api build", - "build:web:docker": "yarn workspace dbgate-web build:docker", + "build:web:docker": "yarn workspace dbgate-web build", "build:app:local": "cd app && yarn build:local", "start:app:local": "cd app && yarn start:local", "setCurrentVersion": "node setCurrentVersion", @@ -29,7 +29,7 @@ "fillNativeModules": "node fillNativeModules", "fillNativeModulesElectron": "node fillNativeModules --eletron", "prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src", - "copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2", + "copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2", "prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build", "prepare": "yarn build:lib", "start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"", diff --git a/packages/api/src/controllers/jsldata.js b/packages/api/src/controllers/jsldata.js index 1bf19df08..84ce60e5f 100644 --- a/packages/api/src/controllers/jsldata.js +++ b/packages/api/src/controllers/jsldata.js @@ -121,7 +121,13 @@ module.exports = { getStats_meta: 'get', getStats({ jslid }) { const file = `${getJslFileName(jslid)}.stats`; - if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf-8')); + if (fs.existsSync(file)) { + try { + return JSON.parse(fs.readFileSync(file, 'utf-8')); + } catch (e) { + return {}; + } + } return {}; }, diff --git a/packages/api/src/controllers/plugins.js b/packages/api/src/controllers/plugins.js index bc438ce42..794019433 100644 --- a/packages/api/src/controllers/plugins.js +++ b/packages/api/src/controllers/plugins.js @@ -29,7 +29,7 @@ const hasPermission = require('../utility/hasPermission'); const preinstallPluginMinimalVersions = { 'dbgate-plugin-mssql': '1.1.0', - 'dbgate-plugin-mysql': '1.1.0', + 'dbgate-plugin-mysql': '1.1.1', 'dbgate-plugin-postgres': '1.1.0', 'dbgate-plugin-csv': '1.0.8', 'dbgate-plugin-excel': '1.0.6', @@ -149,7 +149,7 @@ module.exports = { return content.commands[command](args); }, - authTypes_meta: 'post', + authTypes_meta: 'get', async authTypes({ engine }) { const packageName = extractPackageName(engine); const content = requirePlugin(packageName); diff --git a/packages/datalib/src/ChangeSet.ts b/packages/datalib/src/ChangeSet.ts index 273844754..37d8c6752 100644 --- a/packages/datalib/src/ChangeSet.ts +++ b/packages/datalib/src/ChangeSet.ts @@ -347,5 +347,6 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn } export function changeSetContainsChanges(changeSet: ChangeSet) { + if (!changeSet) return false; return changeSet.deletes.length > 0 || changeSet.updates.length > 0 || changeSet.inserts.length > 0; } diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts index 9544c1cd1..a8e3cc753 100644 --- a/packages/types/engines.d.ts +++ b/packages/types/engines.d.ts @@ -27,6 +27,7 @@ export interface EngineAuthType { export interface EngineDriver { engine: string; title: string; + defaultPort?: number; connect({ server, port, user, password, database }): any; query(pool: any, sql: string): Promise; stream(pool: any, sql: string, options: StreamOptions); diff --git a/packages/types/extensions.d.ts b/packages/types/extensions.d.ts index ad6560e43..fbd77cc61 100644 --- a/packages/types/extensions.d.ts +++ b/packages/types/extensions.d.ts @@ -21,6 +21,11 @@ export interface FileFormatDefinition { getOutputParams?: (sourceName, values) => any; } +export interface ThemeDefinition { + className: string; + themeName: string; +} + export interface PluginDefinition { packageName: string; manifest: any; @@ -31,4 +36,5 @@ export interface ExtensionsDirectory { plugins: PluginDefinition[]; fileFormats: FileFormatDefinition[]; drivers: EngineDriver[]; + themes: ThemeDefinition[]; } diff --git a/packages/web/.eslintrc.js b/packages/web/.eslintrc.js deleted file mode 100644 index a53d0ca70..000000000 --- a/packages/web/.eslintrc.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - "env": { - "browser": true, - "es6": true - }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended" - ], - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 2018, - "sourceType": "module" - }, - "plugins": [ - "react" - ], - "rules": { - "react/prop-types": "off", - "no-unused-vars": "warn" - } -}; \ No newline at end of file diff --git a/packages/web/README.md b/packages/web/README.md new file mode 100644 index 000000000..7b1ba8363 --- /dev/null +++ b/packages/web/README.md @@ -0,0 +1,105 @@ +*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)* + +--- + +# svelte app + +This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template. + +To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit): + +```bash +npx degit sveltejs/template svelte-app +cd svelte-app +``` + +*Note that you will need to have [Node.js](https://nodejs.org) installed.* + + +## Get started + +Install the dependencies... + +```bash +cd svelte-app +npm install +``` + +...then start [Rollup](https://rollupjs.org): + +```bash +npm run dev +``` + +Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes. + +By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`. + +If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense. + +## Building and running in production mode + +To create an optimised version of the app: + +```bash +npm run build +``` + +You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com). + + +## Single-page app mode + +By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere. + +If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json: + +```js +"start": "sirv public --single" +``` + +## Using TypeScript + +This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with: + +```bash +node scripts/setupTypeScript.js +``` + +Or remove the script via: + +```bash +rm scripts/setupTypeScript.js +``` + +## Deploying to the web + +### With [Vercel](https://vercel.com) + +Install `vercel` if you haven't already: + +```bash +npm install -g vercel +``` + +Then, from within your project folder: + +```bash +cd public +vercel deploy --name my-project +``` + +### With [surge](https://surge.sh/) + +Install `surge` if you haven't already: + +```bash +npm install -g surge +``` + +Then, from within your project folder: + +```bash +npm run build +surge public my-project.surge.sh +``` diff --git a/packages/web/package.json b/packages/web/package.json index 77b431d6c..868b71b53 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,69 +1,51 @@ { "name": "dbgate-web", - "version": "3.9.5", - "files": [ - "build" - ], + "version": "1.0.0", "scripts": { - "start": "cross-env BROWSER=none PORT=5000 react-scripts start", - "build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build", - "build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build", - "build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build", - "prepublishOnly": "yarn build", - "test": "react-scripts test", - "eject": "react-scripts eject", - "ts": "tsc" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] + "build": "cross-env API_URL=ORIGIN rollup -c", + "dev": "cross-env API_URL=http://localhost:3000 rollup -c -w", + "start": "sirv public", + "validate": "svelte-check" }, "devDependencies": { - "@types/react": "^16.9.17", - "@types/styled-components": "^4.4.2", - "dbgate-types": "^3.9.5", - "typescript": "^3.7.4", "@ant-design/colors": "^5.0.0", - "@mdi/font": "^5.8.55", - "@testing-library/jest-dom": "^4.2.4", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^7.1.2", + "@rollup/plugin-commonjs": "^17.0.0", + "@rollup/plugin-node-resolve": "^11.0.0", + "@rollup/plugin-replace": "^2.4.1", + "@rollup/plugin-typescript": "^6.0.0", + "@tsconfig/svelte": "^1.0.0", "ace-builds": "^1.4.8", - "axios": "^0.19.0", "chart.js": "^2.9.4", - "compare-versions": "^3.6.0", - "cross-env": "^6.0.3", + "cross-env": "^7.0.3", "dbgate-datalib": "^3.9.5", "dbgate-sqltree": "^3.9.5", "dbgate-tools": "^3.9.5", - "eslint": "^6.8.0", - "eslint-plugin-react": "^7.17.0", + "dbgate-types": "^3.9.5", "json-stable-stringify": "^1.0.1", "localforage": "^1.9.0", - "markdown-to-jsx": "^7.1.0", + "lodash": "^4.17.15", "randomcolor": "^0.6.2", - "react": "^16.12.0", - "react-ace": "^8.0.0", - "react-chartjs-2": "^2.11.1", - "react-dom": "^16.12.0", - "react-dropzone": "^11.2.3", - "react-helmet": "^6.1.0", - "react-json-view": "^1.19.1", - "react-modal": "^3.11.1", - "react-scripts": "3.3.0", - "react-select": "^3.1.0", - "resize-observer-polyfill": "^1.5.1", + "rollup": "^2.3.4", + "rollup-plugin-copy": "^3.3.0", + "rollup-plugin-css-only": "^3.1.0", + "rollup-plugin-livereload": "^2.0.0", + "rollup-plugin-svelte": "^7.0.0", + "rollup-plugin-terser": "^7.0.0", "socket.io-client": "^2.3.0", "sql-formatter": "^2.3.3", - "styled-components": "^4.4.1", + "svelte": "^3.35.0", + "svelte-check": "^1.0.0", + "svelte-preprocess": "^4.0.0", + "tslib": "^2.0.0", + "typescript": "^3.9.3", "uuid": "^3.4.0" + }, + "dependencies": { + "@mdi/font": "^5.9.55", + "file-selector": "^0.2.4", + "resize-observer-polyfill": "^1.5.1", + "sirv-cli": "^1.0.0", + "svelte-markdown": "^0.1.4", + "svelte-select": "^3.17.0" } -} \ No newline at end of file +} diff --git a/packages/web/public/bulma.css b/packages/web/public/bulma.css new file mode 100644 index 000000000..6492cd0c3 --- /dev/null +++ b/packages/web/public/bulma.css @@ -0,0 +1,419 @@ +.m-0 { + margin: 0 !important; +} + +.mt-0 { + margin-top: 0 !important; +} + +.mr-0 { + margin-right: 0 !important; +} + +.mb-0 { + margin-bottom: 0 !important; +} + +.ml-0 { + margin-left: 0 !important; +} + +.mx-0 { + margin-left: 0 !important; + margin-right: 0 !important; +} + +.my-0 { + margin-top: 0 !important; + margin-bottom: 0 !important; +} + +.m-1 { + margin: 0.25rem !important; +} + +.mt-1 { + margin-top: 0.25rem !important; +} + +.mr-1 { + margin-right: 0.25rem !important; +} + +.mb-1 { + margin-bottom: 0.25rem !important; +} + +.ml-1 { + margin-left: 0.25rem !important; +} + +.mx-1 { + margin-left: 0.25rem !important; + margin-right: 0.25rem !important; +} + +.my-1 { + margin-top: 0.25rem !important; + margin-bottom: 0.25rem !important; +} + +.m-2 { + margin: 0.5rem !important; +} + +.mt-2 { + margin-top: 0.5rem !important; +} + +.mr-2 { + margin-right: 0.5rem !important; +} + +.mb-2 { + margin-bottom: 0.5rem !important; +} + +.ml-2 { + margin-left: 0.5rem !important; +} + +.mx-2 { + margin-left: 0.5rem !important; + margin-right: 0.5rem !important; +} + +.my-2 { + margin-top: 0.5rem !important; + margin-bottom: 0.5rem !important; +} + +.m-3 { + margin: 0.75rem !important; +} + +.mt-3 { + margin-top: 0.75rem !important; +} + +.mr-3 { + margin-right: 0.75rem !important; +} + +.mb-3 { + margin-bottom: 0.75rem !important; +} + +.ml-3 { + margin-left: 0.75rem !important; +} + +.mx-3 { + margin-left: 0.75rem !important; + margin-right: 0.75rem !important; +} + +.my-3 { + margin-top: 0.75rem !important; + margin-bottom: 0.75rem !important; +} + +.m-4 { + margin: 1rem !important; +} + +.mt-4 { + margin-top: 1rem !important; +} + +.mr-4 { + margin-right: 1rem !important; +} + +.mb-4 { + margin-bottom: 1rem !important; +} + +.ml-4 { + margin-left: 1rem !important; +} + +.mx-4 { + margin-left: 1rem !important; + margin-right: 1rem !important; +} + +.my-4 { + margin-top: 1rem !important; + margin-bottom: 1rem !important; +} + +.m-5 { + margin: 1.5rem !important; +} + +.mt-5 { + margin-top: 1.5rem !important; +} + +.mr-5 { + margin-right: 1.5rem !important; +} + +.mb-5 { + margin-bottom: 1.5rem !important; +} + +.ml-5 { + margin-left: 1.5rem !important; +} + +.mx-5 { + margin-left: 1.5rem !important; + margin-right: 1.5rem !important; +} + +.my-5 { + margin-top: 1.5rem !important; + margin-bottom: 1.5rem !important; +} + +.m-6 { + margin: 3rem !important; +} + +.mt-6 { + margin-top: 3rem !important; +} + +.mr-6 { + margin-right: 3rem !important; +} + +.mb-6 { + margin-bottom: 3rem !important; +} + +.ml-6 { + margin-left: 3rem !important; +} + +.mx-6 { + margin-left: 3rem !important; + margin-right: 3rem !important; +} + +.my-6 { + margin-top: 3rem !important; + margin-bottom: 3rem !important; +} + +.p-0 { + padding: 0 !important; +} + +.pt-0 { + padding-top: 0 !important; +} + +.pr-0 { + padding-right: 0 !important; +} + +.pb-0 { + padding-bottom: 0 !important; +} + +.pl-0 { + padding-left: 0 !important; +} + +.px-0 { + padding-left: 0 !important; + padding-right: 0 !important; +} + +.py-0 { + padding-top: 0 !important; + padding-bottom: 0 !important; +} + +.p-1 { + padding: 0.25rem !important; +} + +.pt-1 { + padding-top: 0.25rem !important; +} + +.pr-1 { + padding-right: 0.25rem !important; +} + +.pb-1 { + padding-bottom: 0.25rem !important; +} + +.pl-1 { + padding-left: 0.25rem !important; +} + +.px-1 { + padding-left: 0.25rem !important; + padding-right: 0.25rem !important; +} + +.py-1 { + padding-top: 0.25rem !important; + padding-bottom: 0.25rem !important; +} + +.p-2 { + padding: 0.5rem !important; +} + +.pt-2 { + padding-top: 0.5rem !important; +} + +.pr-2 { + padding-right: 0.5rem !important; +} + +.pb-2 { + padding-bottom: 0.5rem !important; +} + +.pl-2 { + padding-left: 0.5rem !important; +} + +.px-2 { + padding-left: 0.5rem !important; + padding-right: 0.5rem !important; +} + +.py-2 { + padding-top: 0.5rem !important; + padding-bottom: 0.5rem !important; +} + +.p-3 { + padding: 0.75rem !important; +} + +.pt-3 { + padding-top: 0.75rem !important; +} + +.pr-3 { + padding-right: 0.75rem !important; +} + +.pb-3 { + padding-bottom: 0.75rem !important; +} + +.pl-3 { + padding-left: 0.75rem !important; +} + +.px-3 { + padding-left: 0.75rem !important; + padding-right: 0.75rem !important; +} + +.py-3 { + padding-top: 0.75rem !important; + padding-bottom: 0.75rem !important; +} + +.p-4 { + padding: 1rem !important; +} + +.pt-4 { + padding-top: 1rem !important; +} + +.pr-4 { + padding-right: 1rem !important; +} + +.pb-4 { + padding-bottom: 1rem !important; +} + +.pl-4 { + padding-left: 1rem !important; +} + +.px-4 { + padding-left: 1rem !important; + padding-right: 1rem !important; +} + +.py-4 { + padding-top: 1rem !important; + padding-bottom: 1rem !important; +} + +.p-5 { + padding: 1.5rem !important; +} + +.pt-5 { + padding-top: 1.5rem !important; +} + +.pr-5 { + padding-right: 1.5rem !important; +} + +.pb-5 { + padding-bottom: 1.5rem !important; +} + +.pl-5 { + padding-left: 1.5rem !important; +} + +.px-5 { + padding-left: 1.5rem !important; + padding-right: 1.5rem !important; +} + +.py-5 { + padding-top: 1.5rem !important; + padding-bottom: 1.5rem !important; +} + +.p-6 { + padding: 3rem !important; +} + +.pt-6 { + padding-top: 3rem !important; +} + +.pr-6 { + padding-right: 3rem !important; +} + +.pb-6 { + padding-bottom: 3rem !important; +} + +.pl-6 { + padding-left: 3rem !important; +} + +.px-6 { + padding-left: 3rem !important; + padding-right: 3rem !important; +} + +.py-6 { + padding-top: 3rem !important; + padding-bottom: 3rem !important; +} diff --git a/packages/web/public/dimensions.css b/packages/web/public/dimensions.css new file mode 100644 index 000000000..cef75b508 --- /dev/null +++ b/packages/web/public/dimensions.css @@ -0,0 +1,22 @@ +:root { + --dim-widget-icon-size: 60px; + --dim-statusbar-height: 20px; + --dim-left-panel-width: 300px; + --dim-tabs-panel-height: 53px; + --dim-tabs-height: 33px; + --dim-splitter-thickness: 3px; + + --dim-visible-left-panel: 1; /* set from JS */ + --dim-content-left: calc( + var(--dim-widget-icon-size) + var(--dim-visible-left-panel) * + (var(--dim-left-panel-width) + var(--dim-splitter-thickness)) + ); + + --dim-visible-toolbar: 1; /* set from JS */ + + --dim-toolbar-height: 30px; + --dim-header-top: calc(var(--dim-toolbar-height) * var(--dim-visible-toolbar)); + --dim-content-top: calc(var(--dim-header-top) + var(--dim-tabs-panel-height)); + + --dim-large-form-margin: 20px; +} diff --git a/packages/web/public/global.css b/packages/web/public/global.css new file mode 100644 index 000000000..9e186f5d2 --- /dev/null +++ b/packages/web/public/global.css @@ -0,0 +1,142 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, HelveticaNeue-Light, Ubuntu, Droid Sans, + sans-serif; + font-size: 14px; + /* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', + 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', + sans-serif; + */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.horizontal-split-handle { + background-color: var(--theme-border); + width: var(--dim-splitter-thickness); + cursor: col-resize; +} +.horizontal-split-handle:hover { + background-color: var(--theme-bg-2); +} + +.vertical-split-handle { + background-color: var(--theme-border); + height: var(--dim-splitter-thickness); + cursor: row-resize; +} +.vertical-split-handle:hover { + background-color: var(--theme-bg-2); +} + +.icon-invisible { + visibility: hidden; +} +.space-between { + display: flex; + justify-content: space-between; +} +.flex { + display: flex; +} +.nowrap { + white-space: nowrap; +} +.bold { + font-weight: bold; +} +.flex1 { + flex: 1; +} + +.col-9 { + flex-basis: 75%; + max-width: 75%; +} +.col-8 { + flex-basis: 66.6667%; + max-width: 66.6667%; +} +.col-6 { + flex-basis: 50%; + max-width: 50%; +} +.col-4 { + flex-basis: 33.3333%; + max-width: 33.3333%; +} +.col-3 { + flex-basis: 25%; + max-width: 25%; +} + +.largeFormMarker input[type='text'] { + width: 100%; + padding: 10px 10px; + font-size: 14px; + box-sizing: border-box; + border-radius: 4px; +} + +.largeFormMarker input[type='password'] { + width: 100%; + padding: 10px 10px; + font-size: 14px; + box-sizing: border-box; + border-radius: 4px; +} + +.largeFormMarker select { + width: 100%; + padding: 10px 10px; + font-size: 14px; + box-sizing: border-box; + border-radius: 4px; +} + +body *::-webkit-scrollbar { + height: 0.8em; + width: 0.8em; +} +body *::-webkit-scrollbar-track { + border-radius: 1px; + background-color: var(--theme-bg-1); +} +body *::-webkit-scrollbar-corner { + border-radius: 1px; + background-color: var(--theme-bg-2); +} + +body *::-webkit-scrollbar-thumb { + border-radius: 1px; + background-color: var(--theme-bg-3); +} + +body *::-webkit-scrollbar-thumb:hover { + background-color: var(--theme-bg-4); +} + +input { + background-color: var(--theme-bg-0); + color: var(--theme-font-1); + border: 1px solid var(--theme-border); +} + +input[disabled] { + background-color: var(--theme-bg-1); +} + +select { + background-color: var(--theme-bg-0); + color: var(--theme-font-1); + border: 1px solid var(--theme-border); +} + +select[disabled] { + background-color: var(--theme-bg-1); +} + +textarea { + background-color: var(--theme-bg-0); + color: var(--theme-font-1); + border: 1px solid var(--theme-border); +} diff --git a/packages/web/public/icon-colors.css b/packages/web/public/icon-colors.css new file mode 100644 index 000000000..ccaf9fe61 --- /dev/null +++ b/packages/web/public/icon-colors.css @@ -0,0 +1,32 @@ +.color-icon-blue { + color: var(--theme-icon-blue); +} + +.color-icon-green { + color: var(--theme-icon-green); +} + +.color-icon-red { + color: var(--theme-icon-red); +} + +.color-icon-gold { + color: var(--theme-icon-gold); +} + +.color-icon-yellow { + color: var(--theme-icon-yellow); +} + +.color-icon-magenta { + color: var(--theme-icon-magenta); +} + + +.color-icon-inv-green { + color: var(--theme-icon-inv-green); +} + +.color-icon-inv-red { + color: var(--theme-icon-inv-red); +} diff --git a/packages/web/public/index.html b/packages/web/public/index.html index 94e81e72b..55ed2489f 100644 --- a/packages/web/public/index.html +++ b/packages/web/public/index.html @@ -2,43 +2,28 @@ - - - - - - - - - - DbGate + DbGate + + + + + + + + + + + + + + - -
Loading DbGate...
- \ No newline at end of file diff --git a/packages/web/public/robots.txt b/packages/web/public/robots.txt deleted file mode 100644 index 01b0f9a10..000000000 --- a/packages/web/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * diff --git a/packages/web/public/splash.css b/packages/web/public/splash.css deleted file mode 100644 index 3bc0c720f..000000000 --- a/packages/web/public/splash.css +++ /dev/null @@ -1,16 +0,0 @@ -body { - background: #666; -} - -div { - color: white; - font-size: 25pt; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - text-align: center; - font-family: Arial, Helvetica, sans-serif; - margin-top: 40px; -} diff --git a/packages/web/public/splash.html b/packages/web/public/splash.html deleted file mode 100644 index 486dce245..000000000 --- a/packages/web/public/splash.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - -
Starting DbGate...
- - - \ No newline at end of file diff --git a/packages/web/rollup.config.js b/packages/web/rollup.config.js new file mode 100644 index 000000000..bd142e58f --- /dev/null +++ b/packages/web/rollup.config.js @@ -0,0 +1,102 @@ +import svelte from 'rollup-plugin-svelte'; +import commonjs from '@rollup/plugin-commonjs'; +import resolve from '@rollup/plugin-node-resolve'; +import livereload from 'rollup-plugin-livereload'; +import copy from 'rollup-plugin-copy'; +import { terser } from 'rollup-plugin-terser'; +import sveltePreprocess from 'svelte-preprocess'; +import typescript from '@rollup/plugin-typescript'; +import replace from '@rollup/plugin-replace'; +import css from 'rollup-plugin-css-only'; + +const production = !process.env.ROLLUP_WATCH; + +function serve() { + let server; + + function toExit() { + if (server) server.kill(0); + } + + return { + writeBundle() { + if (server) return; + server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + stdio: ['ignore', 'inherit', 'inherit'], + shell: true, + }); + + process.on('SIGTERM', toExit); + process.on('exit', toExit); + }, + }; +} + +export default { + input: 'src/main.ts', + output: { + sourcemap: true, + format: 'iife', + name: 'app', + file: 'public/build/bundle.js', + }, + plugins: [ + copy({ + targets: [ + { + src: '../../node_modules/@mdi/font/css/materialdesignicons.css', + dest: 'public/build/fonts/', + }, + { + src: '../../node_modules/@mdi/font/fonts/*', + dest: 'public/build/fonts/', + }, + ], + }), + + replace({ + 'process.env.API_URL': JSON.stringify(process.env.API_URL), + }), + + svelte({ + preprocess: sveltePreprocess({ sourceMap: !production }), + compilerOptions: { + // enable run-time checks when not in production + dev: !production, + }, + }), + // we'll extract any component CSS out into + // a separate file - better for performance + css({ output: 'bundle.css' }), + + // If you have external dependencies installed from + // npm, you'll most likely need these plugins. In + // some cases you'll need additional configuration - + // consult the documentation for details: + // https://github.com/rollup/plugins/tree/master/packages/commonjs + resolve({ + browser: true, + dedupe: ['svelte'], + }), + commonjs(), + typescript({ + sourceMap: !production, + inlineSources: !production, + }), + + // In dev mode, call `npm run start` once + // the bundle has been generated + !production && serve(), + + // Watch the `public` directory and refresh the + // browser on changes when not in production + !production && livereload('public'), + + // If we're building for production (npm run build + // instead of npm run dev), minify + production && terser(), + ], + watch: { + clearScreen: false, + }, +}; diff --git a/packages/web/src/App.css b/packages/web/src/App.css deleted file mode 100644 index 74b5e0534..000000000 --- a/packages/web/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/packages/web/src/App.js b/packages/web/src/App.js deleted file mode 100644 index b325a80ae..000000000 --- a/packages/web/src/App.js +++ /dev/null @@ -1,57 +0,0 @@ -import React from 'react'; -import './index.css'; -import Screen from './Screen'; -import { - CurrentWidgetProvider, - CurrentDatabaseProvider, - OpenedTabsProvider, - OpenedConnectionsProvider, - LeftPanelWidthProvider, - CurrentArchiveProvider, - CurrentThemeProvider, -} from './utility/globalState'; -import { SocketProvider } from './utility/SocketProvider'; -import ConnectionsPinger from './utility/ConnectionsPinger'; -import { ModalLayerProvider } from './modals/showModal'; -import UploadsProvider from './utility/UploadsProvider'; -import ThemeHelmet from './themes/ThemeHelmet'; -import PluginsProvider from './plugins/PluginsProvider'; -import { ExtensionsProvider } from './utility/useExtensions'; -import { MenuLayerProvider } from './modals/showMenu'; - -function App() { - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export default App; diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte new file mode 100644 index 000000000..65fa8f18b --- /dev/null +++ b/packages/web/src/App.svelte @@ -0,0 +1,10 @@ + + + + + diff --git a/packages/web/src/App.test.js b/packages/web/src/App.test.js deleted file mode 100644 index f50f6952c..000000000 --- a/packages/web/src/App.test.js +++ /dev/null @@ -1,11 +0,0 @@ -// @ts-nocheck - -import React from 'react'; -import { render } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - const { getByText } = render(); - const linkElement = getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/packages/web/src/DragAndDropFileTarget.js b/packages/web/src/DragAndDropFileTarget.js deleted file mode 100644 index 5741ca470..000000000 --- a/packages/web/src/DragAndDropFileTarget.js +++ /dev/null @@ -1,62 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { FontIcon } from './icons'; -import useTheme from './theme/useTheme'; -import getElectron from './utility/getElectron'; -import useExtensions from './utility/useExtensions'; - -const TargetStyled = styled.div` - position: fixed; - display: flex; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: ${props => props.theme.main_background_blue[3]}; - align-items: center; - justify-content: space-around; - z-index: 1000; -`; - -const InfoBox = styled.div``; - -const IconWrapper = styled.div` - display: flex; - justify-content: space-around; - font-size: 50px; - margin-bottom: 20px; -`; - -const InfoWrapper = styled.div` - display: flex; - justify-content: space-around; - margin-top: 10px; -`; - -const TitleWrapper = styled.div` - font-size: 30px; - display: flex; - justify-content: space-around; -`; - -export default function DragAndDropFileTarget({ isDragActive, inputProps }) { - const theme = useTheme(); - const { fileFormats } = useExtensions(); - const electron = getElectron(); - const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name); - if (electron) fileTypeNames.push('SQL'); - return ( - !!isDragActive && ( - - - - - - Drop the files to upload to DbGate - Supported file types: {fileTypeNames.join(', ')} - - - - ) - ); -} diff --git a/packages/web/src/DragAndDropFileTarget.svelte b/packages/web/src/DragAndDropFileTarget.svelte new file mode 100644 index 000000000..7b9342280 --- /dev/null +++ b/packages/web/src/DragAndDropFileTarget.svelte @@ -0,0 +1,55 @@ + + +
+
+
+ +
+
Drop the files to upload to DbGate
+
Supported file types: {fileTypeNames.join(', ')}
+
+
+ + diff --git a/packages/web/src/Screen.js b/packages/web/src/Screen.js deleted file mode 100644 index 8299dce9f..000000000 --- a/packages/web/src/Screen.js +++ /dev/null @@ -1,147 +0,0 @@ -// @ts-nocheck - -import React from 'react'; -import dimensions from './theme/dimensions'; -import styled from 'styled-components'; -import TabsPanel from './TabsPanel'; -import TabContent from './TabContent'; -import WidgetIconPanel from './widgets/WidgetIconPanel'; -import { useCurrentWidget, useLeftPanelWidth, useSetLeftPanelWidth } from './utility/globalState'; -import WidgetContainer from './widgets/WidgetContainer'; -import ToolBar from './widgets/Toolbar'; -import StatusBar from './widgets/StatusBar'; -import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter'; -import { ModalLayer } from './modals/showModal'; -import DragAndDropFileTarget from './DragAndDropFileTarget'; -import { useUploadsZone } from './utility/UploadsProvider'; -import useTheme from './theme/useTheme'; -import { MenuLayer } from './modals/showMenu'; -import ErrorBoundary, { ErrorBoundaryTest } from './utility/ErrorBoundary'; - -const BodyDiv = styled.div` - position: fixed; - top: ${dimensions.tabsPanel.height + dimensions.toolBar.height}px; - left: ${props => props.contentLeft}px; - bottom: ${dimensions.statusBar.height}px; - right: 0; - background-color: ${props => props.theme.content_background}; -`; - -const ToolBarDiv = styled.div` - position: fixed; - top: 0; - left: 0; - right: 0; - background-color: ${props => props.theme.toolbar_background}; - height: ${dimensions.toolBar.height}px; -`; - -const IconBar = styled.div` - position: fixed; - top: ${dimensions.toolBar.height}px; - left: 0; - bottom: ${dimensions.statusBar.height}px; - width: ${dimensions.widgetMenu.iconSize}px; - background-color: ${props => props.theme.widget_background}; -`; - -const LeftPanel = styled.div` - position: fixed; - top: ${dimensions.toolBar.height}px; - left: ${dimensions.widgetMenu.iconSize}px; - bottom: ${dimensions.statusBar.height}px; - background-color: ${props => props.theme.left_background}; - display: flex; -`; - -const TabsPanelContainer = styled.div` - display: flex; - position: fixed; - top: ${dimensions.toolBar.height}px; - left: ${props => props.contentLeft}px; - height: ${dimensions.tabsPanel.height}px; - right: 0; - background-color: ${props => props.theme.tabs_background2}; - border-top: 1px solid ${props => props.theme.border}; - - overflow-x: auto; - - ::-webkit-scrollbar { - height: 7px; - } -} -`; - -const StausBarContainer = styled.div` - position: fixed; - height: ${dimensions.statusBar.height}px; - left: 0; - right: 0; - bottom: 0; - background-color: ${props => props.theme.statusbar_background}; -`; - -const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)` - position: absolute; - top: ${dimensions.toolBar.height}px; - bottom: ${dimensions.statusBar.height}px; -`; - -// const StyledRoot = styled.div` -// // color: ${(props) => props.theme.fontColor}; -// `; - -export default function Screen() { - const theme = useTheme(); - const currentWidget = useCurrentWidget(); - const leftPanelWidth = useLeftPanelWidth(); - const setLeftPanelWidth = useSetLeftPanelWidth(); - const contentLeft = currentWidget - ? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness - : dimensions.widgetMenu.iconSize; - const toolbarPortalRef = React.useRef(); - const statusbarPortalRef = React.useRef(); - const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff)); - - const { getRootProps, getInputProps, isDragActive } = useUploadsZone(); - - return ( -
- - - - - - - - {!!currentWidget && ( - - - - - - )} - {!!currentWidget && ( - - )} - - - - - - - - - - - - - - -
- ); -} diff --git a/packages/web/src/Screen.svelte b/packages/web/src/Screen.svelte new file mode 100644 index 000000000..85ab285f7 --- /dev/null +++ b/packages/web/src/Screen.svelte @@ -0,0 +1,137 @@ + + +
+
+ +
+
+ +
+ {#if $selectedWidget} +
+ +
+ {/if} +
+ +
+
+ +
+ {#if $selectedWidget} +
leftPanelWidth.update(x => x + e.detail)} + /> + {/if} + {#if $visibleCommandPalette} +
+ +
+ {/if} + {#if $visibleToolbar} +
+ +
+ {/if} + + + {#if $isFileDragActive} + + {/if} +
+ + diff --git a/packages/web/src/TabContent.js b/packages/web/src/TabContent.js deleted file mode 100644 index f90da055f..000000000 --- a/packages/web/src/TabContent.js +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import styled from 'styled-components'; -import tabs from './tabs'; -import { useOpenedTabs } from './utility/globalState'; -import ErrorBoundary from './utility/ErrorBoundary'; - -const TabContainerStyled = styled.div` - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - display: flex; - visibility: ${props => - // @ts-ignore - props.tabVisible ? 'visible' : 'hidden'}; -`; - -function TabContainer({ TabComponent, ...props }) { - const { tabVisible, tabid, toolbarPortalRef, statusbarPortalRef } = props; - return ( - // @ts-ignore - - - - - - ); -} - -const TabContainerMemo = React.memo(TabContainer); - -function createTabComponent(selectedTab) { - const TabComponent = tabs[selectedTab.tabComponent]; - if (TabComponent) { - return { - TabComponent, - props: selectedTab.props, - }; - } - return null; -} - -export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) { - const files = useOpenedTabs(); - - const [mountedTabs, setMountedTabs] = React.useState({}); - - const selectedTab = files.find(x => x.selected && x.closedTime == null); - - React.useEffect(() => { - // cleanup closed tabs - - if ( - _.difference( - _.keys(mountedTabs), - _.map( - files.filter(x => x.closedTime == null), - 'tabid' - ) - ).length > 0 - ) { - setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k && x.closedTime == null))); - } - - if (selectedTab) { - const { tabid } = selectedTab; - if (tabid && !mountedTabs[tabid]) - setMountedTabs({ - ...mountedTabs, - [tabid]: createTabComponent(selectedTab), - }); - } - }, [mountedTabs, files]); - - return _.keys(mountedTabs).map(tabid => { - const { TabComponent, props } = mountedTabs[tabid]; - const tabVisible = tabid == (selectedTab && selectedTab.tabid); - return ( - - ); - }); -} diff --git a/packages/web/src/TabContent.svelte b/packages/web/src/TabContent.svelte new file mode 100644 index 000000000..bff6f575e --- /dev/null +++ b/packages/web/src/TabContent.svelte @@ -0,0 +1,35 @@ + + +
+ +
+ + diff --git a/packages/web/src/TabRegister.svelte b/packages/web/src/TabRegister.svelte new file mode 100644 index 000000000..d5bf9de5d --- /dev/null +++ b/packages/web/src/TabRegister.svelte @@ -0,0 +1,62 @@ + + + + +{#each _.keys(mountedTabs) as tabid (tabid)} + +{/each} diff --git a/packages/web/src/TabsPanel.js b/packages/web/src/TabsPanel.js deleted file mode 100644 index 37c3e121d..000000000 --- a/packages/web/src/TabsPanel.js +++ /dev/null @@ -1,300 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import styled from 'styled-components'; -import { DropDownMenuItem, DropDownMenuDivider } from './modals/DropDownMenu'; - -import { useOpenedTabs, useSetOpenedTabs, useCurrentDatabase, useSetCurrentDatabase } from './utility/globalState'; -import { getConnectionInfo } from './utility/metadataLoaders'; -import { FontIcon } from './icons'; -import useTheme from './theme/useTheme'; -import usePropsCompare from './utility/usePropsCompare'; -import { useShowMenu } from './modals/showMenu'; -import { setSelectedTabFunc } from './utility/common'; -import getElectron from './utility/getElectron'; - -// const files = [ -// { name: 'app.js' }, -// { name: 'BranchCategory', type: 'table', selected: true }, -// { name: 'ApplicationList' }, -// ]; - -const DbGroupHandler = styled.div` - display: flex; - flex: 1; - align-content: stretch; -`; - -const DbWrapperHandler = styled.div` - display: flex; - flex-direction: column; - align-items: stretch; -`; - -const DbNameWrapper = styled.div` - text-align: center; - font-size: 8pt; - border-bottom: 1px solid ${props => props.theme.border}; - border-right: 1px solid ${props => props.theme.border}; - cursor: pointer; - user-select: none; - padding: 1px; - position: relative; - white-space: nowrap; - - overflow: hidden; - text-overflow: ellipsis; - // height: 15px; - - &:hover { - background-color: ${props => props.theme.tabs_background3}; - } - background-color: ${props => - // @ts-ignore - props.selected ? props.theme.tabs_background1 : 'inherit'}; -`; - -// const DbNameWrapperInner = styled.div` -// position: absolute; -// white-space: nowrap; -// `; - -const FileTabItem = styled.div` - border-right: 1px solid ${props => props.theme.border}; - padding-left: 15px; - padding-right: 15px; - flex-shrink: 1; - flex-grow: 1; - min-width: 10px; - display: flex; - align-items: center; - cursor: pointer; - user-select: none; - &:hover { - color: ${props => props.theme.tabs_font_hover}; - } - background-color: ${props => - // @ts-ignore - props.selected ? props.theme.tabs_background1 : 'inherit'}; -`; - -const FileNameWrapper = styled.span` - margin-left: 5px; -`; - -const CloseButton = styled(FontIcon)` - margin-left: 5px; - color: gray; - &:hover { - color: ${props => props.theme.tabs_font2}; - } -`; - -function TabContextMenu({ close, closeAll, closeOthers, closeWithSameDb, closeWithOtherDb, props }) { - const { database } = props || {}; - const { conid } = props || {}; - return ( - <> - Close - Close all - Close others - {conid && database && ( - Close with same DB - {database} - )} - {conid && database && ( - Close with other DB than {database} - )} - - ); -} - -function getTabDbName(tab) { - if (tab.props && tab.props.conid && tab.props.database) return tab.props.database; - if (tab.props && tab.props.archiveFolder) return tab.props.archiveFolder; - return '(no DB)'; -} - -function getTabDbKey(tab) { - if (tab.props && tab.props.conid && tab.props.database) return `database://${tab.props.database}-${tab.props.conid}`; - if (tab.props && tab.props.archiveFolder) return `archive://${tab.props.archiveFolder}`; - return '_no'; -} - -function getDbIcon(key) { - if (key.startsWith('database://')) return 'icon database'; - if (key.startsWith('archive://')) return 'icon archive'; - return 'icon file'; -} - -function buildTooltip(tab) { - let res = tab.tooltip; - if (tab.props && tab.props.savedFilePath) { - if (res) res += '\n'; - res += tab.props.savedFilePath; - } - return res; -} - -export default function TabsPanel() { - // const formatDbKey = (conid, database) => `${database}-${conid}`; - const theme = useTheme(); - const showMenu = useShowMenu(); - - const tabs = useOpenedTabs(); - const setOpenedTabs = useSetOpenedTabs(); - const currentDb = useCurrentDatabase(); - const setCurrentDb = useSetCurrentDatabase(); - - const { name, connection } = currentDb || {}; - const currentDbKey = name && connection ? `database://${name}-${connection._id}` : '_no'; - - const handleTabClick = (e, tabid) => { - if (e.target.closest('.tabCloseButton')) { - return; - } - setOpenedTabs(files => setSelectedTabFunc(files, tabid)); - }; - const closeTabFunc = closeCondition => tabid => { - setOpenedTabs(files => { - const active = files.find(x => x.tabid == tabid); - if (!active) return files; - - const newFiles = files.map(x => ({ - ...x, - closedTime: x.closedTime || (closeCondition(x, active) ? new Date().getTime() : undefined), - })); - - if (newFiles.find(x => x.selected && x.closedTime == null)) { - return newFiles; - } - - const selectedIndex = _.findLastIndex(newFiles, x => x.closedTime == null); - - return newFiles.map((x, index) => ({ - ...x, - selected: index == selectedIndex, - })); - }); - }; - - const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid); - const closeAll = () => { - const closedTime = new Date().getTime(); - setOpenedTabs(tabs => - tabs.map(tab => ({ - ...tab, - closedTime: tab.closedTime || closedTime, - selected: false, - })) - ); - }; - const closeWithSameDb = closeTabFunc( - (x, active) => - _.get(x, 'props.conid') == _.get(active, 'props.conid') && - _.get(x, 'props.database') == _.get(active, 'props.database') - ); - const closeWithOtherDb = closeTabFunc( - (x, active) => - _.get(x, 'props.conid') != _.get(active, 'props.conid') || - _.get(x, 'props.database') != _.get(active, 'props.database') - ); - const closeOthers = closeTabFunc((x, active) => x.tabid != active.tabid); - const handleMouseUp = (e, tabid) => { - if (e.button == 1) { - e.preventDefault(); - closeTab(tabid); - } - }; - const handleContextMenu = (event, tabid, props) => { - event.preventDefault(); - showMenu( - event.pageX, - event.pageY, - closeTab(tabid)} - closeAll={closeAll} - closeOthers={() => closeOthers(tabid)} - closeWithSameDb={() => closeWithSameDb(tabid)} - closeWithOtherDb={() => closeWithOtherDb(tabid)} - props={props} - /> - ); - }; - - React.useEffect(() => { - const electron = getElectron(); - if (electron) { - const { ipcRenderer } = electron; - const activeTab = tabs.find(x => x.selected); - window['dbgate_activeTabId'] = activeTab ? activeTab.tabid : null; - ipcRenderer.send('update-menu'); - } - }, [tabs]); - - // console.log( - // 't', - // tabs.map(x => x.tooltip) - // ); - const tabsWithDb = tabs - .filter(x => !x.closedTime) - .map(tab => ({ - ...tab, - tabDbName: getTabDbName(tab), - tabDbKey: getTabDbKey(tab), - })); - const tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey'); - const dbKeys = _.keys(tabsByDb).sort(); - - const handleSetDb = async props => { - const { conid, database } = props || {}; - if (conid) { - const connection = await getConnectionInfo({ conid, database }); - if (connection) { - setCurrentDb({ connection, name: database }); - return; - } - } - setCurrentDb(null); - }; - - return ( - <> - {dbKeys.map(dbKey => ( - - handleSetDb(tabsByDb[dbKey][0].props)} - theme={theme} - > - {tabsByDb[dbKey][0].tabDbName} - - - {_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => ( - handleTabClick(e, tab.tabid)} - onMouseUp={e => handleMouseUp(e, tab.tabid)} - onContextMenu={e => handleContextMenu(e, tab.tabid, tab.props)} - > - {} - {tab.title} - { - e.preventDefault(); - closeTab(tab.tabid); - }} - /> - - ))} - - - ))} - - ); -} diff --git a/packages/web/src/appobj/AppObjectCore.js b/packages/web/src/appobj/AppObjectCore.js deleted file mode 100644 index 58efa6b2d..000000000 --- a/packages/web/src/appobj/AppObjectCore.js +++ /dev/null @@ -1,97 +0,0 @@ -// @ts-nocheck - -import _ from 'lodash'; -import React from 'react'; -import styled from 'styled-components'; -import { FontIcon } from '../icons'; -import { useShowMenu } from '../modals/showMenu'; -import useTheme from '../theme/useTheme'; - -const AppObjectDiv = styled.div` - padding: 5px; - ${props => - !props.disableHover && - ` - &:hover { - background-color: ${props.theme.left_background_blue[1]}; - } - `} - cursor: pointer; - white-space: nowrap; - font-weight: ${props => (props.isBold ? 'bold' : 'normal')}; -`; - -const IconWrap = styled.span` - margin-right: 5px; -`; - -const StatusIconWrap = styled.span` - margin-left: 5px; -`; - -const ExtInfoWrap = styled.span` - font-weight: normal; - margin-left: 5px; - color: ${props => props.theme.left_font3}; -`; - -export function AppObjectCore({ - title, - icon, - data, - onClick = undefined, - onClick2 = undefined, - onClick3 = undefined, - isBold = undefined, - isBusy = undefined, - prefix = undefined, - statusIcon = undefined, - extInfo = undefined, - statusTitle = undefined, - disableHover = false, - children = null, - Menu = undefined, - ...other -}) { - const theme = useTheme(); - const showMenu = useShowMenu(); - - const handleContextMenu = event => { - if (!Menu) return; - - event.preventDefault(); - showMenu(event.pageX, event.pageY, ); - }; - - return ( - <> - { - if (onClick) onClick(data); - if (onClick2) onClick2(data); - if (onClick3) onClick3(data); - }} - theme={theme} - isBold={isBold} - draggable - onDragStart={e => { - e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data)); - }} - disableHover={disableHover} - {...other} - > - {prefix} - {isBusy ? : } - {title} - {statusIcon && ( - - - - )} - {extInfo && {extInfo}} - - {children} - - ); -} diff --git a/packages/web/src/appobj/AppObjectCore.svelte b/packages/web/src/appobj/AppObjectCore.svelte new file mode 100644 index 000000000..00ff3aab2 --- /dev/null +++ b/packages/web/src/appobj/AppObjectCore.svelte @@ -0,0 +1,83 @@ + + +
{ + e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data)); + }} +> + {#if expandIcon} + + + + {/if} + {#if isBusy} + + {:else} + + {/if} + {title} + {#if statusIcon} + + + + {/if} + {#if extInfo} + + {extInfo} + + {/if} +
+ + + diff --git a/packages/web/src/appobj/AppObjectGroup.svelte b/packages/web/src/appobj/AppObjectGroup.svelte new file mode 100644 index 000000000..85a49ea70 --- /dev/null +++ b/packages/web/src/appobj/AppObjectGroup.svelte @@ -0,0 +1,48 @@ + + +
(isExpanded = !isExpanded)}> + + + + + {group} + {items && `(${countText})`} +
+ +{#if isExpanded} + {#each filtered as item (module.extractKey(item.data))} + + {/each} +{/if} + + diff --git a/packages/web/src/appobj/AppObjectList.js b/packages/web/src/appobj/AppObjectList.js deleted file mode 100644 index 7f6ac8556..000000000 --- a/packages/web/src/appobj/AppObjectList.js +++ /dev/null @@ -1,164 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import styled from 'styled-components'; -import { ExpandIcon } from '../icons'; -import useTheme from '../theme/useTheme'; - -const SubItemsDiv = styled.div` - margin-left: 28px; -`; - -const ExpandIconHolder2 = styled.span` - margin-right: 3px; - // position: relative; - // top: -3px; -`; - -const ExpandIconHolder = styled.span` - margin-right: 5px; -`; - -const GroupDiv = styled.div` - user-select: none; - padding: 5px; - &:hover { - background-color: ${props => props.theme.left_background_blue[1]}; - } - cursor: pointer; - white-space: nowrap; - font-weight: bold; -`; - -function AppObjectListItem({ - AppObjectComponent, - data, - filter, - onObjectClick, - isExpandable, - SubItems, - getCommonProps, - expandOnClick, - ExpandIconComponent, -}) { - const [isExpanded, setIsExpanded] = React.useState(false); - - const expandable = data && isExpandable && isExpandable(data); - - React.useEffect(() => { - if (!expandable) { - setIsExpanded(false); - } - }, [expandable]); - - let commonProps = { - prefix: SubItems ? ( - - {expandable ? ( - { - setIsExpanded(v => !v); - e.stopPropagation(); - }} - /> - ) : ( - - )} - - ) : null, - }; - - if (SubItems && expandOnClick) { - commonProps.onClick2 = () => setIsExpanded(v => !v); - } - if (onObjectClick) { - commonProps.onClick3 = onObjectClick; - } - - if (getCommonProps) { - commonProps = { ...commonProps, ...getCommonProps(data) }; - } - - let res = ; - if (SubItems && isExpanded) { - res = ( - <> - {res} - - - - - ); - } - return res; -} - -function AppObjectGroup({ group, items }) { - const [isExpanded, setIsExpanded] = React.useState(true); - const theme = useTheme(); - const filtered = items.filter(x => x.component); - let countText = filtered.length.toString(); - if (filtered.length < items.length) countText += `/${items.length}`; - - return ( - <> - setIsExpanded(!isExpanded)} theme={theme}> - - - - {group} {items && `(${countText})`} - - {isExpanded && filtered.map(x => x.component)} - - ); -} - -export function AppObjectList({ - list, - AppObjectComponent, - SubItems = undefined, - onObjectClick = undefined, - filter = undefined, - groupFunc = undefined, - groupOrdered = undefined, - isExpandable = undefined, - getCommonProps = undefined, - expandOnClick = false, - ExpandIconComponent = ExpandIcon, -}) { - const createComponent = data => ( - - ); - - if (groupFunc) { - const listGrouped = _.compact( - (list || []).map(data => { - const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data); - const component = matcher && !matcher(filter) ? null : createComponent(data); - const group = groupFunc(data); - return { group, data, component }; - }) - ); - const groups = _.groupBy(listGrouped, 'group'); - return (groupOrdered || _.keys(groups)).map(group => ( - - )); - } - - return (list || []).map(data => { - const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data); - if (matcher && !matcher(filter)) return null; - return createComponent(data); - }); -} diff --git a/packages/web/src/appobj/AppObjectList.svelte b/packages/web/src/appobj/AppObjectList.svelte new file mode 100644 index 000000000..92e3e74dc --- /dev/null +++ b/packages/web/src/appobj/AppObjectList.svelte @@ -0,0 +1,53 @@ + + +{#if groupFunc} + {#each _.keys(groups) as group (group)} + + {/each} +{:else} + {#each filtered as data (module.extractKey(data))} + + {/each} +{/if} diff --git a/packages/web/src/appobj/AppObjectListItem.svelte b/packages/web/src/appobj/AppObjectListItem.svelte new file mode 100644 index 000000000..9b88412e7 --- /dev/null +++ b/packages/web/src/appobj/AppObjectListItem.svelte @@ -0,0 +1,56 @@ + + + + + + +{#if isExpanded && subItemsComponent} +
+ +
+{/if} + + diff --git a/packages/web/src/appobj/ArchiveFileAppObject.js b/packages/web/src/appobj/ArchiveFileAppObject.js deleted file mode 100644 index 3fed3ba7c..000000000 --- a/packages/web/src/appobj/ArchiveFileAppObject.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import { filterName } from 'dbgate-datalib'; -import axios from '../utility/axios'; -import { AppObjectCore } from './AppObjectCore'; -import useOpenNewTab from '../utility/useOpenNewTab'; - -function openArchive(openNewTab, fileName, folderName) { - openNewTab({ - title: fileName, - icon: 'img archive', - tooltip: `${folderName}\n${fileName}`, - tabComponent: 'ArchiveFileTab', - props: { - archiveFile: fileName, - archiveFolder: folderName, - }, - }); -} - -function Menu({ data }) { - const openNewTab = useOpenNewTab(); - const handleDelete = () => { - axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName }); - // setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid)); - }; - const handleOpenRead = () => { - openArchive(openNewTab, data.fileName, data.folderName); - }; - const handleOpenWrite = async () => { - // const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName }); - - openNewTab({ - title: data.fileName, - icon: 'img archive', - tabComponent: 'FreeTableTab', - props: { - initialArgs: { - functionName: 'archiveReader', - props: { - fileName: data.fileName, - folderName: data.folderName, - }, - }, - archiveFile: data.fileName, - archiveFolder: data.folderName, - }, - }); - }; - - return ( - <> - Open (readonly) - Open in free table editor - Delete - - ); -} - -function ArchiveFileAppObject({ data, commonProps }) { - const { fileName, folderName } = data; - const openNewTab = useOpenNewTab(); - const onClick = () => { - openArchive(openNewTab, fileName, folderName); - }; - - return ( - - ); -} - -ArchiveFileAppObject.extractKey = data => data.fileName; -ArchiveFileAppObject.createMatcher = ({ fileName }) => filter => filterName(filter, fileName); - -export default ArchiveFileAppObject; diff --git a/packages/web/src/appobj/ArchiveFileAppObject.svelte b/packages/web/src/appobj/ArchiveFileAppObject.svelte new file mode 100644 index 000000000..9031f0a24 --- /dev/null +++ b/packages/web/src/appobj/ArchiveFileAppObject.svelte @@ -0,0 +1,74 @@ + + + + + diff --git a/packages/web/src/appobj/ArchiveFolderAppObject.js b/packages/web/src/appobj/ArchiveFolderAppObject.js deleted file mode 100644 index bcfced98e..000000000 --- a/packages/web/src/appobj/ArchiveFolderAppObject.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import axios from '../utility/axios'; -import { filterName } from 'dbgate-datalib'; -import { AppObjectCore } from './AppObjectCore'; -import { useCurrentArchive } from '../utility/globalState'; - -function Menu({ data }) { - const handleDelete = () => { - axios.post('archive/delete-folder', { folder: data.name }); - }; - return <>{data.name != 'default' && Delete}; -} - -function ArchiveFolderAppObject({ data, commonProps }) { - const { name } = data; - const currentArchive = useCurrentArchive(); - - return ( - - ); -} - -ArchiveFolderAppObject.extractKey = data => data.name; -ArchiveFolderAppObject.createMatcher = data => filter => filterName(filter, data.name); - -export default ArchiveFolderAppObject; diff --git a/packages/web/src/appobj/ArchiveFolderAppObject.svelte b/packages/web/src/appobj/ArchiveFolderAppObject.svelte new file mode 100644 index 000000000..2bb03331f --- /dev/null +++ b/packages/web/src/appobj/ArchiveFolderAppObject.svelte @@ -0,0 +1,33 @@ + + + + + ($currentArchive = data.name)} + menu={createMenu} +/> diff --git a/packages/web/src/appobj/ClosedTabAppObject.js b/packages/web/src/appobj/ClosedTabAppObject.js deleted file mode 100644 index 0ef7e11b9..000000000 --- a/packages/web/src/appobj/ClosedTabAppObject.js +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import moment from 'moment'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import { useSetOpenedTabs } from '../utility/globalState'; -import { AppObjectCore } from './AppObjectCore'; -import { setSelectedTabFunc } from '../utility/common'; -import styled from 'styled-components'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; - -const InfoDiv = styled.div` - margin-left: 30px; - color: ${props => props.theme.left_font3}; -`; - -function Menu({ data }) { - const setOpenedTabs = useSetOpenedTabs(); - const handleDelete = () => { - setOpenedTabs(tabs => tabs.filter(x => x.tabid != data.tabid)); - }; - const handleDeleteOlder = () => { - setOpenedTabs(tabs => tabs.filter(x => !x.closedTime || x.closedTime >= data.closedTime)); - }; - return ( - <> - Delete - Delete older - - ); -} - -function ClosedTabAppObject({ data, commonProps }) { - const { tabid, props, selected, icon, title, closedTime, busy } = data; - const setOpenedTabs = useSetOpenedTabs(); - const theme = useTheme(); - - const onClick = () => { - setOpenedTabs(files => - setSelectedTabFunc( - files.map(x => ({ - ...x, - closedTime: x.tabid == tabid ? undefined : x.closedTime, - })), - tabid - ) - ); - }; - - return ( - - {data.props && data.props.database && ( - - {data.props.database} - - )} - {data.contentPreview && {data.contentPreview}} - - ); -} - -ClosedTabAppObject.extractKey = data => data.tabid; - -export default ClosedTabAppObject; diff --git a/packages/web/src/appobj/ClosedTabAppObject.svelte b/packages/web/src/appobj/ClosedTabAppObject.svelte new file mode 100644 index 000000000..e6b8a12d8 --- /dev/null +++ b/packages/web/src/appobj/ClosedTabAppObject.svelte @@ -0,0 +1,70 @@ + + + + + + {#if data.props && data.props.database} +
+ + {data.props.database} +
+ {/if} + {#if data.contentPreview} +
+ {data.contentPreview} +
+ {/if} +
+ + diff --git a/packages/web/src/appobj/ColumnAppObject.svelte b/packages/web/src/appobj/ColumnAppObject.svelte new file mode 100644 index 000000000..8598414e0 --- /dev/null +++ b/packages/web/src/appobj/ColumnAppObject.svelte @@ -0,0 +1,22 @@ + + + + + diff --git a/packages/web/src/appobj/ConnectionAppObject.js b/packages/web/src/appobj/ConnectionAppObject.js deleted file mode 100644 index 4e5c7c3b0..000000000 --- a/packages/web/src/appobj/ConnectionAppObject.js +++ /dev/null @@ -1,119 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import ConnectionModal from '../modals/ConnectionModal'; -import axios from '../utility/axios'; -import { filterName } from 'dbgate-datalib'; -import ConfirmModal from '../modals/ConfirmModal'; -import CreateDatabaseModal from '../modals/CreateDatabaseModal'; -import { useCurrentDatabase, useOpenedConnections, useSetOpenedConnections } from '../utility/globalState'; -import { AppObjectCore } from './AppObjectCore'; -import useShowModal from '../modals/showModal'; -import { useConfig } from '../utility/metadataLoaders'; -import useExtensions from '../utility/useExtensions'; - -function Menu({ data }) { - const openedConnections = useOpenedConnections(); - const setOpenedConnections = useSetOpenedConnections(); - const showModal = useShowModal(); - const config = useConfig(); - - const handleEdit = () => { - showModal(modalState => ); - }; - const handleDelete = () => { - showModal(modalState => ( - axios.post('connections/delete', data)} - /> - )); - }; - const handleCreateDatabase = () => { - showModal(modalState => ); - }; - const handleRefresh = () => { - axios.post('server-connections/refresh', { conid: data._id }); - }; - const handleDisconnect = () => { - setOpenedConnections(list => list.filter(x => x != data._id)); - }; - const handleConnect = () => { - setOpenedConnections(list => _.uniq([...list, data._id])); - }; - return ( - <> - {config.runAsPortal == false && ( - <> - Edit - Delete - - )} - {!openedConnections.includes(data._id) && Connect} - {openedConnections.includes(data._id) && data.status && ( - Refresh - )} - {openedConnections.includes(data._id) && ( - Disconnect - )} - {openedConnections.includes(data._id) && ( - Create database - )} - - ); -} - -function ConnectionAppObject({ data, commonProps }) { - const { _id, server, displayName, engine, status } = data; - const openedConnections = useOpenedConnections(); - const setOpenedConnections = useSetOpenedConnections(); - const currentDatabase = useCurrentDatabase(); - const extensions = useExtensions(); - - const isBold = _.get(currentDatabase, 'connection._id') == _id; - const onClick = () => setOpenedConnections(c => _.uniq([...c, _id])); - - let statusIcon = null; - let statusTitle = null; - - let extInfo = null; - if (extensions.drivers.find(x => x.engine == engine)) { - const match = (engine || '').match(/^([^@]*)@/); - extInfo = match ? match[1] : engine; - } else { - extInfo = engine; - statusIcon = 'img warn'; - statusTitle = `Engine driver ${engine} not found, review installed plugins and change engine in edit connection dialog`; - } - - if (openedConnections.includes(_id)) { - if (!status) statusIcon = 'icon loading'; - else if (status.name == 'pending') statusIcon = 'icon loading'; - else if (status.name == 'ok') statusIcon = 'img ok'; - else statusIcon = 'img error'; - if (status && status.name == 'error') { - statusTitle = status.message; - } - } - - return ( - - ); -} - -ConnectionAppObject.extractKey = data => data._id; -ConnectionAppObject.createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server); - -export default ConnectionAppObject; diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte new file mode 100644 index 000000000..4c8ba006b --- /dev/null +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -0,0 +1,116 @@ + + + + + ($openedConnections = _.uniq([...$openedConnections, data._id]))} + on:click +/> diff --git a/packages/web/src/appobj/DatabaseAppObject.js b/packages/web/src/appobj/DatabaseAppObject.js deleted file mode 100644 index 04cef6bd9..000000000 --- a/packages/web/src/appobj/DatabaseAppObject.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import ImportExportModal from '../modals/ImportExportModal'; -import { getDefaultFileFormat } from '../utility/fileformats'; -import { useCurrentDatabase } from '../utility/globalState'; -import { AppObjectCore } from './AppObjectCore'; -import useShowModal from '../modals/showModal'; -import useExtensions from '../utility/useExtensions'; -import useOpenNewTab from '../utility/useOpenNewTab'; - -function Menu({ data }) { - const { connection, name } = data; - const openNewTab = useOpenNewTab(); - - const extensions = useExtensions(); - const showModal = useShowModal(); - - const tooltip = `${connection.displayName || connection.server}\n${name}`; - - const handleNewQuery = () => { - openNewTab({ - title: 'Query #', - icon: 'img sql-file', - tooltip, - tabComponent: 'QueryTab', - props: { - conid: connection._id, - database: name, - }, - }); - }; - - const handleImport = () => { - showModal(modalState => ( - - )); - }; - - const handleExport = () => { - showModal(modalState => ( - - )); - }; - - return ( - <> - New query - Import - Export - - ); -} - -function DatabaseAppObject({ data, commonProps }) { - const { name, connection } = data; - const currentDatabase = useCurrentDatabase(); - return ( - - ); -} - -DatabaseAppObject.extractKey = props => props.name; - -export default DatabaseAppObject; diff --git a/packages/web/src/appobj/DatabaseAppObject.svelte b/packages/web/src/appobj/DatabaseAppObject.svelte new file mode 100644 index 000000000..136b1dcd7 --- /dev/null +++ b/packages/web/src/appobj/DatabaseAppObject.svelte @@ -0,0 +1,23 @@ + + + + + ($currentDatabase = data)} +/> diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.js b/packages/web/src/appobj/DatabaseObjectAppObject.js deleted file mode 100644 index dbf5bff84..000000000 --- a/packages/web/src/appobj/DatabaseObjectAppObject.js +++ /dev/null @@ -1,325 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu'; -import { getConnectionInfo } from '../utility/metadataLoaders'; -import fullDisplayName from '../utility/fullDisplayName'; -import { filterName } from 'dbgate-datalib'; -import ImportExportModal from '../modals/ImportExportModal'; -import { useSetOpenedTabs } from '../utility/globalState'; -import { AppObjectCore } from './AppObjectCore'; -import useShowModal from '../modals/showModal'; -import { findEngineDriver } from 'dbgate-tools'; -import useExtensions from '../utility/useExtensions'; -import useOpenNewTab from '../utility/useOpenNewTab'; -import uuidv1 from 'uuid/v1'; -import { AppObjectList } from './AppObjectList'; - -const icons = { - tables: 'img table', - views: 'img view', - procedures: 'img procedure', - functions: 'img function', -}; - -const menus = { - tables: [ - { - label: 'Open data', - tab: 'TableDataTab', - forceNewTab: true, - }, - { - label: 'Open form', - tab: 'TableDataTab', - forceNewTab: true, - initialData: { - grid: { - isFormView: true, - }, - }, - }, - { - label: 'Open structure', - tab: 'TableStructureTab', - }, - { - label: 'Query designer', - isQueryDesigner: true, - }, - { - isDivider: true, - }, - { - label: 'Export', - isExport: true, - }, - { - label: 'Open in free table editor', - isOpenFreeTable: true, - }, - { - label: 'Open active chart', - isActiveChart: true, - }, - { - isDivider: true, - }, - { - label: 'SQL: CREATE TABLE', - sqlTemplate: 'CREATE TABLE', - }, - ], - views: [ - { - label: 'Open data', - tab: 'ViewDataTab', - forceNewTab: true, - }, - { - label: 'Open structure', - tab: 'TableStructureTab', - }, - { - label: 'Query designer', - isQueryDesigner: true, - }, - { - isDivider: true, - }, - { - label: 'Export', - isExport: true, - }, - { - label: 'Open in free table editor', - isOpenFreeTable: true, - }, - { - label: 'Open active chart', - isActiveChart: true, - }, - { - isDivider: true, - }, - { - label: 'SQL: CREATE VIEW', - sqlTemplate: 'CREATE OBJECT', - }, - { - label: 'SQL: CREATE TABLE', - sqlTemplate: 'CREATE TABLE', - }, - ], - procedures: [ - { - label: 'SQL: CREATE PROCEDURE', - sqlTemplate: 'CREATE OBJECT', - }, - { - label: 'SQL: EXECUTE', - sqlTemplate: 'EXECUTE PROCEDURE', - }, - ], - functions: [ - { - label: 'SQL: CREATE FUNCTION', - sqlTemplate: 'CREATE OBJECT', - }, - ], -}; - -const defaultTabs = { - tables: 'TableDataTab', - views: 'ViewDataTab', -}; - -export async function openDatabaseObjectDetail( - openNewTab, - tabComponent, - sqlTemplate, - { schemaName, pureName, conid, database, objectTypeField }, - forceNewTab, - initialData -) { - const connection = await getConnectionInfo({ conid }); - const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({ - schemaName, - pureName, - })}`; - - openNewTab( - { - title: sqlTemplate ? 'Query #' : pureName, - tooltip, - icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField], - tabComponent: sqlTemplate ? 'QueryTab' : tabComponent, - props: { - schemaName, - pureName, - conid, - database, - objectTypeField, - initialArgs: sqlTemplate ? { sqlTemplate } : null, - }, - }, - initialData, - { forceNewTab } - ); -} - -function Menu({ data }) { - const showModal = useShowModal(); - const openNewTab = useOpenNewTab(); - const extensions = useExtensions(); - - const getDriver = async () => { - const conn = await getConnectionInfo(data); - if (!conn) return; - const driver = findEngineDriver(conn, extensions); - return driver; - }; - - return ( - <> - {menus[data.objectTypeField].map(menu => - menu.isDivider ? ( - - ) : ( - { - if (menu.isExport) { - showModal(modalState => ( - - )); - } else if (menu.isOpenFreeTable) { - const coninfo = await getConnectionInfo(data); - openNewTab({ - title: data.pureName, - icon: 'img free-table', - tabComponent: 'FreeTableTab', - props: { - initialArgs: { - functionName: 'tableReader', - props: { - connection: { - ...coninfo, - database: data.database, - }, - schemaName: data.schemaName, - pureName: data.pureName, - }, - }, - }, - }); - } else if (menu.isActiveChart) { - const driver = await getDriver(); - const dmp = driver.createDumper(); - dmp.put('^select * from %f', data); - openNewTab( - { - title: data.pureName, - icon: 'img chart', - tabComponent: 'ChartTab', - props: { - conid: data.conid, - database: data.database, - }, - }, - { - editor: { - config: { chartType: 'bar' }, - sql: dmp.s, - }, - } - ); - } else if (menu.isQueryDesigner) { - openNewTab( - { - title: 'Query #', - icon: 'img query-design', - tabComponent: 'QueryDesignTab', - props: { - conid: data.conid, - database: data.database, - }, - }, - { - editor: { - tables: [ - { - ...data, - designerId: uuidv1(), - left: 50, - top: 50, - }, - ], - }, - } - ); - } else { - openDatabaseObjectDetail( - openNewTab, - menu.tab, - menu.sqlTemplate, - data, - menu.forceNewTab, - menu.initialData - ); - } - }} - > - {menu.label} - - ) - )} - - ); -} - -function DatabaseObjectAppObject({ data, commonProps }) { - const { conid, database, pureName, schemaName, objectTypeField } = data; - const openNewTab = useOpenNewTab(); - const onClick = ({ schemaName, pureName }) => { - openDatabaseObjectDetail( - openNewTab, - defaultTabs[objectTypeField], - defaultTabs[objectTypeField] ? null : 'CREATE OBJECT', - { - schemaName, - pureName, - conid, - database, - objectTypeField, - }, - false - ); - }; - - return ( - - ); -} - -DatabaseObjectAppObject.extractKey = ({ schemaName, pureName }) => - schemaName ? `${schemaName}.${pureName}` : pureName; - -DatabaseObjectAppObject.createMatcher = ({ pureName }) => filter => filterName(filter, pureName); - -export default DatabaseObjectAppObject; diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte new file mode 100644 index 000000000..26ebe5573 --- /dev/null +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -0,0 +1,310 @@ + + + + + diff --git a/packages/web/src/appobj/FavoriteFileAppObject.js b/packages/web/src/appobj/FavoriteFileAppObject.js deleted file mode 100644 index d9136a0f5..000000000 --- a/packages/web/src/appobj/FavoriteFileAppObject.js +++ /dev/null @@ -1,104 +0,0 @@ -import React from 'react'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import FavoriteModal from '../modals/FavoriteModal'; -import useShowModal from '../modals/showModal'; -import axios from '../utility/axios'; -import { copyTextToClipboard } from '../utility/clipboard'; -import getElectron from '../utility/getElectron'; -import useOpenNewTab from '../utility/useOpenNewTab'; -import { SavedFileAppObjectBase } from './SavedFileAppObject'; - -export function useOpenFavorite() { - const openNewTab = useOpenNewTab(); - - const openFavorite = React.useCallback( - async favorite => { - const { icon, tabComponent, title, props, tabdata } = favorite; - let tabdataNew = tabdata; - if (props.savedFile) { - const resp = await axios.post('files/load', { - folder: props.savedFolder, - file: props.savedFile, - format: props.savedFormat, - }); - tabdataNew = { - ...tabdata, - editor: resp.data, - }; - } - openNewTab( - { - title, - icon: icon || 'img favorite', - props, - tabComponent, - }, - tabdataNew - ); - }, - [openNewTab] - ); - - return openFavorite; -} - -export function FavoriteFileAppObject({ data, commonProps }) { - const { icon, tabComponent, title, props, tabdata, urlPath } = data; - const openNewTab = useOpenNewTab(); - const showModal = useShowModal(); - const openFavorite = useOpenFavorite(); - const electron = getElectron(); - - const editFavorite = () => { - showModal(modalState => ); - }; - - const editFavoriteJson = async () => { - const resp = await axios.post('files/load', { - folder: 'favorites', - file: data.file, - format: 'text', - }); - - openNewTab( - { - icon: 'icon favorite', - title, - tabComponent: 'FavoriteEditorTab', - props: { - savedFile: data.file, - savedFormat: 'text', - savedFolder: 'favorites', - }, - }, - { editor: JSON.stringify(JSON.parse(resp.data), null, 2) } - ); - }; - - const copyLink = () => { - copyTextToClipboard(`${document.location.origin}#favorite=${urlPath}`); - }; - - return ( - { - openFavorite(data); - }} - menuExt={ - <> - Edit - Edit JSON definition - {!electron && urlPath && Copy link} - - } - /> - ); -} - -FavoriteFileAppObject.extractKey = data => data.file; diff --git a/packages/web/src/appobj/FavoriteFileAppObject.svelte b/packages/web/src/appobj/FavoriteFileAppObject.svelte new file mode 100644 index 000000000..417567187 --- /dev/null +++ b/packages/web/src/appobj/FavoriteFileAppObject.svelte @@ -0,0 +1,100 @@ + + + + + openFavorite(data)} +/> diff --git a/packages/web/src/appobj/MacroAppObject.js b/packages/web/src/appobj/MacroAppObject.js deleted file mode 100644 index 4a59e6c35..000000000 --- a/packages/web/src/appobj/MacroAppObject.js +++ /dev/null @@ -1,15 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import { filterName } from 'dbgate-datalib'; -import { AppObjectCore } from './AppObjectCore'; - -function MacroAppObject({ data, commonProps }) { - const { name, type, title, group } = data; - - return ; -} - -MacroAppObject.extractKey = data => data.name; -MacroAppObject.createMatcher = ({ name, title }) => filter => filterName(filter, name, title); - -export default MacroAppObject; diff --git a/packages/web/src/appobj/MacroAppObject.svelte b/packages/web/src/appobj/MacroAppObject.svelte new file mode 100644 index 000000000..8aebb4bae --- /dev/null +++ b/packages/web/src/appobj/MacroAppObject.svelte @@ -0,0 +1,23 @@ + + + + + ($selectedMacro = data)} +/> diff --git a/packages/web/src/appobj/SavedFileAppObject.js b/packages/web/src/appobj/SavedFileAppObject.js deleted file mode 100644 index 3feb1e9fb..000000000 --- a/packages/web/src/appobj/SavedFileAppObject.js +++ /dev/null @@ -1,314 +0,0 @@ -import React from 'react'; -import axios from '../utility/axios'; -import _ from 'lodash'; -import { DropDownMenuItem } from '../modals/DropDownMenu'; -import { AppObjectCore } from './AppObjectCore'; -import useNewQuery from '../query/useNewQuery'; -import { useCurrentDatabase } from '../utility/globalState'; -import ScriptWriter from '../impexp/ScriptWriter'; -import { extractPackageName } from 'dbgate-tools'; -import useShowModal from '../modals/showModal'; -import InputTextModal from '../modals/InputTextModal'; -import useHasPermission from '../utility/useHasPermission'; -import useOpenNewTab from '../utility/useOpenNewTab'; -import ConfirmModal from '../modals/ConfirmModal'; - -function Menu({ data, menuExt = null, title = undefined, disableRename = false }) { - const hasPermission = useHasPermission(); - const showModal = useShowModal(); - const handleDelete = () => { - showModal(modalState => ( - { - axios.post('files/delete', data); - }} - /> - )); - }; - const handleRename = () => { - showModal(modalState => ( - { - axios.post('files/rename', { ...data, newFile }); - }} - /> - )); - }; - return ( - <> - {hasPermission(`files/${data.folder}/write`) && ( - Delete - )} - {hasPermission(`files/${data.folder}/write`) && !disableRename && ( - Rename - )} - {menuExt} - - ); -} - -export function SavedFileAppObjectBase({ - data, - commonProps, - format, - icon, - onLoad, - title = undefined, - menuExt = null, - disableRename = false, -}) { - const { file, folder } = data; - - const onClick = async () => { - const resp = await axios.post('files/load', { folder, file, format }); - onLoad(resp.data); - }; - - return ( - } - /> - ); -} - -export function SavedSqlFileAppObject({ data, commonProps }) { - const { file, folder } = data; - const newQuery = useNewQuery(); - const currentDatabase = useCurrentDatabase(); - const openNewTab = useOpenNewTab(); - - const connection = _.get(currentDatabase, 'connection'); - const database = _.get(currentDatabase, 'name'); - - const handleGenerateExecute = () => { - const script = new ScriptWriter(); - const conn = { - ..._.omit(connection, ['displayName', '_id']), - database, - }; - script.put(`const sql = await dbgateApi.loadFile('${folder}/${file}');`); - script.put(`await dbgateApi.executeQuery({ sql, connection: ${JSON.stringify(conn)} });`); - // @ts-ignore - script.requirePackage(extractPackageName(conn.engine)); - - openNewTab( - { - title: 'Shell #', - icon: 'img shell', - tabComponent: 'ShellTab', - }, - { editor: script.getScript() } - ); - }; - - return ( - Generate shell execute - ) : null - } - onLoad={data => { - newQuery({ - title: file, - initialData: data, - // @ts-ignore - savedFile: file, - savedFolder: 'sql', - savedFormat: 'text', - }); - }} - /> - ); -} - -export function SavedShellFileAppObject({ data, commonProps }) { - const { file, folder } = data; - const openNewTab = useOpenNewTab(); - - return ( - { - openNewTab( - { - title: file, - icon: 'img shell', - tabComponent: 'ShellTab', - props: { - savedFile: file, - savedFolder: 'shell', - savedFormat: 'text', - }, - }, - { editor: data } - ); - }} - /> - ); -} - -export function SavedChartFileAppObject({ data, commonProps }) { - const { file, folder } = data; - const openNewTab = useOpenNewTab(); - - const currentDatabase = useCurrentDatabase(); - - const connection = _.get(currentDatabase, 'connection') || {}; - const database = _.get(currentDatabase, 'name'); - - const tooltip = `${connection.displayName || connection.server}\n${database}`; - - return ( - { - openNewTab( - { - title: file, - icon: 'img chart', - tooltip, - props: { - conid: connection._id, - database, - savedFile: file, - savedFolder: 'charts', - savedFormat: 'json', - }, - tabComponent: 'ChartTab', - }, - { editor: data } - ); - }} - /> - ); -} - -export function SavedQueryFileAppObject({ data, commonProps }) { - const { file, folder } = data; - const openNewTab = useOpenNewTab(); - - const currentDatabase = useCurrentDatabase(); - - const connection = _.get(currentDatabase, 'connection') || {}; - const database = _.get(currentDatabase, 'name'); - - const tooltip = `${connection.displayName || connection.server}\n${database}`; - - return ( - { - openNewTab( - { - title: file, - icon: 'img query-design', - tooltip, - props: { - conid: connection._id, - database, - savedFile: file, - savedFolder: 'query', - savedFormat: 'json', - }, - tabComponent: 'QueryDesignTab', - }, - { editor: data } - ); - }} - /> - ); -} - -export function SavedMarkdownFileAppObject({ data, commonProps }) { - const { file, folder } = data; - const openNewTab = useOpenNewTab(); - - const showPage = () => { - openNewTab({ - title: file, - icon: 'img markdown', - tabComponent: 'MarkdownViewTab', - props: { - savedFile: file, - savedFolder: 'markdown', - savedFormat: 'text', - }, - }); - }; - return ( - { - openNewTab( - { - title: file, - icon: 'img markdown', - tabComponent: 'MarkdownEditorTab', - props: { - savedFile: file, - savedFolder: 'markdown', - savedFormat: 'text', - }, - }, - { editor: data } - ); - }} - menuExt={Show page} - /> - ); -} - -export function SavedFileAppObject({ data, commonProps }) { - const { folder } = data; - const folderTypes = { - sql: SavedSqlFileAppObject, - shell: SavedShellFileAppObject, - charts: SavedChartFileAppObject, - markdown: SavedMarkdownFileAppObject, - query: SavedQueryFileAppObject, - }; - const AppObject = folderTypes[folder]; - if (AppObject) { - return ; - } - return null; -} - -[ - SavedSqlFileAppObject, - SavedShellFileAppObject, - SavedChartFileAppObject, - SavedMarkdownFileAppObject, - SavedFileAppObject, -].forEach(fn => { - // @ts-ignore - fn.extractKey = data => data.file; -}); diff --git a/packages/web/src/appobj/SavedFileAppObject.svelte b/packages/web/src/appobj/SavedFileAppObject.svelte new file mode 100644 index 000000000..95a71e762 --- /dev/null +++ b/packages/web/src/appobj/SavedFileAppObject.svelte @@ -0,0 +1,154 @@ + + + + + diff --git a/packages/web/src/appobj/SubColumnParamList.js b/packages/web/src/appobj/SubColumnParamList.js deleted file mode 100644 index 792b915d8..000000000 --- a/packages/web/src/appobj/SubColumnParamList.js +++ /dev/null @@ -1,36 +0,0 @@ -import { findForeignKeyForColumn } from 'dbgate-tools'; -import React from 'react'; -import { getColumnIcon } from '../datagrid/ColumnLabel'; -import { AppObjectCore } from './AppObjectCore'; -import { AppObjectList } from './AppObjectList'; - -function ColumnAppObject({ data, commonProps }) { - const { columnName, dataType, foreignKey } = data; - let extInfo = dataType; - if (foreignKey) extInfo += ` -> ${foreignKey.refTableName}`; - return ( - - ); -} -ColumnAppObject.extractKey = ({ columnName }) => columnName; - -export default function SubColumnParamList({ data }) { - const { columns } = data; - - return ( - ({ - ...col, - foreignKey: findForeignKeyForColumn(data, col), - }))} - AppObjectComponent={ColumnAppObject} - /> - ); -} diff --git a/packages/web/src/appobj/SubColumnParamList.svelte b/packages/web/src/appobj/SubColumnParamList.svelte new file mode 100644 index 000000000..bdb86ccbc --- /dev/null +++ b/packages/web/src/appobj/SubColumnParamList.svelte @@ -0,0 +1,16 @@ + + + ({ + ...col, + foreignKey: findForeignKeyForColumn(data, col), + }))} + module={columnAppObject} +/> diff --git a/packages/web/src/appobj/SubDatabaseList.svelte b/packages/web/src/appobj/SubDatabaseList.svelte new file mode 100644 index 000000000..8b557f67d --- /dev/null +++ b/packages/web/src/appobj/SubDatabaseList.svelte @@ -0,0 +1,11 @@ + + + ({ ...db, connection: data }))} module={databaseAppObject} /> diff --git a/packages/web/src/celldata/CellDataView.js b/packages/web/src/celldata/CellDataView.js deleted file mode 100644 index bea7c6ec5..000000000 --- a/packages/web/src/celldata/CellDataView.js +++ /dev/null @@ -1,99 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import { SelectField } from '../utility/inputs'; -import ErrorInfo from '../widgets/ErrorInfo'; -import styled from 'styled-components'; -import { TextCellViewWrap, TextCellViewNoWrap } from './TextCellView'; -import JsonCellView from './JsonCellDataView'; -import useTheme from '../theme/useTheme'; - -const Toolbar = styled.div` - display: flex; - background: ${props => props.theme.toolbar_background}; - align-items: center; -`; - -const MainWrapper = styled.div` - display: flex; - flex: 1; - flex-direction: column; -`; - -const DataWrapper = styled.div` - display: flex; - flex: 1; -`; - -const formats = [ - { - type: 'textWrap', - title: 'Text (wrap)', - Component: TextCellViewWrap, - single: true, - }, - { - type: 'text', - title: 'Text (no wrap)', - Component: TextCellViewNoWrap, - single: true, - }, - { - type: 'json', - title: 'Json', - Component: JsonCellView, - single: true, - }, -]; - -function autodetect(selection, grider, value) { - if (_.isString(value)) { - if (value.startsWith('[') || value.startsWith('{')) return 'json'; - } - return 'textWrap'; -} - -export default function CellDataView({ selection = undefined, grider = undefined, selectedValue = undefined }) { - const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect'); - const theme = useTheme(); - let value = null; - if (grider && selection && selection.length == 1) { - const rowData = grider.getRowData(selection[0].row); - const { column } = selection[0]; - if (rowData) value = rowData[column]; - } - if (selectedValue) { - value = selectedValue; - } - const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]); - const autodetectFormat = formats.find(x => x.type == autodetectFormatType); - - const usedFormatType = selectedFormatType == 'autodetect' ? autodetectFormatType : selectedFormatType; - const usedFormat = formats.find(x => x.type == usedFormatType); - - const { Component } = usedFormat || {}; - - return ( - - - Format: - setSelectedFormatType(e.target.value)}> - - - {formats.map(fmt => ( - - ))} - - - - - {usedFormat == null || (usedFormat.single && value == null) ? ( - - ) : ( - - )} - - - ); -} diff --git a/packages/web/src/celldata/JsonCellDataView.js b/packages/web/src/celldata/JsonCellDataView.js deleted file mode 100644 index 3987fc456..000000000 --- a/packages/web/src/celldata/JsonCellDataView.js +++ /dev/null @@ -1,35 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import ReactJson from 'react-json-view'; -import ErrorInfo from '../widgets/ErrorInfo'; -import useTheme from '../theme/useTheme'; - -const OuterWrapper = styled.div` - flex: 1; - position: relative; -`; - -const InnerWrapper = styled.div` - overflow: scroll; - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; -`; - -export default function JsonCellView({ value }) { - const theme = useTheme(); - try { - const json = React.useMemo(() => JSON.parse(value), [value]); - return ( - - - - - - ); - } catch (err) { - return ; - } -} diff --git a/packages/web/src/celldata/TextCellView.js b/packages/web/src/celldata/TextCellView.js deleted file mode 100644 index dc9bc4882..000000000 --- a/packages/web/src/celldata/TextCellView.js +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; - -const StyledInput = styled.textarea` - flex: 1; -`; - -export function TextCellViewWrap({ value, grider, selection }) { - return ; -} - -export function TextCellViewNoWrap({ value, grider, selection }) { - return ( - grider.setCellValue(selection[0].row, selection[0].column, e.target.value)} - /> - ); -} diff --git a/packages/web/src/charts/ChartCore.svelte b/packages/web/src/charts/ChartCore.svelte new file mode 100644 index 000000000..2f904625a --- /dev/null +++ b/packages/web/src/charts/ChartCore.svelte @@ -0,0 +1,36 @@ + + + diff --git a/packages/web/src/charts/ChartEditor.js b/packages/web/src/charts/ChartEditor.js deleted file mode 100644 index 588f740a0..000000000 --- a/packages/web/src/charts/ChartEditor.js +++ /dev/null @@ -1,154 +0,0 @@ -import React from 'react'; -import Chart from 'react-chartjs-2'; -import _ from 'lodash'; -import styled from 'styled-components'; -import useTheme from '../theme/useTheme'; -import useDimensions from '../utility/useDimensions'; -import { HorizontalSplitter } from '../widgets/Splitter'; -import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar'; -import { FormCheckboxField, FormSelectField, FormTextField } from '../utility/forms'; -import DataChart from './DataChart'; -import { FormProviderCore } from '../utility/FormProvider'; -import { loadChartData, loadChartStructure } from './chartDataLoader'; -import useExtensions from '../utility/useExtensions'; -import { getConnectionInfo } from '../utility/metadataLoaders'; -import { findEngineDriver } from 'dbgate-tools'; -import { FormFieldTemplateTiny } from '../utility/formStyle'; -import { ManagerInnerContainer } from '../datagrid/ManagerStyles'; -import { presetPrimaryColors } from '@ant-design/colors'; -import ErrorInfo from '../widgets/ErrorInfo'; - -const LeftContainer = styled.div` - background-color: ${props => props.theme.manager_background}; - display: flex; - flex: 1; -`; - -export default function ChartEditor({ data, config, setConfig, sql, conid, database }) { - const [managerSize, setManagerSize] = React.useState(0); - const theme = useTheme(); - const extensions = useExtensions(); - const [error, setError] = React.useState(null); - - const [availableColumnNames, setAvailableColumnNames] = React.useState([]); - const [loadedData, setLoadedData] = React.useState(null); - - const getDriver = async () => { - const conn = await getConnectionInfo({ conid }); - if (!conn) return; - const driver = findEngineDriver(conn, extensions); - return driver; - }; - - const handleLoadColumns = async () => { - const driver = await getDriver(); - if (!driver) return; - try { - const columns = await loadChartStructure(driver, conid, database, sql); - setAvailableColumnNames(columns); - } catch (err) { - setError(err.message); - } - }; - - const handleLoadData = async () => { - const driver = await getDriver(); - if (!driver) return; - const loaded = await loadChartData(driver, conid, database, sql, config); - if (!loaded) return; - const { columns, rows } = loaded; - setLoadedData({ - structure: columns, - rows, - }); - }; - - React.useEffect(() => { - if (sql && conid && database) { - handleLoadColumns(); - } - }, [sql, conid, database, extensions]); - - React.useEffect(() => { - if (data) { - setAvailableColumnNames(data ? data.structure.columns.map(x => x.columnName) : []); - } - }, [data]); - - React.useEffect(() => { - if (config.labelColumn && sql && conid && database) { - handleLoadData(); - } - }, [config, sql, conid, database, availableColumnNames]); - - if (error) { - return ( -
- -
- ); - } - - return ( - - - - - - - - - - {/* */} - - - {/* - */} - - - - - - - - - - - - - {availableColumnNames.length > 0 && ( - - - {availableColumnNames.map(col => ( - - ))} - - )} - {availableColumnNames.map(col => ( - - - {config[`dataColumn_${col}`] && ( - - - - {_.keys(presetPrimaryColors).map(color => ( - - ))} - - )} - - ))} - - - - - - - - - ); -} diff --git a/packages/web/src/charts/ChartEditor.svelte b/packages/web/src/charts/ChartEditor.svelte new file mode 100644 index 000000000..5a04c3851 --- /dev/null +++ b/packages/web/src/charts/ChartEditor.svelte @@ -0,0 +1,160 @@ + + + + +
+ + + + + + + + + + + + + {#if availableColumnNames.length > 0} + ({ value: col, label: col }))]} + /> + {/if} + + {#each availableColumnNames as col (col)} + + {#if config[`dataColumn_${col}`]} + ({ value: color, label: _.startCase(color) })), + ]} + /> + {/if} + {/each} + + + +
+ + + + +
+
+ + diff --git a/packages/web/src/charts/ChartToolbar.js b/packages/web/src/charts/ChartToolbar.js deleted file mode 100644 index 62b3a1aaf..000000000 --- a/packages/web/src/charts/ChartToolbar.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import ToolbarButton from '../widgets/ToolbarButton'; - -export default function ChartToolbar({ modelState, dispatchModel }) { - return ( - <> - dispatchModel({ type: 'undo' })} icon="icon undo"> - Undo - - dispatchModel({ type: 'redo' })} icon="icon redo"> - Redo - - - ); -} diff --git a/packages/web/src/charts/DataChart.js b/packages/web/src/charts/DataChart.js deleted file mode 100644 index 6afcf36dc..000000000 --- a/packages/web/src/charts/DataChart.js +++ /dev/null @@ -1,165 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import Chart from 'react-chartjs-2'; -import randomcolor from 'randomcolor'; -import styled from 'styled-components'; -import useDimensions from '../utility/useDimensions'; -import { useForm } from '../utility/FormProvider'; -import useTheme from '../theme/useTheme'; -import moment from 'moment'; - -const ChartWrapper = styled.div` - flex: 1; - overflow: hidden; -`; - -function getTimeAxis(labels) { - const res = []; - for (const label of labels) { - const parsed = moment(label); - if (!parsed.isValid()) return null; - const iso = parsed.toISOString(); - if (iso < '1850-01-01T00:00:00' || iso > '2150-01-01T00:00:00') return null; - res.push(parsed); - } - return res; -} - -function getLabels(labelValues, timeAxis, chartType) { - if (!timeAxis) return labelValues; - if (chartType === 'line') return timeAxis.map(x => x.toDate()); - return timeAxis.map(x => x.format('D. M. YYYY')); -} - -function getOptions(timeAxis, chartType) { - if (timeAxis && chartType === 'line') { - return { - scales: { - xAxes: [ - { - type: 'time', - distribution: 'linear', - - time: { - tooltipFormat: 'D. M. YYYY HH:mm', - displayFormats: { - millisecond: 'HH:mm:ss.SSS', - second: 'HH:mm:ss', - minute: 'HH:mm', - hour: 'D.M hA', - day: 'D. M.', - week: 'D. M. YYYY', - month: 'MM-YYYY', - quarter: '[Q]Q - YYYY', - year: 'YYYY', - }, - }, - }, - ], - }, - }; - } - return {}; -} - -function createChartData(freeData, labelColumn, dataColumns, colorSeed, chartType, dataColumnColors, theme) { - if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return [{}, {}]; - const colors = randomcolor({ - count: _.max([freeData.rows.length, dataColumns.length, 1]), - seed: colorSeed, - }); - let backgroundColor = null; - let borderColor = null; - const labelValues = freeData.rows.map(x => x[labelColumn]); - const timeAxis = getTimeAxis(labelValues); - const labels = getLabels(labelValues, timeAxis, chartType); - const res = { - labels, - datasets: dataColumns.map((dataColumn, columnIndex) => { - if (chartType == 'line' || chartType == 'bar') { - const color = dataColumnColors[dataColumn]; - if (color) { - backgroundColor = theme.main_palettes[color][4] + '80'; - borderColor = theme.main_palettes[color][7]; - } else { - backgroundColor = colors[columnIndex] + '80'; - borderColor = colors[columnIndex]; - } - } else { - backgroundColor = colors; - } - - return { - label: dataColumn, - data: freeData.rows.map(row => row[dataColumn]), - backgroundColor, - borderColor, - borderWidth: 1, - }; - }), - }; - - const options = getOptions(timeAxis, chartType); - return [res, options]; -} - -export function extractDataColumns(values) { - const dataColumns = []; - for (const key in values) { - if (key.startsWith('dataColumn_') && values[key]) { - dataColumns.push(key.substring('dataColumn_'.length)); - } - } - return dataColumns; -} -export function extractDataColumnColors(values, dataColumns) { - const res = {}; - for (const column of dataColumns) { - const color = values[`dataColumnColor_${column}`]; - if (color) res[column] = color; - } - return res; -} - -export default function DataChart({ data }) { - const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions(); - const { values } = useForm(); - const theme = useTheme(); - - const { labelColumn } = values; - const dataColumns = extractDataColumns(values); - const dataColumnColors = extractDataColumnColors(values, dataColumns); - const [chartData, options] = createChartData( - data, - labelColumn, - dataColumns, - values.colorSeed || '5', - values.chartType, - dataColumnColors, - theme - ); - - return ( - - - - ); -} diff --git a/packages/web/src/charts/DataChart.svelte b/packages/web/src/charts/DataChart.svelte new file mode 100644 index 000000000..4032c2856 --- /dev/null +++ b/packages/web/src/charts/DataChart.svelte @@ -0,0 +1,143 @@ + + + + +
+ {#if chartData} + {#key `${$values.chartType}|${clientWidth}|${clientHeight}`} + + {/key} + {/if} +
+ + diff --git a/packages/web/src/charts/chartDataLoader.ts b/packages/web/src/charts/chartDataLoader.ts index 7ee23cadd..7f7bcbc54 100644 --- a/packages/web/src/charts/chartDataLoader.ts +++ b/packages/web/src/charts/chartDataLoader.ts @@ -1,8 +1,7 @@ import { dumpSqlSelect, Select } from 'dbgate-sqltree'; import { EngineDriver } from 'dbgate-types'; -import axios from '../utility/axios'; +import axiosInstance from '../utility/axiosInstance'; import _ from 'lodash'; -import { extractDataColumns } from './DataChart'; export async function loadChartStructure(driver: EngineDriver, conid, database, sql) { const select: Select = { @@ -17,7 +16,7 @@ export async function loadChartStructure(driver: EngineDriver, conid, database, const dmp = driver.createDumper(); dumpSqlSelect(dmp, select); - const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s }); + const resp = await axiosInstance.post('database-connections/query-data', { conid, database, sql: dmp.s }); if (resp.data.errorMessage) throw new Error(resp.data.errorMessage); return resp.data.columns.map(x => x.columnName); } @@ -75,7 +74,7 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql, const dmp = driver.createDumper(); dumpSqlSelect(dmp, select); - const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s }); + const resp = await axiosInstance.post('database-connections/query-data', { conid, database, sql: dmp.s }); let { rows, columns } = resp.data; if (truncateFrom == 'end' && rows) { rows = _.reverse([...rows]); @@ -103,3 +102,21 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql, rows, }; } + +export function extractDataColumns(values) { + const dataColumns = []; + for (const key in values) { + if (key.startsWith('dataColumn_') && values[key]) { + dataColumns.push(key.substring('dataColumn_'.length)); + } + } + return dataColumns; +} +export function extractDataColumnColors(values, dataColumns) { + const res = {}; + for (const column of dataColumns) { + const color = values[`dataColumnColor_${column}`]; + if (color) res[column] = color; + } + return res; +} diff --git a/packages/web/src/commands/CommandListener.svelte b/packages/web/src/commands/CommandListener.svelte new file mode 100644 index 000000000..2e92772f1 --- /dev/null +++ b/packages/web/src/commands/CommandListener.svelte @@ -0,0 +1,53 @@ + + + diff --git a/packages/web/src/commands/CommandPalette.svelte b/packages/web/src/commands/CommandPalette.svelte new file mode 100644 index 000000000..72bbcb42e --- /dev/null +++ b/packages/web/src/commands/CommandPalette.svelte @@ -0,0 +1,112 @@ + + + + +
($visibleCommandPalette = false)}> + + {#each filteredItems as command, index} +
handleCommand(command)}> +
{command.text}
+ {#if command.keyText} +
{command.keyText}
+ {/if} +
+ {/each} +
+ + diff --git a/packages/web/src/commands/invalidateCommands.ts b/packages/web/src/commands/invalidateCommands.ts new file mode 100644 index 000000000..f6fcd6301 --- /dev/null +++ b/packages/web/src/commands/invalidateCommands.ts @@ -0,0 +1,67 @@ +import { tick } from 'svelte'; +import { commands } from '../stores'; +import { GlobalCommand } from './registerCommand'; + +let isInvalidated = false; + +export default async function invalidateCommands() { + if (isInvalidated) return; + isInvalidated = true; + await tick(); + + isInvalidated = false; + + commands.update(dct => { + let res = null; + for (const command of Object.values(dct) as GlobalCommand[]) { + if (command.isGroupCommand) continue; + const { testEnabled } = command; + let enabled = command.enabled; + if (testEnabled) enabled = testEnabled(); + if (enabled != command.enabled) { + if (!res) res = { ...dct }; + res[command.id].enabled = enabled; + } + } + if (res) { + const values = Object.values(res) as GlobalCommand[]; + // test enabled for group commands + for (const command of values) { + if (!command.isGroupCommand) continue; + const groupSources = values.filter(x => x.group == command.group && !x.isGroupCommand && x.enabled); + command.enabled = groupSources.length > 0; + // for (const source of groupSources) { + // source.keyTextFromGroup = command.keyText; + // } + } + } + return res || dct; + }); +} + +let isInvalidatedDefinitions = false; + +export async function invalidateCommandDefinitions() { + if (isInvalidatedDefinitions) return; + isInvalidatedDefinitions = true; + await tick(); + + isInvalidatedDefinitions = false; + + commands.update(dct => { + let res = { ...dct }; + const values = Object.values(res) as GlobalCommand[]; + // test enabled for group commands + for (const command of values) { + if (!command.isGroupCommand) continue; + const groupSources = values.filter(x => x.group == command.group && !x.isGroupCommand); + + for (const source of groupSources) { + source.keyTextFromGroup = command.keyText; + } + } + return res; + }); + + invalidateCommands(); +} diff --git a/packages/web/src/commands/registerCommand.ts b/packages/web/src/commands/registerCommand.ts new file mode 100644 index 000000000..9425ab0fc --- /dev/null +++ b/packages/web/src/commands/registerCommand.ts @@ -0,0 +1,43 @@ +import { commands } from '../stores'; +import { invalidateCommandDefinitions } from './invalidateCommands'; + +export interface SubCommand { + text: string; + onClick: Function; +} + +export interface GlobalCommand { + id: string; + category: string; // null for group commands + isGroupCommand?: boolean; + name: string; + text?: string /* category: name */; + keyText?: string; + keyTextFromGroup?: string; // automatically filled from group + group?: string; + getSubCommands?: () => SubCommand[]; + onClick?: Function; + testEnabled?: () => boolean; + // enabledStore?: any; + icon?: string; + toolbar?: boolean; + enabled?: boolean; + showDisabled?: boolean; + toolbarName?: string; + menuName?: string; + toolbarOrder?: number; + disableHandleKeyText?: string; +} + +export default function registerCommand(command: GlobalCommand) { + const { testEnabled } = command; + commands.update(x => ({ + ...x, + [command.id]: { + text: `${command.category}: ${command.name}`, + ...command, + enabled: !testEnabled, + }, + })); + invalidateCommandDefinitions(); +} diff --git a/packages/web/src/commands/runCommand.ts b/packages/web/src/commands/runCommand.ts new file mode 100644 index 000000000..5212f12ef --- /dev/null +++ b/packages/web/src/commands/runCommand.ts @@ -0,0 +1,26 @@ +import { getCommands } from '../stores'; +import { GlobalCommand } from './registerCommand'; + +export default function runCommand(id) { + const commandsValue = getCommands(); + const command = commandsValue[id]; + if (command) { + if (!command.enabled) return; + if (command.isGroupCommand) { + runGroupCommand(command.group); + } else { + if (command.onClick) { + command.onClick(); + } + } + } +} + +window['dbgate_runCommand'] = runCommand; + +export function runGroupCommand(group) { + const commandsValue = getCommands(); + const values = Object.values(commandsValue) as GlobalCommand[]; + const real = values.find(x => x.group == group && !x.isGroupCommand && x.enabled); + if (real && real.onClick) real.onClick(); +} diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts new file mode 100644 index 000000000..aa39261fb --- /dev/null +++ b/packages/web/src/commands/stdCommands.ts @@ -0,0 +1,291 @@ +import { currentTheme, extensions, getVisibleToolbar, visibleToolbar } from '../stores'; +import registerCommand from './registerCommand'; +import { derived, get } from 'svelte/store'; +import { ThemeDefinition } from 'dbgate-types'; +import ConnectionModal from '../modals/ConnectionModal.svelte'; +import AboutModal from '../modals/AboutModal.svelte'; +import ImportExportModal from '../modals/ImportExportModal.svelte'; +import { showModal } from '../modals/modalTools'; +import newQuery from '../query/newQuery'; +import saveTabFile from '../utility/saveTabFile'; +import openNewTab from '../utility/openNewTab'; +import getElectron from '../utility/getElectron'; +import { openElectronFile } from '../utility/openElectronFile'; +import { getDefaultFileFormat } from '../plugins/fileformats'; + +const electron = getElectron(); + +function themeCommand(theme: ThemeDefinition) { + return { + text: theme.themeName, + onClick: () => currentTheme.set(theme.className), + // onPreview: () => { + // const old = get(currentTheme); + // currentTheme.set(css); + // return ok => { + // if (!ok) currentTheme.set(old); + // }; + // }, + }; +} + +registerCommand({ + id: 'theme.changeTheme', + category: 'Theme', + name: 'Change', + getSubCommands: () => get(extensions).themes.map(themeCommand), +}); + +registerCommand({ + id: 'toolbar.show', + category: 'Toolbar', + name: 'Show', + onClick: () => visibleToolbar.set(1), + testEnabled: () => !getVisibleToolbar(), +}); + +registerCommand({ + id: 'toolbar.hide', + category: 'Toolbar', + name: 'Hide', + onClick: () => visibleToolbar.set(0), + testEnabled: () => getVisibleToolbar(), +}); + +registerCommand({ + id: 'about.show', + category: 'About', + name: 'Show', + toolbarName: 'About', + onClick: () => showModal(AboutModal), +}); + +registerCommand({ + id: 'new.connection', + toolbar: true, + icon: 'icon connection', + toolbarName: 'Add connection', + category: 'New', + toolbarOrder: 1, + name: 'Connection', + onClick: () => showModal(ConnectionModal), +}); + +registerCommand({ + id: 'new.query', + category: 'New', + icon: 'icon file', + toolbar: true, + toolbarOrder: 2, + name: 'Query', + keyText: 'Ctrl+Q', + onClick: () => newQuery(), +}); + +registerCommand({ + id: 'new.shell', + category: 'New', + icon: 'img shell', + name: 'JavaScript Shell', + onClick: () => { + openNewTab({ + title: 'Shell #', + icon: 'img shell', + tabComponent: 'ShellTab', + }); + }, +}); + +registerCommand({ + id: 'new.markdown', + category: 'New', + icon: 'img markdown', + name: 'Markdown page', + onClick: () => { + openNewTab({ + title: 'Page #', + icon: 'img markdown', + tabComponent: 'MarkdownEditorTab', + }); + }, +}); + +registerCommand({ + id: 'new.freetable', + category: 'New', + icon: 'img markdown', + name: 'Free table editor', + onClick: () => { + openNewTab({ + title: 'Data #', + icon: 'img free-table', + tabComponent: 'FreeTableTab', + }); + }, +}); + +registerCommand({ + id: 'group.save', + category: null, + isGroupCommand: true, + name: 'Save', + keyText: 'Ctrl+S', + group: 'save', +}); + +registerCommand({ + id: 'group.saveAs', + category: null, + isGroupCommand: true, + name: 'Save As', + keyText: 'Ctrl+Shift+S', + group: 'saveAs', +}); + +registerCommand({ + id: 'group.undo', + category: null, + isGroupCommand: true, + name: 'Undo', + keyText: 'Ctrl+Z', + group: 'undo', +}); + +registerCommand({ + id: 'group.redo', + category: null, + isGroupCommand: true, + name: 'Redo', + keyText: 'Ctrl+Y', + group: 'redo', +}); + +if (electron) { + registerCommand({ + id: 'file.open', + category: 'File', + name: 'Open', + keyText: 'Ctrl+O', + onClick: openElectronFile, + }); +} + +registerCommand({ + id: 'file.import', + category: 'File', + name: 'Import data', + toolbar: true, + icon: 'icon import', + onClick: () => + showModal(ImportExportModal, { + importToArchive: true, + initialValues: { sourceStorageType: getDefaultFileFormat(get(extensions)).storageType }, + }), +}); + +export function registerFileCommands({ + idPrefix, + category, + getCurrentEditor, + folder, + format, + fileExtension, + save = true, + execute = false, + toggleComment = false, + findReplace = false, + undoRedo = false, +}) { + if (save) { + registerCommand({ + id: idPrefix + '.save', + group: 'save', + category, + name: 'Save', + // keyText: 'Ctrl+S', + icon: 'icon save', + toolbar: true, + testEnabled: () => getCurrentEditor() != null, + onClick: () => saveTabFile(getCurrentEditor(), false, folder, format, fileExtension), + }); + registerCommand({ + id: idPrefix + '.saveAs', + group: 'saveAs', + category, + name: 'Save As', + testEnabled: () => getCurrentEditor() != null, + onClick: () => saveTabFile(getCurrentEditor(), true, folder, format, fileExtension), + }); + } + + if (execute) { + registerCommand({ + id: idPrefix + '.execute', + category, + name: 'Execute', + icon: 'icon run', + toolbar: true, + keyText: 'F5 | Ctrl+Enter', + testEnabled: () => getCurrentEditor() != null && !getCurrentEditor()?.isBusy(), + onClick: () => getCurrentEditor().execute(), + }); + registerCommand({ + id: idPrefix + '.kill', + category, + name: 'Kill', + icon: 'icon close', + toolbar: true, + testEnabled: () => getCurrentEditor() != null && getCurrentEditor()?.canKill(), + onClick: () => getCurrentEditor().kill(), + }); + } + + if (toggleComment) { + registerCommand({ + id: idPrefix + '.toggleComment', + category, + name: 'Toggle comment', + keyText: 'Ctrl+/', + disableHandleKeyText: 'Ctrl+/', + testEnabled: () => getCurrentEditor() != null, + onClick: () => getCurrentEditor().toggleComment(), + }); + } + + if (findReplace) { + registerCommand({ + id: idPrefix + '.find', + category, + name: 'Find', + keyText: 'Ctrl+F', + testEnabled: () => getCurrentEditor() != null, + onClick: () => getCurrentEditor().find(), + }); + registerCommand({ + id: idPrefix + '.replace', + category, + keyText: 'Ctrl+H', + name: 'Replace', + testEnabled: () => getCurrentEditor() != null, + onClick: () => getCurrentEditor().replace(), + }); + } + if (undoRedo) { + registerCommand({ + id: idPrefix + '.undo', + category, + name: 'Undo', + group: 'undo', + testEnabled: () => getCurrentEditor()?.canUndo(), + onClick: () => getCurrentEditor().undo(), + }); + registerCommand({ + id: idPrefix + '.redo', + category, + group: 'redo', + name: 'Redo', + testEnabled: () => getCurrentEditor()?.canRedo(), + onClick: () => getCurrentEditor().redo(), + }); + } +} diff --git a/packages/web/src/datagrid/ChangeSetGrider.ts b/packages/web/src/datagrid/ChangeSetGrider.ts index 6aac275b6..ba642eb4f 100644 --- a/packages/web/src/datagrid/ChangeSetGrider.ts +++ b/packages/web/src/datagrid/ChangeSetGrider.ts @@ -143,10 +143,10 @@ export default class ChangeSetGrider extends Grider { this.dispatchChangeSet({ type: 'redo' }); } get canUndo() { - return this.changeSetState.canUndo; + return this.changeSetState?.canUndo; } get canRedo() { - return this.changeSetState.canRedo; + return this.changeSetState?.canRedo; } get containsChanges() { return changeSetContainsChanges(this.changeSet); @@ -155,10 +155,10 @@ export default class ChangeSetGrider extends Grider { return this.insertedRows.length > 0; } - static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider { - return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display); - } - static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) { - return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display]; - } + // static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider { + // return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display); + // } + // static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) { + // return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display]; + // } } diff --git a/packages/web/src/datagrid/ColumnHeaderControl.js b/packages/web/src/datagrid/ColumnHeaderControl.js deleted file mode 100644 index f0f121409..000000000 --- a/packages/web/src/datagrid/ColumnHeaderControl.js +++ /dev/null @@ -1,134 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import ColumnLabel from './ColumnLabel'; -import DropDownButton from '../widgets/DropDownButton'; -import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu'; -import { useSplitterDrag } from '../widgets/Splitter'; -import { isTypeDateTime } from 'dbgate-tools'; -import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject'; -import { useSetOpenedTabs } from '../utility/globalState'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; -import useOpenNewTab from '../utility/useOpenNewTab'; - -const HeaderDiv = styled.div` - display: flex; - flex-wrap: nowrap; -`; - -const LabelDiv = styled.div` - flex: 1; - min-width: 10px; - // padding-left: 2px; - padding: 2px; - margin: auto; - white-space: nowrap; -`; - -const IconWrapper = styled.span` - margin-left: 3px; -`; - -const ResizeHandle = styled.div` - background-color: ${props => props.theme.border}; - width: 2px; - cursor: col-resize; - z-index: 1; -`; - -const GroupingLabel = styled.span` - color: green; - white-space: nowrap; -`; - -export default function ColumnHeaderControl({ - column, - setSort, - onResize, - order, - setGrouping, - grouping, - conid, - database, -}) { - const onResizeDown = useSplitterDrag('clientX', onResize); - const { foreignKey } = column; - const openNewTab = useOpenNewTab(); - const theme = useTheme(); - - const openReferencedTable = () => { - openDatabaseObjectDetail(openNewTab, 'TableDataTab', null, { - schemaName: foreignKey.refSchemaName, - pureName: foreignKey.refTableName, - conid, - database, - objectTypeField: 'tables', - }); - // openNewTab(setOpenedTabs, { - // title: foreignKey.refTableName, - // tooltip, - // icon: sqlTemplate ? 'sql.svg' : icons[objectTypeField], - // tabComponent: sqlTemplate ? 'QueryTab' : tabComponent, - // props: { - // schemaName, - // pureName, - // conid, - // database, - // objectTypeField, - // initialArgs: sqlTemplate ? { sqlTemplate } : null, - // }, - // }); - }; - return ( - - - {grouping && ( - {grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}: - )} - - - {order == 'ASC' && ( - - - - )} - {order == 'DESC' && ( - - - - )} - - {setSort && ( - - setSort('ASC')}>Sort ascending - setSort('DESC')}>Sort descending - - {foreignKey && ( - - Open table {foreignKey.refTableName} - - )} - {foreignKey && } - setGrouping('GROUP')}>Group by - setGrouping('MAX')}>MAX - setGrouping('MIN')}>MIN - setGrouping('SUM')}>SUM - setGrouping('AVG')}>AVG - setGrouping('COUNT')}>COUNT - setGrouping('COUNT DISTINCT')}>COUNT DISTINCT - {isTypeDateTime(column.dataType) && ( - <> - - setGrouping('GROUP:YEAR')}>Group by YEAR - setGrouping('GROUP:MONTH')}>Group by MONTH - setGrouping('GROUP:DAY')}>Group by DAY - {/* setGrouping('GROUP:HOUR')}>Group by HOUR - setGrouping('GROUP:MINUTE')}>Group by MINUTE */} - - )} - - )} - - - ); -} diff --git a/packages/web/src/datagrid/ColumnHeaderControl.svelte b/packages/web/src/datagrid/ColumnHeaderControl.svelte new file mode 100644 index 000000000..e6f0a2a1d --- /dev/null +++ b/packages/web/src/datagrid/ColumnHeaderControl.svelte @@ -0,0 +1,104 @@ + + +
+
+ {#if grouping} + + {grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()} + + {/if} + +
+ {#if order == 'ASC'} + + + + {/if} + {#if order == 'DESC'} + + + + {/if} + +
+
+ + diff --git a/packages/web/src/datagrid/ColumnLabel.js b/packages/web/src/datagrid/ColumnLabel.js deleted file mode 100644 index 89c6eb953..000000000 --- a/packages/web/src/datagrid/ColumnLabel.js +++ /dev/null @@ -1,35 +0,0 @@ -//@ts-nocheck - -import React from 'react'; -import styled from 'styled-components'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; - -const Label = styled.span` - font-weight: ${props => (props.notNull ? 'bold' : 'normal')}; - white-space: nowrap; -`; -const ExtInfoWrap = styled.span` - font-weight: normal; - margin-left: 5px; - color: ${props => props.theme.left_font3}; -`; - -export function getColumnIcon(column, forceIcon = false) { - if (column.autoIncrement) return 'img autoincrement'; - if (column.foreignKey) return 'img foreign-key'; - if (forceIcon) return 'img column'; - return null; -} - -/** @param column {import('dbgate-datalib').DisplayColumn|import('dbgate-types').ColumnInfo} */ -export default function ColumnLabel(column) { - const icon = getColumnIcon(column, column.forceIcon); - const theme = useTheme(); - return ( - - ); -} diff --git a/packages/web/src/datagrid/ColumnManager.js b/packages/web/src/datagrid/ColumnManager.js deleted file mode 100644 index 1ae61b112..000000000 --- a/packages/web/src/datagrid/ColumnManager.js +++ /dev/null @@ -1,94 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import ColumnLabel from './ColumnLabel'; -import { filterName } from 'dbgate-datalib'; -import { ExpandIcon } from '../icons'; -import InlineButton from '../widgets/InlineButton'; -import { ManagerInnerContainer } from './ManagerStyles'; -import SearchInput from '../widgets/SearchInput'; -import useTheme from '../theme/useTheme'; - -const Wrapper = styled.div``; - -const Row = styled.div` - margin-left: 5px; - margin-right: 5px; - cursor: pointer; - white-space: nowrap; - &:hover { - background-color: ${props => props.theme.manager_background_blue[1]}; - } -`; - -const SearchBoxWrapper = styled.div` - display: flex; - margin-bottom: 5px; -`; - -const Button = styled.button` - // -webkit-appearance: none; - // -moz-appearance: none; - // appearance: none; - // width: 50px; -`; - -/** - * @param {object} props - * @param {import('dbgate-datalib').GridDisplay} props.display - * @param {import('dbgate-datalib').DisplayColumn} props.column - */ -function ColumnManagerRow(props) { - const { display, column } = props; - const [isHover, setIsHover] = React.useState(false); - const theme = useTheme(); - return ( - setIsHover(true)} - onMouseLeave={() => setIsHover(false)} - theme={theme} - onClick={e => { - // @ts-ignore - if (e.target.closest('.expandColumnIcon')) return; - display.focusColumn(column.uniqueName); - }} - > - display.toggleExpandedColumn(column.uniqueName)} - /> - display.setColumnVisibility(column.uniquePath, !column.isChecked)} - > - - - ); -} - -/** @param props {import('./types').DataGridProps} */ -export default function ColumnManager(props) { - const { display } = props; - const [columnFilter, setColumnFilter] = React.useState(''); - - return ( - <> - - - display.hideAllColumns()}>Hide - display.showAllColumns()}>Show - - - {display - .getColumns(columnFilter) - .filter(column => filterName(columnFilter, column.columnName)) - .map(column => ( - - ))} - - - ); -} diff --git a/packages/web/src/datagrid/ColumnManager.svelte b/packages/web/src/datagrid/ColumnManager.svelte new file mode 100644 index 000000000..745491dcf --- /dev/null +++ b/packages/web/src/datagrid/ColumnManager.svelte @@ -0,0 +1,28 @@ + + + + + display.hideAllColumns()}>Hide + display.showAllColumns()}>Show + + + {#each display + .getColumns(filter) + .filter(column => filterName(filter, column.columnName)) as column (column.uniqueName)} + + {/each} + diff --git a/packages/web/src/datagrid/ColumnManagerRow.svelte b/packages/web/src/datagrid/ColumnManagerRow.svelte new file mode 100644 index 000000000..46b4f1586 --- /dev/null +++ b/packages/web/src/datagrid/ColumnManagerRow.svelte @@ -0,0 +1,44 @@ + + +
{ + // @ts-ignore + if (e.target.closest('.expandColumnIcon')) return; + display.focusColumn(column.uniqueName); + }} +> + + display.toggleExpandedColumn(column.uniqueName)} + /> + + display.setColumnVisibility(column.uniquePath, !column.isChecked)} + /> + +
+ + diff --git a/packages/web/src/datagrid/DataFilterControl.js b/packages/web/src/datagrid/DataFilterControl.js deleted file mode 100644 index 8a09d4191..000000000 --- a/packages/web/src/datagrid/DataFilterControl.js +++ /dev/null @@ -1,501 +0,0 @@ -// @ts-nocheck -import React from 'react'; -import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu'; -import styled from 'styled-components'; -import keycodes from '../utility/keycodes'; -import { parseFilter, createMultiLineFilter } from 'dbgate-filterparser'; -import InlineButton from '../widgets/InlineButton'; -import useShowModal from '../modals/showModal'; -import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal'; -import SetFilterModal from '../modals/SetFilterModal'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; -import { useShowMenu } from '../modals/showMenu'; -// import { $ } from '../../Utility/jquery'; -// import autobind from 'autobind-decorator'; -// import * as React from 'react'; - -// import { createMultiLineFilter } from '../../DataLib/FilterTools'; -// import { ModalDialog } from '../Dialogs'; -// import { FilterDialog } from '../Dialogs/FilterDialog'; -// import { FilterMultipleValuesDialog } from '../Dialogs/FilterMultipleValuesDialog'; -// import { IconSpan } from '../Navigation/NavUtils'; -// import { KeyCodes } from '../ReactDataGrid/KeyCodes'; -// import { DropDownMenu, DropDownMenuDivider, DropDownMenuItem, DropDownSubmenuItem } from './DropDownMenu'; -// import { FilterParserType } from '../../SwaggerClients'; -// import { IFilterHolder } from '../CommonControls'; -// import { GrayFilterIcon } from '../Icons'; - -// export interface IDataFilterControlProps { -// filterType: FilterParserType; -// getFilter: Function; -// setFilter: Function; -// width: number; -// onControlKey?: Function; -// isReadOnly?: boolean; -// inputElementId?: string; -// } - -const FilterDiv = styled.div` - display: flex; -`; -const FilterInput = styled.input` - flex: 1; - min-width: 10px; - background-color: ${props => - props.state == 'ok' - ? props.theme.input_background_green[1] - : props.state == 'error' - ? props.theme.input_background_red[1] - : props.theme.input_background}; -`; -// const FilterButton = styled.button` -// color: gray; -// `; - -function DropDownContent({ filterType, setFilter, filterMultipleValues, openFilterWindow }) { - switch (filterType) { - case 'number': - return ( - <> - setFilter('')}>Clear Filter - filterMultipleValues()}>Filter multiple values - openFilterWindow('=')}>Equals... - openFilterWindow('<>')}>Does Not Equal... - setFilter('NULL')}>Is Null - setFilter('NOT NULL')}>Is Not Null - openFilterWindow('>')}>Greater Than... - openFilterWindow('>=')}>Greater Than Or Equal To... - openFilterWindow('<')}>Less Than... - openFilterWindow('<=')}>Less Than Or Equal To... - - ); - case 'logical': - return ( - <> - setFilter('')}>Clear Filter - filterMultipleValues()}>Filter multiple values - setFilter('NULL')}>Is Null - setFilter('NOT NULL')}>Is Not Null - setFilter('TRUE')}>Is True - setFilter('FALSE')}>Is False - setFilter('TRUE, NULL')}>Is True or NULL - setFilter('FALSE, NULL')}>Is False or NULL - - ); - case 'datetime': - return ( - <> - setFilter('')}>Clear Filter - filterMultipleValues()}>Filter multiple values - setFilter('NULL')}>Is Null - setFilter('NOT NULL')}>Is Not Null - - - - openFilterWindow('<=')}>Before... - openFilterWindow('>=')}>After... - openFilterWindow('>=;<=')}>Between... - - - - setFilter('TOMORROW')}>Tomorrow - setFilter('TODAY')}>Today - setFilter('YESTERDAY')}>Yesterday - - - - setFilter('NEXT WEEK')}>Next Week - setFilter('THIS WEEK')}>This Week - setFilter('LAST WEEK')}>Last Week - - - - setFilter('NEXT MONTH')}>Next Month - setFilter('THIS MONTH')}>This Month - setFilter('LAST MONTH')}>Last Month - - - - setFilter('NEXT YEAR')}>Next Year - setFilter('THIS YEAR')}>This Year - setFilter('LAST YEAR')}>Last Year - - - - {/* - setFilter('JAN')}>January - setFilter('FEB')}>February - setFilter('MAR')}>March - setFilter('APR')}>April - setFilter('JUN')}>June - setFilter('JUL')}>July - setFilter('AUG')}>August - setFilter('SEP')}>September - setFilter('OCT')}>October - setFilter('NOV')}>November - setFilter('DEC')}>December - - - - setFilter('MON')}>Monday - setFilter('TUE')}>Tuesday - setFilter('WED')}>Wednesday - setFilter('THU')}>Thursday - setFilter('FRI')}>Friday - setFilter('SAT')}>Saturday - setFilter('SUN')}>Sunday - */} - - ); - case 'string': - return ( - <> - setFilter('')}>Clear Filter - filterMultipleValues()}>Filter multiple values - - openFilterWindow('=')}>Equals... - openFilterWindow('<>')}>Does Not Equal... - setFilter('NULL')}>Is Null - setFilter('NOT NULL')}>Is Not Null - setFilter('EMPTY, NULL')}>Is Empty Or Null - setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value - - - - openFilterWindow('+')}>Contains... - openFilterWindow('~')}>Does Not Contain... - openFilterWindow('^')}>Begins With... - openFilterWindow('!^')}>Does Not Begin With... - openFilterWindow('$')}>Ends With... - openFilterWindow('!$')}>Does Not End With... - - ); - } -} - -export default function DataFilterControl({ - isReadOnly = false, - filterType, - filter, - setFilter, - focusIndex = 0, - onFocusGrid = undefined, -}) { - const showModal = useShowModal(); - const showMenu = useShowMenu(); - const theme = useTheme(); - const [filterState, setFilterState] = React.useState('empty'); - const setFilterText = filter => { - setFilter(filter); - editorRef.current.value = filter || ''; - updateFilterState(); - }; - const applyFilter = () => { - if ((filter || '') == (editorRef.current.value || '')) return; - setFilter(editorRef.current.value); - }; - const filterMultipleValues = () => { - showModal(modalState => ( - setFilterText(createMultiLineFilter(mode, text))} - /> - )); - }; - const openFilterWindow = operator => { - showModal(modalState => ( - setFilterText(text)} - condition1={operator} - /> - )); - }; - const buttonRef = React.useRef(); - const editorRef = React.useRef(); - - React.useEffect(() => { - if (focusIndex) editorRef.current.focus(); - }, [focusIndex]); - - const handleKeyDown = ev => { - if (isReadOnly) return; - if (ev.keyCode == keycodes.enter) { - applyFilter(); - } - if (ev.keyCode == keycodes.escape) { - setFilterText(''); - } - if (ev.keyCode == keycodes.downArrow) { - if (onFocusGrid) onFocusGrid(); - // ev.stopPropagation(); - ev.preventDefault(); - } - // if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) { - // if (this.props.onControlKey) this.props.onControlKey(ev.keyCode); - // } - }; - - const updateFilterState = () => { - const value = editorRef.current.value; - try { - if (value) { - parseFilter(value, filterType); - setFilterState('ok'); - } else { - setFilterState('empty'); - } - } catch (err) { - // console.log('PARSE ERROR', err); - setFilterState('error'); - } - }; - - React.useEffect(() => { - editorRef.current.value = filter || ''; - updateFilterState(); - }, [filter]); - - const handleShowMenu = () => { - const rect = buttonRef.current.getBoundingClientRect(); - showMenu( - rect.left, - rect.bottom, - - ); - }; - - function handlePaste(event) { - var pastedText = undefined; - // @ts-ignore - if (window.clipboardData && window.clipboardData.getData) { - // IE - // @ts-ignore - pastedText = window.clipboardData.getData('Text'); - } else if (event.clipboardData && event.clipboardData.getData) { - pastedText = event.clipboardData.getData('text/plain'); - } - if (pastedText && pastedText.includes('\n')) { - event.preventDefault(); - setFilterText(createMultiLineFilter('is', pastedText)); - } - } - - return ( - - - - - - - ); -} -// domEditor: Element; - -// @autobind -// applyFilter() { -// this.props.setFilter($(this.domEditor).val()); -// } - -// @autobind -// clearFilter() { -// $(this.domEditor).val(''); -// this.applyFilter(); -// } - -// setFilter(value: string) { -// $(this.domEditor).val(value); -// this.applyFilter(); -// return false; -// } - -// render() { -// let dropDownContent = null; - -// let filterIconSpan = ; -// //filterIconSpan = null; - -// if (this.props.filterType == 'Number') { -// dropDownContent = -// this.setFilter('')}>Clear Filter -// this.filterMultipleValues()}>Filter multiple values -// this.openFilterWindow('=')}>Equals... -// this.openFilterWindow('<>')}>Does Not Equal... -// this.setFilter('NULL')}>Is Null -// this.setFilter('NOT NULL')}>Is Not Null -// this.openFilterWindow('>')}>Greater Than... -// this.openFilterWindow('>=')}>Greater Than Or Equal To... -// this.openFilterWindow('<')}>Less Than... -// this.openFilterWindow('<=')}>Less Than Or Equal To... -// ; -// } - -// if (this.props.filterType == 'Logical') { -// dropDownContent = -// this.setFilter('')}>Clear Filter -// this.filterMultipleValues()}>Filter multiple values -// this.setFilter('NULL')}>Is Null -// this.setFilter('NOT NULL')}>Is Not Null -// this.setFilter('TRUE')}>Is True -// this.setFilter('FALSE')}>Is False -// this.setFilter('TRUE, NULL')}>Is True or NULL -// this.setFilter('FALSE, NULL')}>Is False or NULL -// ; -// } - -// if (this.props.filterType == 'DateTime') { -// dropDownContent = -// this.setFilter('')}>Clear Filter -// this.filterMultipleValues()}>Filter multiple values -// this.setFilter('NULL')}>Is Null -// this.setFilter('NOT NULL')}>Is Not Null - -// - -// this.openFilterWindow('<=')}>Before... -// this.openFilterWindow('>=')}>After... -// this.openFilterWindow('>=;<=')}>Between... - -// - -// this.setFilter('TOMORROW')}>Tomorrow -// this.setFilter('TODAY')}>Today -// this.setFilter('YESTERDAY')}>Yesterday - -// - -// this.setFilter('NEXT WEEK')}>Next Week -// this.setFilter('THIS WEEK')}>This Week -// this.setFilter('LAST WEEK')}>Last Week - -// - -// this.setFilter('NEXT MONTH')}>Next Month -// this.setFilter('THIS MONTH')}>This Month -// this.setFilter('LAST MONTH')}>Last Month - -// - -// this.setFilter('NEXT YEAR')}>Next Year -// this.setFilter('THIS YEAR')}>This Year -// this.setFilter('LAST YEAR')}>Last Year - -// - -// - -// this.setFilter('JAN')}>January -// this.setFilter('FEB')}>February -// this.setFilter('MAR')}>March -// this.setFilter('APR')}>April -// this.setFilter('JUN')}>June -// this.setFilter('JUL')}>July -// this.setFilter('AUG')}>August -// this.setFilter('SEP')}>September -// this.setFilter('OCT')}>October -// this.setFilter('NOV')}>November -// this.setFilter('DEC')}>December - -// - -// this.setFilter('MON')}>Monday -// this.setFilter('TUE')}>Tuesday -// this.setFilter('WED')}>Wednesday -// this.setFilter('THU')}>Thursday -// this.setFilter('FRI')}>Friday -// this.setFilter('SAT')}>Saturday -// this.setFilter('SUN')}>Sunday - -// -// ; -// } - -// if (this.props.filterType == 'String') { -// dropDownContent = -// this.setFilter('')}>Clear Filter -// this.filterMultipleValues()}>Filter multiple values - -// this.openFilterWindow('=')}>Equals... -// this.openFilterWindow('<>')}>Does Not Equal... -// this.setFilter('NULL')}>Is Null -// this.setFilter('NOT NULL')}>Is Not Null -// this.setFilter('EMPTY, NULL')}>Is Empty Or Null -// this.setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value - -// - -// this.openFilterWindow('+')}>Contains... -// this.openFilterWindow('~')}>Does Not Contain... -// this.openFilterWindow('^')}>Begins With... -// this.openFilterWindow('!^')}>Does Not Begin With... -// this.openFilterWindow('$')}>Ends With... -// this.openFilterWindow('!$')}>Does Not End With... -// ; -// } - -// if (this.props.isReadOnly) { -// dropDownContent = ; -// } - -// return
-// this.setDomEditor(x)} onKeyDown={this.editorKeyDown} placeholder='Search' > - -// {dropDownContent} -//
; -// } - -// async filterMultipleValues() { -// let result = await ModalDialog.run(); -// if (!result) return; -// let { mode, text } = result; -// let filter = createMultiLineFilter(mode, text); -// this.setFilter(filter); -// } - -// openFilterWindow(selectedOperator: string) { -// FilterDialog.runFilter(this, this.props.filterType, selectedOperator); -// return false; -// } - -// setDomEditor(editor) { -// this.domEditor = editor; -// $(editor).val(this.props.getFilter()); -// } - -// @autobind -// editorKeyDown(ev) { -// if (this.props.isReadOnly) return; -// if (ev.keyCode == KeyCodes.Enter) { -// this.applyFilter(); -// } -// if (ev.keyCode == KeyCodes.Escape) { -// this.clearFilter(); -// } -// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) { -// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode); -// } -// } - -// focus() { -// $(this.domEditor).focus(); -// } -// } diff --git a/packages/web/src/datagrid/DataFilterControl.svelte b/packages/web/src/datagrid/DataFilterControl.svelte new file mode 100644 index 000000000..9ce5e1c34 --- /dev/null +++ b/packages/web/src/datagrid/DataFilterControl.svelte @@ -0,0 +1,218 @@ + + + + +
+ + + {#if showResizeSplitter} +
+ {/if} +
+ + diff --git a/packages/web/src/datagrid/DataGrid.js b/packages/web/src/datagrid/DataGrid.js deleted file mode 100644 index d8cca115a..000000000 --- a/packages/web/src/datagrid/DataGrid.js +++ /dev/null @@ -1,83 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import ColumnManager from './ColumnManager'; -import FormViewFilters from '../formview/FormViewFilters'; - -import ReferenceManager from './ReferenceManager'; -import { HorizontalSplitter } from '../widgets/Splitter'; -import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar'; -import CellDataView from '../celldata/CellDataView'; -import useTheme from '../theme/useTheme'; - -const LeftContainer = styled.div` - background-color: ${props => props.theme.manager_background}; - display: flex; - flex: 1; -`; - -const DataGridContainer = styled.div` - position: relative; - flex-grow: 1; -`; - -export default function DataGrid(props) { - const { GridCore, FormView, formDisplay } = props; - const theme = useTheme(); - const [managerSize, setManagerSize] = React.useState(0); - const [selection, setSelection] = React.useState([]); - const [formSelection, setFormSelection] = React.useState(null); - const [grider, setGrider] = React.useState(null); - const [collapsedWidgets, setCollapsedWidgets] = React.useState([]); - // const [formViewData, setFormViewData] = React.useState(null); - const isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView); - - return ( - - - - {!isFormView && ( - - - - )} - {isFormView && ( - - - - )} - {props.showReferences && props.display.hasReferences && ( - - - - )} - - {isFormView ? ( - - ) : ( - - )} - - - - - - {isFormView ? ( - - ) : ( - - )} - - - ); -} diff --git a/packages/web/src/datagrid/DataGrid.svelte b/packages/web/src/datagrid/DataGrid.svelte new file mode 100644 index 000000000..0075d105c --- /dev/null +++ b/packages/web/src/datagrid/DataGrid.svelte @@ -0,0 +1,63 @@ + + + +
+ + + + + + + + + + + + + +
+ + {#if isFormView} + + {:else} + + {/if} + +
+ + diff --git a/packages/web/src/datagrid/DataGridCell.svelte b/packages/web/src/datagrid/DataGridCell.svelte new file mode 100644 index 000000000..7405474b7 --- /dev/null +++ b/packages/web/src/datagrid/DataGridCell.svelte @@ -0,0 +1,153 @@ + + + + + + {#if hideContent} + + {:else} + {#if value == null} + (NULL) + {:else if _.isDate(value)} + {moment(value).format('YYYY-MM-DD HH:mm:ss')} + {:else if value === true} + 1 + {:else if value === false} + 0 + {:else if _.isNumber(value)} + {#if value >= 10000 || value <= -10000} + {value.toLocaleString()} + {:else} + {value.toString()} + {/if} + {:else if _.isString(value)} + {#if dateTimeRegex.test(value)} + {moment(value).format('YYYY-MM-DD HH:mm:ss')} + {:else} + {highlightSpecialCharacters(value)} + {/if} + {:else if _.isPlainObject(value)} + {#if _.isArray(value.data)} + {#if value.data.length == 1 && isTypeLogical(col.dataType)} + {value.data[0]} + {:else} + ({value.data.length} bytes) + {/if} + {:else} + (RAW) + {/if} + {:else} + {value.toString()} + {/if} + + {#if hintFieldsAllowed && hintFieldsAllowed.includes(col.uniqueName) && rowData} + {rowData[col.hintColumnName]} + {/if} + + {#if col.foreignKey && rowData[col.uniqueName]} + onSetFormView(rowData, col)} /> + {/if} + {/if} + + + diff --git a/packages/web/src/datagrid/DataGridContextMenu.js b/packages/web/src/datagrid/DataGridContextMenu.js deleted file mode 100644 index f95dbff32..000000000 --- a/packages/web/src/datagrid/DataGridContextMenu.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu'; - -export default function DataGridContextMenu({ - copy, - revertRowChanges, - deleteSelectedRows, - insertNewRow, - setNull, - reload, - exportGrid, - filterSelectedValue, - openQuery, - openFreeTable, - openChartSelection, - openActiveChart, - switchToForm, -}) { - return ( - <> - {!!reload && ( - - Reload - - )} - {!!reload && } - - Copy - - {revertRowChanges && ( - - Revert row changes - - )} - {deleteSelectedRows && ( - - Delete selected rows - - )} - {insertNewRow && ( - - Insert new row - - )} - - {setNull && ( - - Set NULL - - )} - {exportGrid && Export} - {filterSelectedValue && ( - - Filter selected value - - )} - {openQuery && Open query} - Open selection in free table editor - Open chart from selection - {openActiveChart && Open active chart} - {!!switchToForm && ( - - Form view - - )} - - ); -} diff --git a/packages/web/src/datagrid/DataGridCore.js b/packages/web/src/datagrid/DataGridCore.js deleted file mode 100644 index 933089ce7..000000000 --- a/packages/web/src/datagrid/DataGridCore.js +++ /dev/null @@ -1,1124 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import ReactDOM from 'react-dom'; -import styled from 'styled-components'; -import { HorizontalScrollBar, VerticalScrollBar } from './ScrollBars'; -import useDimensions from '../utility/useDimensions'; -import DataFilterControl from './DataFilterControl'; -import stableStringify from 'json-stable-stringify'; -import { getFilterType, getFilterValueExpression } from 'dbgate-filterparser'; -import { cellFromEvent, getCellRange, topLeftCell, isRegularCell, nullCell, emptyCellArray } from './selection'; -import keycodes from '../utility/keycodes'; -import DataGridRow from './DataGridRow'; -import { - countColumnSizes, - countVisibleRealColumns, - filterCellForRow, - filterCellsForRow, - cellIsSelected, -} from './gridutil'; -import { copyTextToClipboard } from '../utility/clipboard'; -import DataGridToolbar from './DataGridToolbar'; -// import usePropsCompare from '../utility/usePropsCompare'; -import ColumnHeaderControl from './ColumnHeaderControl'; -import InlineButton from '../widgets/InlineButton'; -import DataGridContextMenu from './DataGridContextMenu'; -import LoadingInfo from '../widgets/LoadingInfo'; -import ErrorInfo from '../widgets/ErrorInfo'; -import { useSetOpenedTabs } from '../utility/globalState'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; -import { useShowMenu } from '../modals/showMenu'; -import useOpenNewTab from '../utility/useOpenNewTab'; -import axios from '../utility/axios'; -import openReferenceForm from '../formview/openReferenceForm'; - -const GridContainer = styled.div` - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - user-select: none; -`; - -const Table = styled.table` - position: absolute; - left: 0; - top: 0; - bottom: 20px; - // right: 20px; - overflow: scroll; - border-collapse: collapse; - outline: none; -`; -const TableHead = styled.thead` - // display: block; - // width: 300px; -`; -const TableBody = styled.tbody` - // display: block; - // overflow: auto; - // height: 100px; -`; -const TableHeaderRow = styled.tr` - // height: 35px; -`; -const TableHeaderCell = styled.td` - // font-weight: bold; - border: 1px solid ${props => props.theme.border}; - // border-collapse: collapse; - text-align: left; - padding: 0; - // padding: 2px; - margin: 0; - background-color: ${props => props.theme.gridheader_background}; - overflow: hidden; -`; -const TableFilterCell = styled.td` - text-align: left; - overflow: hidden; - margin: 0; - padding: 0; -`; -const wheelRowCount = 5; -const FocusField = styled.input` - // visibility: hidden - position: absolute; - left: -1000px; - top: -1000px; -`; - -const RowCountLabel = styled.div` - position: absolute; - background-color: ${props => props.theme.gridbody_background_yellow[1]}; - right: 40px; - bottom: 20px; -`; - -/** @param props {import('./types').DataGridProps} */ -export default function DataGridCore(props) { - const { - display, - conid, - database, - tabVisible, - loadNextData, - errorMessage, - isLoadedAll, - loadedTime, - exportGrid, - openActiveChart, - allRowCount, - openQuery, - onSave, - isLoading, - grider, - onSelectionChanged, - frameSelection, - onKeyDown, - formViewAvailable, - } = props; - // console.log('RENDER GRID', display.baseTable.pureName); - const columns = React.useMemo(() => display.allColumns, [display]); - const openNewTab = useOpenNewTab(); - - // usePropsCompare(props); - - // console.log(`GRID, conid=${conid}, database=${database}, sql=${sql}`); - - const focusFieldRef = React.useRef(null); - - const [vScrollValueToSet, setvScrollValueToSet] = React.useState(); - const [vScrollValueToSetDate, setvScrollValueToSetDate] = React.useState(new Date()); - - const [hScrollValueToSet, sethScrollValueToSet] = React.useState(); - const [hScrollValueToSetDate, sethScrollValueToSetDate] = React.useState(new Date()); - - const [currentCell, setCurrentCell] = React.useState(topLeftCell); - const [selectedCells, setSelectedCells] = React.useState([topLeftCell]); - const [dragStartCell, setDragStartCell] = React.useState(nullCell); - const [shiftDragStartCell, setShiftDragStartCell] = React.useState(nullCell); - const [autofillDragStartCell, setAutofillDragStartCell] = React.useState(nullCell); - const [autofillSelectedCells, setAutofillSelectedCells] = React.useState(emptyCellArray); - const [focusFilterInputs, setFocusFilterInputs] = React.useState({}); - const showMenu = useShowMenu(); - - const autofillMarkerCell = React.useMemo( - () => - selectedCells && selectedCells.length > 0 && _.uniq(selectedCells.map(x => x[0])).length == 1 - ? [_.max(selectedCells.map(x => x[0])), _.max(selectedCells.map(x => x[1]))] - : null, - [selectedCells] - ); - - const [firstVisibleRowScrollIndex, setFirstVisibleRowScrollIndex] = React.useState(0); - const [firstVisibleColumnScrollIndex, setFirstVisibleColumnScrollIndex] = React.useState(0); - - const [headerRowRef, { height: rowHeight }] = useDimensions(); - const [tableBodyRef] = useDimensions(); - const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions(); - - const [inplaceEditorState, dispatchInsplaceEditor] = React.useReducer((state, action) => { - switch (action.type) { - case 'show': - if (!grider.editable) return {}; - return { - cell: action.cell, - text: action.text, - selectAll: action.selectAll, - }; - case 'close': { - const [row, col] = currentCell || []; - if (focusFieldRef.current) focusFieldRef.current.focus(); - // @ts-ignore - if (action.mode == 'enter' && row) setTimeout(() => moveCurrentCell(row + 1, col), 0); - if (action.mode == 'save') setTimeout(handleSave, 0); - return {}; - } - case 'shouldSave': { - return { - ...state, - shouldSave: true, - }; - } - } - return {}; - }, {}); - - // usePropsCompare({ loadedRows, columns, containerWidth, display }); - - const columnSizes = React.useMemo(() => countColumnSizes(grider, columns, containerWidth, display), [ - grider, - columns, - containerWidth, - display, - ]); - const headerColWidth = 40; - - // console.log('containerWidth', containerWidth); - - const gridScrollAreaHeight = containerHeight - 2 * rowHeight; - const gridScrollAreaWidth = containerWidth - columnSizes.frozenSize - headerColWidth - 32; - - const visibleRowCountUpperBound = Math.ceil(gridScrollAreaHeight / Math.floor(Math.max(1, rowHeight))); - const visibleRowCountLowerBound = Math.floor(gridScrollAreaHeight / Math.ceil(Math.max(1, rowHeight))); - // const visibleRowCountUpperBound = 20; - // const visibleRowCountLowerBound = 20; - // console.log('containerHeight', containerHeight); - // console.log('visibleRowCountUpperBound', visibleRowCountUpperBound); - // console.log('rowHeight', rowHeight); - - React.useEffect(() => { - if (tabVisible) { - if (focusFieldRef.current) focusFieldRef.current.focus(); - } - }, [tabVisible, focusFieldRef.current]); - - React.useEffect(() => { - if (onSelectionChanged) { - onSelectionChanged(getSelectedCellsPublished()); - } - }, [onSelectionChanged, selectedCells]); - - const maxScrollColumn = React.useMemo(() => { - let newColumn = columnSizes.scrollInView(0, columns.length - 1 - columnSizes.frozenCount, gridScrollAreaWidth); - return newColumn; - }, [columnSizes, gridScrollAreaWidth]); - - React.useEffect(() => { - if (props.onReferenceSourceChanged && (grider.rowCount > 0 || isLoadedAll)) { - props.onReferenceSourceChanged(getSelectedRowData(), loadedTime); - } - }, [props.onReferenceSourceChanged, selectedCells, props.refReloadToken, grider.getRowData(0)]); - - // usePropsCompare({ columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns }); - - const visibleRealColumns = React.useMemo( - () => countVisibleRealColumns(columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns), - [columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns] - ); - - const realColumnUniqueNames = React.useMemo( - () => - _.range(columnSizes.realCount).map(realIndex => (columns[columnSizes.realToModel(realIndex)] || {}).uniqueName), - [columnSizes, columns] - ); - - React.useEffect(() => { - if (display && display.focusedColumn) { - const invMap = _.invert(realColumnUniqueNames); - const colIndex = invMap[display.focusedColumn]; - if (colIndex) { - scrollIntoView([null, colIndex]); - } - } - }, [display && display.focusedColumn]); - - React.useEffect(() => { - if (loadNextData && firstVisibleRowScrollIndex + visibleRowCountUpperBound >= grider.rowCount) { - loadNextData(); - } - }); - - React.useEffect(() => { - if (display.groupColumns && display.baseTable) { - props.onReferenceClick({ - schemaName: display.baseTable.schemaName, - pureName: display.baseTable.pureName, - columns: display.groupColumns.map(col => ({ - baseName: col, - refName: col, - dataType: _.get(display.baseTable && display.baseTable.columns.find(x => x.columnName == col), 'dataType'), - })), - }); - } - }, [display.baseTable, stableStringify(display && display.groupColumns)]); - - const theme = useTheme(); - - const rowCountInfo = React.useMemo(() => { - if (selectedCells.length > 1 && selectedCells.every(x => _.isNumber(x[0]) && _.isNumber(x[1]))) { - let sum = _.sumBy(selectedCells, cell => { - const row = grider.getRowData(cell[0]); - if (row) { - const colName = realColumnUniqueNames[cell[1]]; - if (colName) { - const data = row[colName]; - if (!data) return 0; - let num = +data; - if (_.isNaN(num)) return 0; - return num; - } - } - return 0; - }); - let count = selectedCells.length; - let rowCount = getSelectedRowData().length; - return `Rows: ${rowCount.toLocaleString()}, Count: ${count.toLocaleString()}, Sum:${sum.toLocaleString()}`; - } - if (allRowCount == null) return 'Loading row count...'; - return `Rows: ${allRowCount.toLocaleString()}`; - }, [selectedCells, allRowCount, grider, visibleRealColumns]); - - const handleSetFormView = React.useMemo( - () => - formViewAvailable && display.baseTable && display.baseTable.primaryKey - ? (rowData, column) => { - if (column) { - openReferenceForm(rowData, column, openNewTab, conid, database); - } else { - display.switchToFormView(rowData); - } - } - : null, - [formViewAvailable, display, openNewTab] - ); - - if (!columns || columns.length == 0) return ; - - if (errorMessage) { - return ; - } - - if (grider.errors && grider.errors.length > 0) { - return ( -
- {grider.errors.map((err, index) => ( - - ))} -
- ); - } - - const handleRowScroll = value => { - setFirstVisibleRowScrollIndex(value); - }; - - const handleColumnScroll = value => { - setFirstVisibleColumnScrollIndex(value); - }; - - const getSelectedFreeData = () => { - const columns = getSelectedColumns(); - const rows = getSelectedRowData().map(row => _.pickBy(row, (v, col) => columns.find(x => x.columnName == col))); - return { - structure: { - columns, - }, - rows, - }; - }; - - const handleOpenFreeTable = () => { - openNewTab( - { - title: 'Data #', - icon: 'img free-table', - tabComponent: 'FreeTableTab', - props: {}, - }, - { editor: getSelectedFreeData() } - ); - }; - - const handleOpenChart = () => { - openNewTab( - { - title: 'Chart #', - icon: 'img chart', - tabComponent: 'ChartTab', - props: {}, - }, - { - editor: { - data: getSelectedFreeData(), - config: { chartType: 'bar' }, - }, - } - ); - }; - - const handleContextMenu = event => { - event.preventDefault(); - showMenu( - event.pageX, - event.pageY, - display.reload() : null} - setNull={grider.editable ? setNull : null} - exportGrid={exportGrid} - filterSelectedValue={display.filterable ? filterSelectedValue : null} - openQuery={openQuery} - openFreeTable={handleOpenFreeTable} - openChartSelection={handleOpenChart} - openActiveChart={openActiveChart} - switchToForm={handleSwitchToFormView} - /> - ); - }; - - function handleGridMouseDown(event) { - if (event.target.closest('.buttonLike')) return; - if (event.target.closest('.resizeHandleControl')) return; - if (event.target.closest('input')) return; - - // event.target.closest('table').focus(); - event.preventDefault(); - if (focusFieldRef.current) focusFieldRef.current.focus(); - const cell = cellFromEvent(event); - - if (event.button == 2 && cell && cellIsSelected(cell[0], cell[1], selectedCells)) return; - - const autofill = event.target.closest('div.autofillHandleMarker'); - if (autofill) { - setAutofillDragStartCell(cell); - } else { - setCurrentCell(cell); - - if (event.ctrlKey) { - if (isRegularCell(cell)) { - if (selectedCells.find(x => x[0] == cell[0] && x[1] == cell[1])) { - setSelectedCells(selectedCells.filter(x => x[0] != cell[0] || x[1] != cell[1])); - } else { - setSelectedCells([...selectedCells, cell]); - } - } - } else { - setSelectedCells(getCellRange(cell, cell)); - setDragStartCell(cell); - - if (isRegularCell(cell) && !_.isEqual(cell, inplaceEditorState.cell) && _.isEqual(cell, currentCell)) { - // @ts-ignore - dispatchInsplaceEditor({ type: 'show', cell, selectAll: true }); - } else if (!_.isEqual(cell, inplaceEditorState.cell)) { - // @ts-ignore - dispatchInsplaceEditor({ type: 'close' }); - } - } - } - - if (display.focusedColumn) display.focusColumn(null); - } - - function handleCopy(event) { - if (event && event.target.localName == 'input') return; - if (event) event.preventDefault(); - copyToClipboard(); - } - - function setCellValue(cell, value) { - grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value); - } - - function handlePaste(event) { - var pastedText = undefined; - // @ts-ignore - if (window.clipboardData && window.clipboardData.getData) { - // IE - // @ts-ignore - pastedText = window.clipboardData.getData('Text'); - } else if (event.clipboardData && event.clipboardData.getData) { - pastedText = event.clipboardData.getData('text/plain'); - } - event.preventDefault(); - grider.beginUpdate(); - const pasteRows = pastedText - .replace(/\r/g, '') - .split('\n') - .map(row => row.split('\t')); - const selectedRegular = cellsToRegularCells(selectedCells); - if (selectedRegular.length <= 1) { - const startRow = isRegularCell(currentCell) ? currentCell[0] : grider.rowCount; - const startCol = isRegularCell(currentCell) ? currentCell[1] : 0; - let rowIndex = startRow; - for (const rowData of pasteRows) { - if (rowIndex >= grider.rowCountInUpdate) { - grider.insertRow(); - } - let colIndex = startCol; - for (const cell of rowData) { - setCellValue([rowIndex, colIndex], cell == '(NULL)' ? null : cell); - colIndex += 1; - } - rowIndex += 1; - } - } - if (selectedRegular.length > 1) { - const startRow = _.min(selectedRegular.map(x => x[0])); - const startCol = _.min(selectedRegular.map(x => x[1])); - for (const cell of selectedRegular) { - const [rowIndex, colIndex] = cell; - const selectionRow = rowIndex - startRow; - const selectionCol = colIndex - startCol; - const pasteRow = pasteRows[selectionRow % pasteRows.length]; - const pasteCell = pasteRow[selectionCol % pasteRow.length]; - setCellValue(cell, pasteCell); - } - } - grider.endUpdate(); - } - - function setNull() { - grider.beginUpdate(); - selectedCells.filter(isRegularCell).forEach(cell => { - setCellValue(cell, null); - }); - grider.endUpdate(); - } - - function cellsToRegularCells(cells) { - cells = _.flatten( - cells.map(cell => { - if (cell[1] == 'header') { - return _.range(0, columnSizes.count).map(col => [cell[0], col]); - } - return [cell]; - }) - ); - cells = _.flatten( - cells.map(cell => { - if (cell[0] == 'header') { - return _.range(0, grider.rowCount).map(row => [row, cell[1]]); - } - return [cell]; - }) - ); - return cells.filter(isRegularCell); - } - - function copyToClipboard() { - const cells = cellsToRegularCells(selectedCells); - const rowIndexes = _.sortBy(_.uniq(cells.map(x => x[0]))); - const lines = rowIndexes.map(rowIndex => { - let colIndexes = _.sortBy(cells.filter(x => x[0] == rowIndex).map(x => x[1])); - const rowData = grider.getRowData(rowIndex); - if (!rowData) return ''; - const line = colIndexes - .map(col => realColumnUniqueNames[col]) - .map(col => (rowData[col] == null ? '(NULL)' : rowData[col])) - .join('\t'); - return line; - }); - const text = lines.join('\r\n'); - copyTextToClipboard(text); - } - - function handleGridMouseMove(event) { - if (autofillDragStartCell) { - const cell = cellFromEvent(event); - if (isRegularCell(cell) && (cell[0] == autofillDragStartCell[0] || cell[1] == autofillDragStartCell[1])) { - const autoFillStart = [selectedCells[0][0], _.min(selectedCells.map(x => x[1]))]; - // @ts-ignore - setAutofillSelectedCells(getCellRange(autoFillStart, cell)); - } - } else if (dragStartCell) { - const cell = cellFromEvent(event); - setCurrentCell(cell); - setSelectedCells(getCellRange(dragStartCell, cell)); - } - } - - function handleGridMouseUp(event) { - if (dragStartCell) { - const cell = cellFromEvent(event); - setCurrentCell(cell); - setSelectedCells(getCellRange(dragStartCell, cell)); - setDragStartCell(null); - } - if (autofillDragStartCell) { - const currentRowNumber = currentCell[0]; - if (_.isNumber(currentRowNumber)) { - const rowIndexes = _.uniq((autofillSelectedCells || []).map(x => x[0])).filter(x => x != currentRowNumber); - const colNames = selectedCells.map(cell => realColumnUniqueNames[cell[1]]); - const changeObject = _.pick(grider.getRowData(currentRowNumber), colNames); - grider.beginUpdate(); - for (const index of rowIndexes) grider.updateRow(index, changeObject); - grider.endUpdate(); - } - - setAutofillDragStartCell(null); - setAutofillSelectedCells([]); - setSelectedCells(autofillSelectedCells); - } - } - - function getSelectedRowIndexes() { - if (selectedCells.find(x => x[0] == 'header')) return _.range(0, grider.rowCount); - return _.uniq((selectedCells || []).map(x => x[0])).filter(x => _.isNumber(x)); - } - - function getSelectedColumnIndexes() { - if (selectedCells.find(x => x[1] == 'header')) return _.range(0, realColumnUniqueNames.length); - return _.uniq((selectedCells || []).map(x => x[1])).filter(x => _.isNumber(x)); - } - - function getSelectedCellsPublished() { - const regular = cellsToRegularCells(selectedCells); - // @ts-ignore - return regular - .map(cell => ({ - row: cell[0], - column: realColumnUniqueNames[cell[1]], - })) - .filter(x => x.column); - - // return regular.map((cell) => { - // const row = cell[0]; - // const column = realColumnUniqueNames[cell[1]]; - // let value = null; - // if (grider && column) { - // let rowData = grider.getRowData(row); - // if (rowData) value = rowData[column]; - // } - // return { - // row, - // column, - // value, - // }; - // }); - } - - function getSelectedRowData() { - return _.compact(getSelectedRowIndexes().map(index => grider.getRowData(index))); - } - - function getSelectedColumns() { - return _.compact( - getSelectedColumnIndexes().map(index => ({ - columnName: realColumnUniqueNames[index], - })) - ); - } - - function revertRowChanges() { - grider.beginUpdate(); - for (const index of getSelectedRowIndexes()) { - if (_.isNumber(index)) grider.revertRowChanges(index); - } - grider.endUpdate(); - } - - function filterSelectedValue() { - const flts = {}; - for (const cell of selectedCells) { - if (!isRegularCell(cell)) continue; - const modelIndex = columnSizes.realToModel(cell[1]); - const columnName = columns[modelIndex].uniqueName; - let value = grider.getRowData(cell[0])[columnName]; - let svalue = getFilterValueExpression(value, columns[modelIndex].dataType); - if (_.has(flts, columnName)) flts[columnName] += ',' + svalue; - else flts[columnName] = svalue; - } - - display.setFilters(flts); - } - - function deleteSelectedRows() { - grider.beginUpdate(); - for (const index of getSelectedRowIndexes()) { - if (_.isNumber(index)) grider.deleteRow(index); - } - grider.endUpdate(); - } - - function handleGridWheel(event) { - let newFirstVisibleRowScrollIndex = firstVisibleRowScrollIndex; - if (event.deltaY > 0) { - newFirstVisibleRowScrollIndex += wheelRowCount; - } - if (event.deltaY < 0) { - newFirstVisibleRowScrollIndex -= wheelRowCount; - } - let rowCount = grider.rowCount; - if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) { - newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1; - } - if (newFirstVisibleRowScrollIndex < 0) { - newFirstVisibleRowScrollIndex = 0; - } - setFirstVisibleRowScrollIndex(newFirstVisibleRowScrollIndex); - // @ts-ignore - setvScrollValueToSet(newFirstVisibleRowScrollIndex); - setvScrollValueToSetDate(new Date()); - } - - function undo() { - grider.undo(); - } - function redo() { - grider.redo(); - } - - function handleSave() { - if (inplaceEditorState.cell) { - // @ts-ignore - dispatchInsplaceEditor({ type: 'shouldSave' }); - return; - } - if (onSave) onSave(); - } - - const insertNewRow = () => { - if (grider.canInsert) { - const rowIndex = grider.insertRow(); - const cell = [rowIndex, (currentCell && currentCell[1]) || 0]; - // @ts-ignore - setCurrentCell(cell); - // @ts-ignore - setSelectedCells([cell]); - scrollIntoView(cell); - } - }; - - const selectTopmostCell = uniquePath => { - const modelIndex = columns.findIndex(x => x.uniquePath == uniquePath); - const realIndex = columnSizes.modelToReal(modelIndex); - let cell = [firstVisibleRowScrollIndex, realIndex]; - // @ts-ignore - setCurrentCell(cell); - // @ts-ignore - setSelectedCells([cell]); - focusFieldRef.current.focus(); - }; - - function handleGridKeyDown(event) { - if (onKeyDown) { - onKeyDown(event); - } - - if (event.keyCode == keycodes.f5) { - event.preventDefault(); - display.reload(); - } - - if (event.keyCode == keycodes.f4) { - event.preventDefault(); - handleSwitchToFormView(); - } - - if (event.keyCode == keycodes.s && event.ctrlKey) { - event.preventDefault(); - handleSave(); - // this.saveAndFocus(); - } - - if (event.keyCode == keycodes.n0 && event.ctrlKey) { - event.preventDefault(); - setNull(); - } - - if (event.keyCode == keycodes.r && event.ctrlKey) { - event.preventDefault(); - revertRowChanges(); - } - - if (event.keyCode == keycodes.f && event.ctrlKey) { - event.preventDefault(); - filterSelectedValue(); - } - - if (event.keyCode == keycodes.z && event.ctrlKey) { - event.preventDefault(); - undo(); - } - - if (event.keyCode == keycodes.y && event.ctrlKey) { - event.preventDefault(); - redo(); - } - - if (event.keyCode == keycodes.c && event.ctrlKey) { - event.preventDefault(); - copyToClipboard(); - } - - if (event.keyCode == keycodes.delete && event.ctrlKey) { - event.preventDefault(); - deleteSelectedRows(); - // this.saveAndFocus(); - } - - if (event.keyCode == keycodes.insert && !event.ctrlKey) { - event.preventDefault(); - insertNewRow(); - // this.saveAndFocus(); - } - - if (inplaceEditorState.cell) return; - - if ( - !event.ctrlKey && - !event.altKey && - ((event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) || - (event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) || - event.keyCode == keycodes.dash) - ) { - // @ts-ignore - dispatchInsplaceEditor({ type: 'show', text: event.nativeEvent.key, cell: currentCell }); - // console.log('event', event.nativeEvent); - } - - if (event.keyCode == keycodes.f2) { - // @ts-ignore - dispatchInsplaceEditor({ type: 'show', cell: currentCell, selectAll: true }); - } - - const moved = handleCursorMove(event); - - if (moved) { - if (event.shiftKey) { - if (!isRegularCell(shiftDragStartCell)) { - setShiftDragStartCell(currentCell); - } - } else { - setShiftDragStartCell(nullCell); - } - } - - const newCell = handleCursorMove(event); - if (event.shiftKey && newCell) { - // @ts-ignore - setSelectedCells(getCellRange(shiftDragStartCell || currentCell, newCell)); - } - } - - function handleCursorMove(event) { - if (!isRegularCell(currentCell)) return null; - let rowCount = grider.rowCount; - if (event.ctrlKey) { - switch (event.keyCode) { - case keycodes.upArrow: - case keycodes.pageUp: - return moveCurrentCell(0, currentCell[1], event); - case keycodes.downArrow: - case keycodes.pageDown: - return moveCurrentCell(rowCount - 1, currentCell[1], event); - case keycodes.leftArrow: - return moveCurrentCell(currentCell[0], 0, event); - case keycodes.rightArrow: - return moveCurrentCell(currentCell[0], columnSizes.realCount - 1, event); - case keycodes.home: - return moveCurrentCell(0, 0, event); - case keycodes.end: - return moveCurrentCell(rowCount - 1, columnSizes.realCount - 1, event); - case keycodes.a: - setSelectedCells([['header', 'header']]); - event.preventDefault(); - return ['header', 'header']; - } - } else { - switch (event.keyCode) { - case keycodes.upArrow: - if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]); - return moveCurrentCell(currentCell[0] - 1, currentCell[1], event); - case keycodes.downArrow: - case keycodes.enter: - return moveCurrentCell(currentCell[0] + 1, currentCell[1], event); - case keycodes.leftArrow: - return moveCurrentCell(currentCell[0], currentCell[1] - 1, event); - case keycodes.rightArrow: - return moveCurrentCell(currentCell[0], currentCell[1] + 1, event); - 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); - case keycodes.pageDown: - return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event); - } - } - return null; - } - - function focusFilterEditor(columnRealIndex) { - let modelIndex = columnSizes.realToModel(columnRealIndex); - setFocusFilterInputs(cols => ({ - ...cols, - [columns[modelIndex].uniqueName]: (cols[columns[modelIndex].uniqueName] || 0) + 1, - })); - // this.headerFilters[this.columns[modelIndex].uniquePath].focus(); - return ['filter', columnRealIndex]; - } - - 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; - setCurrentCell([row, col]); - // setSelectedCells([...(event.ctrlKey ? selectedCells : []), [row, col]]); - setSelectedCells([[row, col]]); - scrollIntoView([row, col]); - // this.selectedCells.push(this.currentCell); - // this.scrollIntoView(this.currentCell); - - if (event) event.preventDefault(); - return [row, col]; - } - - function scrollIntoView(cell) { - const [row, col] = cell; - - if (row != null) { - let newRow = null; - const rowCount = grider.rowCount; - if (rowCount == 0) return; - - if (row < firstVisibleRowScrollIndex) newRow = row; - else if (row + 1 >= firstVisibleRowScrollIndex + visibleRowCountLowerBound) - newRow = row - visibleRowCountLowerBound + 2; - - if (newRow < 0) newRow = 0; - if (newRow >= rowCount) newRow = rowCount - 1; - - if (newRow != null) { - setFirstVisibleRowScrollIndex(newRow); - // firstVisibleRowScrollIndex = newRow; - setvScrollValueToSet(newRow); - setvScrollValueToSetDate(new Date()); - // vscroll.value = newRow; - } - //int newRow = _rowSizes.ScrollInView(FirstVisibleRowScrollIndex, cell.Row.Value - _rowSizes.FrozenCount, GridScrollAreaHeight); - //ScrollContent(newRow, FirstVisibleColumnScrollIndex); - } - - if (col != null) { - if (col >= columnSizes.frozenCount) { - let newColumn = columnSizes.scrollInView( - firstVisibleColumnScrollIndex, - col - columnSizes.frozenCount, - gridScrollAreaWidth - ); - setFirstVisibleColumnScrollIndex(newColumn); - - // @ts-ignore - sethScrollValueToSet(newColumn); - sethScrollValueToSetDate(new Date()); - - // firstVisibleColumnScrollIndex = newColumn; - // hscroll.value = newColumn; - } - } - } - - function setGrouping(uniqueName, groupFunc) { - display.setGrouping(uniqueName, groupFunc); - } - - // console.log('visibleRowCountUpperBound', visibleRowCountUpperBound); - // console.log('gridScrollAreaHeight', gridScrollAreaHeight); - // console.log('containerHeight', containerHeight); - - const hederColwidthPx = `${headerColWidth}px`; - const filterCount = display.filterCount; - - const handleClearFilters = () => { - display.clearFilters(); - }; - - const handleSwitchToFormView = - formViewAvailable && display.baseTable && display.baseTable.primaryKey - ? () => { - const cell = currentCell; - const rowData = isRegularCell(cell) ? grider.getRowData(cell[0]) : null; - display.switchToFormView(rowData); - } - : null; - - // console.log('visibleRealColumnIndexes', visibleRealColumnIndexes); - // console.log( - // 'gridScrollAreaWidth / columnSizes.getVisibleScrollSizeSum()', - // gridScrollAreaWidth, - // columnSizes.getVisibleScrollSizeSum() - // ); - - // const loadedAndInsertedRows = [...loadedRows, ...insertedRows]; - - // console.log('focusFieldRef.current', focusFieldRef.current); - - return ( - - - - - - - {visibleRealColumns.map(col => ( - - display.setSort(col.uniqueName, order) : null} - order={display.getSortOrder(col.uniqueName)} - onResize={diff => display.resizeColumn(col.uniqueName, col.widthNumber, diff)} - setGrouping={display.sortable ? groupFunc => setGrouping(col.uniqueName, groupFunc) : null} - grouping={display.getGrouping(col.uniqueName)} - /> - - ))} - - {display.filterable && ( - - - {filterCount > 0 && ( - - - - )} - - {visibleRealColumns.map(col => ( - - display.setFilter(col.uniqueName, value)} - focusIndex={focusFilterInputs[col.uniqueName]} - onFocusGrid={() => { - selectTopmostCell(col.uniqueName); - // focusFieldRef.current.focus(); - }} - /> - - ))} - - )} - - - {_.range(firstVisibleRowScrollIndex, firstVisibleRowScrollIndex + visibleRowCountUpperBound).map(rowIndex => ( - - ))} - -
- - - {allRowCount && {rowCountInfo}} - {props.toolbarPortalRef && - props.toolbarPortalRef.current && - tabVisible && - ReactDOM.createPortal( - display.reload()} - save={handleSave} - grider={grider} - reconnect={async () => { - await axios.post('database-connections/refresh', { conid, database }); - display.reload(); - }} - switchToForm={handleSwitchToFormView} - />, - props.toolbarPortalRef.current - )} - {isLoading && } -
- ); -} diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte new file mode 100644 index 000000000..ab85c5a40 --- /dev/null +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -0,0 +1,1086 @@ + + + + +
+ { + lastFocusedDataGrid = instance; + clearLastFocusedFormView(); + invalidateCommands(); + }} + on:paste={handlePaste} + /> + + + + + {/each} + + {#if display.filterable} + + + {#each visibleRealColumns as col (col.uniqueName)} + + {/each} + + {/if} + + + {#each _.range(firstVisibleRowScrollIndex, Math.min(firstVisibleRowScrollIndex + visibleRowCountUpperBound, grider.rowCount)) as rowIndex (rowIndex)} + + {/each} + +
+ {#each visibleRealColumns as col (col.uniqueName)} + + display.setSort(col.uniqueName, order) : null} + order={display.getSortOrder(col.uniqueName)} + on:resizeSplitter={e => { + // @ts-ignore + display.resizeColumn(col.uniqueName, col.width, e.detail); + }} + setGrouping={display.sortable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null} + grouping={display.getGrouping(col.uniqueName)} + /> +
+ {#if display.filterCount > 0} + display.clearFilters()} square> + + + {/if} + + display.setFilter(col.uniqueName, value)} + showResizeSplitter + on:resizeSplitter={e => { + // @ts-ignore + display.resizeColumn(col.uniqueName, col.width, e.detail); + }} + /> +
+ (firstVisibleColumnScrollIndex = e.detail)} + bind:this={domHorizontalScroll} + /> + (firstVisibleRowScrollIndex = e.detail)} + bind:this={domVerticalScroll} + /> + {#if allRowCount} +
+ {getRowCountInfo(selectedCells, grider, realColumnUniqueNames, getSelectedRowData(), allRowCount)} +
+ {/if} + + {#if isLoading} + + {/if} +
+ + diff --git a/packages/web/src/datagrid/DataGridRow.js b/packages/web/src/datagrid/DataGridRow.js deleted file mode 100644 index f468ac252..000000000 --- a/packages/web/src/datagrid/DataGridRow.js +++ /dev/null @@ -1,333 +0,0 @@ -// @ts-nocheck -import moment from 'moment'; -import _ from 'lodash'; -import React from 'react'; -import styled from 'styled-components'; -import InplaceEditor from './InplaceEditor'; -import { cellIsSelected } from './gridutil'; -import { isTypeLogical } from 'dbgate-tools'; -import useTheme from '../theme/useTheme'; -import { FontIcon } from '../icons'; - -const TableBodyCell = styled.td` - font-weight: normal; - border: 1px solid ${props => props.theme.border}; - // border-collapse: collapse; - padding: 2px; - white-space: nowrap; - position: relative; - overflow: hidden; - ${props => - props.isSelected && - !props.isAutofillSelected && - !props.isFocusedColumn && - ` - background: initial; - background-color: ${props.theme.gridbody_selection[4]}; - color: ${props.theme.gridbody_invfont1};`} - - ${props => - props.isFrameSelected && - ` - outline: 3px solid ${props.theme.gridbody_selection[4]}; - outline-offset: -3px;`} - - ${props => - props.isAutofillSelected && - !props.isFocusedColumn && - ` - outline: 3px solid ${props.theme.gridbody_selection[4]}; - outline-offset: -3px;`} - - ${props => - props.isModifiedRow && - !props.isInsertedRow && - !props.isSelected && - !props.isAutofillSelected && - !props.isModifiedCell && - !props.isFocusedColumn && - ` - background-color: ${props.theme.gridbody_background_gold[1]};`} - ${props => - !props.isSelected && - !props.isAutofillSelected && - !props.isInsertedRow && - !props.isFocusedColumn && - props.isModifiedCell && - ` - background-color: ${props.theme.gridbody_background_orange[1]};`} - - ${props => - !props.isSelected && - !props.isAutofillSelected && - !props.isFocusedColumn && - props.isInsertedRow && - ` - background-color: ${props.theme.gridbody_background_green[1]};`} - - ${props => - !props.isSelected && - !props.isAutofillSelected && - !props.isFocusedColumn && - props.isDeletedRow && - ` - background-color: ${props.theme.gridbody_background_volcano[1]}; - `} - - ${props => - props.isFocusedColumn && - ` - background-color: ${props.theme.gridbody_background_yellow[0]}; - `} - - ${props => - props.isDeletedRow && - ` - background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg=='); - // from http://www.patternify.com/ - background-repeat: repeat-x; - background-position: 50% 50%;`} -`; - -const HintSpan = styled.span` - color: ${props => props.theme.gridbody_font3}; - margin-left: 5px; -`; -const NullSpan = styled.span` - color: ${props => props.theme.gridbody_font3}; - font-style: italic; -`; - -const TableBodyRow = styled.tr` - // height: 35px; - background-color: ${props => props.theme.gridbody_background}; - &:nth-child(6n + 3) { - background-color: ${props => props.theme.gridbody_background_alt2}; - } - &:nth-child(6n + 6) { - background-color: ${props => props.theme.gridbody_background_alt3}; - } -`; - -const TableHeaderCell = styled.td` - border: 1px solid ${props => props.theme.border}; - text-align: left; - padding: 2px; - background-color: ${props => props.theme.gridheader_background}; - overflow: hidden; - position: relative; -`; - -const AutoFillPoint = styled.div` - width: 8px; - height: 8px; - background-color: ${props => props.theme.gridbody_selection[6]}; - position: absolute; - right: 0px; - bottom: 0px; - overflow: visible; - cursor: crosshair; -`; - -export const ShowFormButton = styled.div` - position: absolute; - right: 0px; - top: 1px; - color: ${props => props.theme.gridbody_font3}; - background-color: ${props => props.theme.gridheader_background}; - border: 1px solid ${props => props.theme.gridheader_background}; - &:hover { - color: ${props => props.theme.gridheader_font_hover}; - border: 1px solid ${props => props.theme.border}; - top: 1px; - right: 0px; - } -`; - -function makeBulletString(value) { - return _.pad('', value.length, '•'); -} - -function highlightSpecialCharacters(value) { - value = value.replace(/\n/g, '↲'); - value = value.replace(/\r/g, ''); - value = value.replace(/^(\s+)/, makeBulletString); - value = value.replace(/(\s+)$/, makeBulletString); - value = value.replace(/(\s\s+)/g, makeBulletString); - return value; -} - -const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/; - -export function CellFormattedValue({ value, dataType, theme }) { - if (value == null) return (NULL); - if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss'); - if (value === true) return '1'; - if (value === false) return '0'; - if (_.isNumber(value)) { - if (value >= 10000 || value <= -10000) { - return value.toLocaleString(); - } - return value.toString(); - } - if (_.isString(value)) { - if (dateTimeRegex.test(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss'); - return highlightSpecialCharacters(value); - } - if (_.isPlainObject(value)) { - if (_.isArray(value.data)) { - if (value.data.length == 1 && isTypeLogical(dataType)) return value.data[0]; - return ({value.data.length} bytes); - } - return (RAW); - } - return value.toString(); -} - -function RowHeaderCell({ rowIndex, theme, onSetFormView, rowData }) { - const [mouseIn, setMouseIn] = React.useState(false); - - return ( - setMouseIn(true) : null} - onMouseLeave={onSetFormView ? () => setMouseIn(false) : null} - > - {rowIndex + 1} - {!!onSetFormView && mouseIn && ( - { - e.stopPropagation(); - onSetFormView(rowData); - }} - > - - - )} - - ); -} - -/** @param props {import('./types').DataGridProps} */ -function DataGridRow(props) { - const { - rowHeight, - rowIndex, - visibleRealColumns, - inplaceEditorState, - dispatchInsplaceEditor, - autofillMarkerCell, - selectedCells, - autofillSelectedCells, - focusedColumn, - grider, - frameSelection, - onSetFormView, - } = props; - // usePropsCompare({ - // rowHeight, - // rowIndex, - // visibleRealColumns, - // inplaceEditorState, - // dispatchInsplaceEditor, - // row, - // display, - // changeSet, - // setChangeSet, - // insertedRowIndex, - // autofillMarkerCell, - // selectedCells, - // autofillSelectedCells, - // }); - - // console.log('RENDER ROW', rowIndex); - - const theme = useTheme(); - - const rowData = grider.getRowData(rowIndex); - const rowStatus = grider.getRowStatus(rowIndex); - - const hintFieldsAllowed = visibleRealColumns - .filter(col => { - if (!col.hintColumnName) return false; - if (rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) return false; - return true; - }) - .map(col => col.uniqueName); - - if (!rowData) return null; - - return ( - - - - {visibleRealColumns.map(col => ( - - {inplaceEditorState.cell && - rowIndex == inplaceEditorState.cell[0] && - col.colIndex == inplaceEditorState.cell[1] ? ( - grider.setCellValue(rowIndex, col.uniqueName, value)} - /> - ) : ( - <> - - {hintFieldsAllowed.includes(col.uniqueName) && ( - {rowData[col.hintColumnName]} - )} - {col.foreignKey && rowData[col.uniqueName] && ( - { - e.stopPropagation(); - onSetFormView(rowData, col); - }} - > - - - )} - - )} - {autofillMarkerCell && autofillMarkerCell[1] == col.colIndex && autofillMarkerCell[0] == rowIndex && ( - - )} - - ))} - - ); -} - -export default React.memo(DataGridRow); diff --git a/packages/web/src/datagrid/DataGridRow.svelte b/packages/web/src/datagrid/DataGridRow.svelte new file mode 100644 index 000000000..458ec1409 --- /dev/null +++ b/packages/web/src/datagrid/DataGridRow.svelte @@ -0,0 +1,80 @@ + + + + onSetFormView(rowData, null) : null} /> + {#each visibleRealColumns as col (col.uniqueName)} + {#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]} + + grider.setCellValue(rowIndex, col.uniqueName, value)} + /> + + {:else} + + {/if} + {/each} + + + diff --git a/packages/web/src/datagrid/DataGridToolbar.js b/packages/web/src/datagrid/DataGridToolbar.js deleted file mode 100644 index 800aa8586..000000000 --- a/packages/web/src/datagrid/DataGridToolbar.js +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import ToolbarButton from '../widgets/ToolbarButton'; - -export default function DataGridToolbar({ reload, reconnect, grider, save, switchToForm }) { - return ( - <> - {switchToForm && ( - - Form view - - )} - - Refresh - - - Reconnect - - grider.undo()} icon="icon undo"> - Undo - - grider.redo()} icon="icon redo"> - Redo - - - Save - - grider.revertAllChanges()} icon="icon close"> - Revert - - - ); -} diff --git a/packages/web/src/datagrid/HorizontalScrollBar.svelte b/packages/web/src/datagrid/HorizontalScrollBar.svelte new file mode 100644 index 000000000..da7c03925 --- /dev/null +++ b/packages/web/src/datagrid/HorizontalScrollBar.svelte @@ -0,0 +1,41 @@ + + +
+
 
+
+ + diff --git a/packages/web/src/datagrid/InplaceEditor.js b/packages/web/src/datagrid/InplaceEditor.js deleted file mode 100644 index e32ab10ed..000000000 --- a/packages/web/src/datagrid/InplaceEditor.js +++ /dev/null @@ -1,98 +0,0 @@ -// @ts-nocheck - -import _ from 'lodash'; -import React from 'react'; -import styled from 'styled-components'; -import keycodes from '../utility/keycodes'; - -const StyledInput = styled.input` - border: 0px solid; - outline: none; - margin: 0px; - padding: 0px; -`; - -export default function InplaceEditor({ - widthPx, - // rowIndex, - // uniqueName, - // grider, - cellValue, - inplaceEditorState, - dispatchInsplaceEditor, - onSetValue, -}) { - const editorRef = React.useRef(); - const widthRef = React.useRef(widthPx); - const isChangedRef = React.useRef(!!inplaceEditorState.text); - React.useEffect(() => { - const editor = editorRef.current; - editor.value = inplaceEditorState.text || cellValue; - editor.focus(); - if (inplaceEditorState.selectAll) { - editor.select(); - } - }, []); - function handleBlur() { - if (isChangedRef.current) { - const editor = editorRef.current; - onSetValue(editor.value); - // grider.setCellValue(rowIndex, uniqueName, editor.value); - isChangedRef.current = false; - } - dispatchInsplaceEditor({ type: 'close' }); - } - if (inplaceEditorState.shouldSave) { - const editor = editorRef.current; - if (isChangedRef.current) { - onSetValue(editor.value); - // grider.setCellValue(rowIndex, uniqueName, editor.value); - isChangedRef.current = false; - } - editor.blur(); - dispatchInsplaceEditor({ type: 'close', mode: 'save' }); - } - function handleKeyDown(event) { - const editor = editorRef.current; - switch (event.keyCode) { - case keycodes.escape: - isChangedRef.current = false; - dispatchInsplaceEditor({ type: 'close' }); - break; - case keycodes.enter: - if (isChangedRef.current) { - // grider.setCellValue(rowIndex, uniqueName, editor.value); - onSetValue(editor.value); - isChangedRef.current = false; - } - editor.blur(); - dispatchInsplaceEditor({ type: 'close', mode: 'enter' }); - break; - case keycodes.s: - if (event.ctrlKey) { - if (isChangedRef.current) { - onSetValue(editor.value); - // grider.setCellValue(rowIndex, uniqueName, editor.value); - isChangedRef.current = false; - } - event.preventDefault(); - dispatchInsplaceEditor({ type: 'close', mode: 'save' }); - } - break; - } - } - return ( - (isChangedRef.current = true)} - onKeyDown={handleKeyDown} - style={{ - width: widthRef.current, - minWidth: widthRef.current, - maxWidth: widthRef.current, - }} - /> - ); -} diff --git a/packages/web/src/datagrid/InplaceEditor.svelte b/packages/web/src/datagrid/InplaceEditor.svelte new file mode 100644 index 000000000..bbb1ad527 --- /dev/null +++ b/packages/web/src/datagrid/InplaceEditor.svelte @@ -0,0 +1,81 @@ + + + isChangedRef.set(true)} + on:keydown={handleKeyDown} + on:blur={handleBlur} + bind:this={domEditor} + style={widthCopy ? `width:${widthCopy}px;min-width:${widthCopy}px;max-width:${widthCopy}px` : undefined} +/> + + diff --git a/packages/web/src/datagrid/JslDataGrid.svelte b/packages/web/src/datagrid/JslDataGrid.svelte new file mode 100644 index 000000000..3f17d79da --- /dev/null +++ b/packages/web/src/datagrid/JslDataGrid.svelte @@ -0,0 +1,29 @@ + + +{#key jslid} + +{/key} diff --git a/packages/web/src/datagrid/JslDataGridCore.js b/packages/web/src/datagrid/JslDataGridCore.js deleted file mode 100644 index 1ec74e608..000000000 --- a/packages/web/src/datagrid/JslDataGridCore.js +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; -import axios from '../utility/axios'; -import { useSetOpenedTabs } from '../utility/globalState'; -import useSocket from '../utility/SocketProvider'; -import useShowModal from '../modals/showModal'; -import ImportExportModal from '../modals/ImportExportModal'; -import LoadingDataGridCore from './LoadingDataGridCore'; -import RowsArrayGrider from './RowsArrayGrider'; - -async function loadDataPage(props, offset, limit) { - const { jslid, display } = props; - - const response = await axios.post('jsldata/get-rows', { - jslid, - offset, - limit, - filters: display ? display.compileFilters() : null, - }); - - return response.data; -} - -function dataPageAvailable(props) { - return true; -} - -async function loadRowCount(props) { - const { jslid } = props; - - const response = await axios.request({ - url: 'jsldata/get-stats', - method: 'get', - params: { - jslid, - }, - }); - return response.data.rowCount; -} - -export default function JslDataGridCore(props) { - const { jslid } = props; - const [changeIndex, setChangeIndex] = React.useState(0); - const [rowCountLoaded, setRowCountLoaded] = React.useState(null); - - const showModal = useShowModal(); - - const setOpenedTabs = useSetOpenedTabs(); - const socket = useSocket(); - - function exportGrid() { - const initialValues = {}; - const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/); - if (archiveMatch) { - initialValues.sourceStorageType = 'archive'; - initialValues.sourceArchiveFolder = archiveMatch[1]; - initialValues.sourceList = [archiveMatch[2]]; - } else { - initialValues.sourceStorageType = 'jsldata'; - initialValues.sourceJslId = jslid; - initialValues.sourceList = ['query-data']; - } - showModal(modalState => ); - } - - const handleJslDataStats = React.useCallback( - stats => { - if (stats.changeIndex < changeIndex) return; - setChangeIndex(stats.changeIndex); - setRowCountLoaded(stats.rowCount); - }, - [changeIndex] - ); - - React.useEffect(() => { - if (jslid && socket) { - socket.on(`jsldata-stats-${jslid}`, handleJslDataStats); - return () => { - socket.off(`jsldata-stats-${jslid}`, handleJslDataStats); - }; - } - }, [jslid]); - - return ( - setChangeIndex(0)} - griderFactory={RowsArrayGrider.factory} - griderFactoryDeps={RowsArrayGrider.factoryDeps} - /> - ); -} diff --git a/packages/web/src/datagrid/JslDataGridCore.svelte b/packages/web/src/datagrid/JslDataGridCore.svelte new file mode 100644 index 000000000..fbc5bebe1 --- /dev/null +++ b/packages/web/src/datagrid/JslDataGridCore.svelte @@ -0,0 +1,101 @@ + + + + + diff --git a/packages/web/src/datagrid/LoadingDataGridCore.js b/packages/web/src/datagrid/LoadingDataGridCore.js deleted file mode 100644 index f3a4f5029..000000000 --- a/packages/web/src/datagrid/LoadingDataGridCore.js +++ /dev/null @@ -1,141 +0,0 @@ -import React from 'react'; -import DataGridCore from './DataGridCore'; - -export default function LoadingDataGridCore(props) { - const { - display, - loadDataPage, - dataPageAvailable, - loadRowCount, - loadNextDataToken, - onReload, - exportGrid, - openQuery, - griderFactory, - griderFactoryDeps, - onChangeGrider, - rowCountLoaded, - } = props; - - const [loadProps, setLoadProps] = React.useState({ - isLoading: false, - loadedRows: [], - isLoadedAll: false, - loadedTime: new Date().getTime(), - allRowCount: null, - errorMessage: null, - loadNextDataToken: 0, - }); - const { isLoading, loadedRows, isLoadedAll, loadedTime, allRowCount, errorMessage } = loadProps; - - const loadedTimeRef = React.useRef(0); - - const handleLoadRowCount = async () => { - const rowCount = await loadRowCount(props); - setLoadProps(oldLoadProps => ({ - ...oldLoadProps, - allRowCount: rowCount, - })); - }; - - const reload = () => { - setLoadProps({ - allRowCount: null, - isLoading: false, - loadedRows: [], - isLoadedAll: false, - loadedTime: new Date().getTime(), - errorMessage: null, - loadNextDataToken: 0, - }); - if (onReload) onReload(); - }; - - React.useEffect(() => { - if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) { - display.reload(); - } - if (display.cache.refreshTime > loadedTime) { - reload(); - } - }); - - const loadNextData = async () => { - if (isLoading) return; - setLoadProps(oldLoadProps => ({ - ...oldLoadProps, - isLoading: true, - })); - const loadStart = new Date().getTime(); - loadedTimeRef.current = loadStart; - - const nextRows = await loadDataPage(props, loadedRows.length, 100); - if (loadedTimeRef.current !== loadStart) { - // new load was dispatched - return; - } - // if (!_.isArray(nextRows)) { - // console.log('Error loading data from server', nextRows); - // nextRows = []; - // } - // console.log('nextRows', nextRows); - if (nextRows.errorMessage) { - setLoadProps(oldLoadProps => ({ - ...oldLoadProps, - isLoading: false, - errorMessage: nextRows.errorMessage, - })); - } else { - if (allRowCount == null) handleLoadRowCount(); - const loadedInfo = { - loadedRows: [...loadedRows, ...nextRows], - loadedTime, - }; - setLoadProps(oldLoadProps => ({ - ...oldLoadProps, - isLoading: false, - isLoadedAll: oldLoadProps.loadNextDataToken == loadNextDataToken && nextRows.length === 0, - loadNextDataToken, - ...loadedInfo, - })); - } - }; - - React.useEffect(() => { - setLoadProps(oldProps => ({ - ...oldProps, - isLoadedAll: false, - })); - }, [loadNextDataToken]); - - const griderProps = { ...props, sourceRows: loadedRows }; - const grider = React.useMemo(() => griderFactory(griderProps), griderFactoryDeps(griderProps)); - - React.useEffect(() => { - if (onChangeGrider) onChangeGrider(grider); - }, [grider]); - - const handleLoadNextData = () => { - if (!isLoadedAll && !errorMessage && !grider.disableLoadNextPage) { - if (dataPageAvailable(props)) { - // If not, callbacks to load missing metadata are dispatched - loadNextData(); - } - } - }; - - return ( - - ); -} diff --git a/packages/web/src/datagrid/LoadingDataGridCore.svelte b/packages/web/src/datagrid/LoadingDataGridCore.svelte new file mode 100644 index 000000000..ccebaa93f --- /dev/null +++ b/packages/web/src/datagrid/LoadingDataGridCore.svelte @@ -0,0 +1,122 @@ + + + diff --git a/packages/web/src/datagrid/ManagerStyles.js b/packages/web/src/datagrid/ManagerStyles.js deleted file mode 100644 index 6de11af95..000000000 --- a/packages/web/src/datagrid/ManagerStyles.js +++ /dev/null @@ -1,7 +0,0 @@ -import styled from 'styled-components'; - -export const ManagerInnerContainer = styled.div` - flex: 1 1; - overflow-y: auto; - overflow-x: auto; -`; diff --git a/packages/web/src/datagrid/ReferenceHeader.js b/packages/web/src/datagrid/ReferenceHeader.js deleted file mode 100644 index 4f723a320..000000000 --- a/packages/web/src/datagrid/ReferenceHeader.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import ToolbarButton from '../widgets/ToolbarButton'; -import styled from 'styled-components'; -import dimensions from '../theme/dimensions'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; - -const Container = styled.div` - display: flex; - justify-content: space-between; - align-items: center; - background: ${props => props.theme.gridheader_background_cyan[0]}; - height: ${dimensions.toolBar.height}px; - min-height: ${dimensions.toolBar.height}px; - overflow: hidden; - border-top: 1px solid ${props => props.theme.border}; - border-bottom: 1px solid ${props => props.theme.border}; -`; - -const Header = styled.div` - font-weight: bold; - margin-left: 10px; - display: flex; -`; - -const HeaderText = styled.div` - margin-left: 10px; -`; - -export default function ReferenceHeader({ reference, onClose }) { - const theme = useTheme(); - return ( - -
- - - {reference.pureName} [{reference.columns.map(x => x.refName).join(', ')}] = master [ - {reference.columns.map(x => x.baseName).join(', ')}] - -
- - Close - -
- ); -} diff --git a/packages/web/src/datagrid/ReferenceHeader.svelte b/packages/web/src/datagrid/ReferenceHeader.svelte new file mode 100644 index 000000000..7a2b69b94 --- /dev/null +++ b/packages/web/src/datagrid/ReferenceHeader.svelte @@ -0,0 +1,39 @@ + + +
+
+ +
+ {reference.pureName} [{reference.columns.map(x => x.refName).join(', ')}] = master [ + {reference.columns.map(x => x.baseName).join(', ')}] +
+
+ dispatch('close')}>Close +
+ + diff --git a/packages/web/src/datagrid/ReferenceManager.js b/packages/web/src/datagrid/ReferenceManager.js deleted file mode 100644 index 76b1cfc0e..000000000 --- a/packages/web/src/datagrid/ReferenceManager.js +++ /dev/null @@ -1,114 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { ManagerInnerContainer } from './ManagerStyles'; -import SearchInput from '../widgets/SearchInput'; -import { filterName } from 'dbgate-datalib'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; - -const SearchBoxWrapper = styled.div` - display: flex; - margin-bottom: 5px; -`; - -const Header = styled.div` - font-weight: bold; - white-space: nowrap; -`; - -const LinkContainer = styled.div` - color: ${props => props.theme.manager_font_blue[7]}; - margin: 5px; - &:hover { - text-decoration: underline; - } - cursor: pointer; - display: flex; - flex-wrap: nowrap; -`; - -const NameContainer = styled.div` - margin-left: 5px; - white-space: nowrap; -`; - -function ManagerRow({ tableName, columns, icon, onClick }) { - const theme = useTheme(); - return ( - - - - {tableName} ({columns.map(x => x.columnName).join(', ')}) - - - ); -} - -/** @param props {import('./types').DataGridProps} */ -export default function ReferenceManager(props) { - const [filter, setFilter] = React.useState(''); - const { display } = props; - const { baseTable } = display || {}; - const { foreignKeys } = baseTable || {}; - const { dependencies } = baseTable || {}; - - return ( - <> - - - - - {foreignKeys && foreignKeys.length > 0 && ( - <> -
References tables ({foreignKeys.length})
- {foreignKeys - .filter(fk => filterName(filter, fk.refTableName)) - .map(fk => ( - - props.onReferenceClick({ - schemaName: fk.refSchemaName, - pureName: fk.refTableName, - columns: fk.columns.map(col => ({ - baseName: col.columnName, - refName: col.refColumnName, - })), - }) - } - /> - ))} - - )} - {dependencies && dependencies.length > 0 && ( - <> -
Dependend tables ({dependencies.length})
- {dependencies - .filter(fk => filterName(filter, fk.pureName)) - .map(fk => ( - - props.onReferenceClick({ - schemaName: fk.schemaName, - pureName: fk.pureName, - columns: fk.columns.map(col => ({ - baseName: col.refColumnName, - refName: col.columnName, - })), - }) - } - /> - ))} - - )} -
- - ); -} diff --git a/packages/web/src/datagrid/ReferenceManager.svelte b/packages/web/src/datagrid/ReferenceManager.svelte new file mode 100644 index 000000000..4534c81c5 --- /dev/null +++ b/packages/web/src/datagrid/ReferenceManager.svelte @@ -0,0 +1,89 @@ + + + + + + + {#if foreignKeys.length > 0} +
References tables ({foreignKeys.length})
+ {#each foreignKeys.filter(fk => filterName(filter, fk.refTableName)) as fk} + + {/each} + {/if} + + {#if dependencies.length > 0} +
Dependend tables ({dependencies.length})
+ {#each dependencies.filter(fk => filterName(filter, fk.pureName)) as fk} + + {/each} + {/if} +
+ + diff --git a/packages/web/src/datagrid/RowHeaderCell.svelte b/packages/web/src/datagrid/RowHeaderCell.svelte new file mode 100644 index 000000000..d56fba8e0 --- /dev/null +++ b/packages/web/src/datagrid/RowHeaderCell.svelte @@ -0,0 +1,32 @@ + + + (mouseIn = true)} + on:mouseleave={() => (mouseIn = false)} +> + {rowIndex + 1} + + {#if mouseIn && onShowForm} + + {/if} + + + diff --git a/packages/web/src/datagrid/RowsArrayGrider.ts b/packages/web/src/datagrid/RowsArrayGrider.ts index 76a9e140d..0e21c75e2 100644 --- a/packages/web/src/datagrid/RowsArrayGrider.ts +++ b/packages/web/src/datagrid/RowsArrayGrider.ts @@ -10,11 +10,4 @@ export default class RowsArrayGrider extends Grider { get rowCount() { return this.rows.length; } - - static factory({ sourceRows }): RowsArrayGrider { - return new RowsArrayGrider(sourceRows); - } - static factoryDeps({ sourceRows }) { - return [sourceRows]; - } } diff --git a/packages/web/src/datagrid/ScrollBars.js b/packages/web/src/datagrid/ScrollBars.js deleted file mode 100644 index f2880615d..000000000 --- a/packages/web/src/datagrid/ScrollBars.js +++ /dev/null @@ -1,222 +0,0 @@ -import _ from 'lodash'; -import React from 'react'; -import styled from 'styled-components'; -import useDimensions from '../utility/useDimensions'; - -const StyledHorizontalScrollBar = styled.div` - overflow-x: scroll; - height: 16px; - position: absolute; - bottom: 0; - //left: 100px; - // right: 20px; - right: 0; - left: 0; -`; - -const StyledHorizontalScrollContent = styled.div``; - -const StyledVerticalScrollBar = styled.div` - overflow-y: scroll; - width: 20px; - position: absolute; - right: 0px; - width: 20px; - bottom: 16px; - // bottom: 0; - top: 0; -`; - -const StyledVerticalScrollContent = styled.div``; - -export function HorizontalScrollBar({ - onScroll = undefined, - valueToSet = undefined, - valueToSetDate = undefined, - minimum, - maximum, - viewportRatio = 0.5, -}) { - const [ref, { width }, node] = useDimensions(); - const contentSize = Math.round(width / viewportRatio); - - React.useEffect(() => { - const position01 = (valueToSet - minimum) / (maximum - minimum + 1); - const position = position01 * (contentSize - width); - if (node) node.scrollLeft = Math.floor(position); - }, [valueToSetDate]); - - const handleScroll = () => { - const position = node.scrollLeft; - const ratio = position / (contentSize - width); - if (ratio < 0) return 0; - let res = ratio * (maximum - minimum + 1) + minimum; - onScroll(Math.floor(res + 0.3)); - }; - - return ( - -   - - ); -} - -export function VerticalScrollBar({ - onScroll, - valueToSet = undefined, - valueToSetDate = undefined, - minimum, - maximum, - viewportRatio = 0.5, -}) { - const [ref, { height }, node] = useDimensions(); - const contentSize = Math.round(height / viewportRatio); - - React.useEffect(() => { - const position01 = (valueToSet - minimum) / (maximum - minimum + 1); - const position = position01 * (contentSize - height); - if (node) node.scrollTop = Math.floor(position); - }, [valueToSetDate]); - - const handleScroll = () => { - const position = node.scrollTop; - const ratio = position / (contentSize - height); - if (ratio < 0) return 0; - let res = ratio * (maximum - minimum + 1) + minimum; - onScroll(Math.floor(res + 0.3)); - }; - - return ( - -   - - ); -} - -// export interface IScrollBarProps { -// viewportRatio: number; -// minimum: number; -// maximum: number; -// containerStyle: any; -// onScroll?: any; -// } - -// export abstract class ScrollBarBase extends React.Component { -// domScrollContainer: Element; -// domScrollContent: Element; -// contentSize: number; -// containerResizedBind: any; - -// constructor(props) { -// super(props); -// this.containerResizedBind = this.containerResized.bind(this); -// } - -// componentDidMount() { -// $(this.domScrollContainer).scroll(this.onScroll.bind(this)); -// createResizeDetector(this.domScrollContainer, this.containerResized.bind(this)); -// window.addEventListener('resize', this.containerResizedBind); -// this.updateContentSize(); -// } - -// componentWillUnmount() { -// deleteResizeDetector(this.domScrollContainer); -// window.removeEventListener('resize', this.containerResizedBind); -// } - -// onScroll() { -// if (this.props.onScroll) { -// this.props.onScroll(this.value); -// } -// } - -// get value(): number { -// let position = this.getScrollPosition(); -// let ratio = position / (this.contentSize - this.getContainerSize()); -// if (ratio < 0) return 0; -// let res = ratio * (this.props.maximum - this.props.minimum + 1) + this.props.minimum; -// return Math.floor(res + 0.3); -// } - -// set value(value: number) { -// let position01 = (value - this.props.minimum) / (this.props.maximum - this.props.minimum + 1); -// let position = position01 * (this.contentSize - this.getContainerSize()); -// this.setScrollPosition(Math.floor(position)); -// } - -// containerResized() { -// this.setContentSizeField(); -// this.updateContentSize(); -// } - -// setContentSizeField() { -// let lastContentSize = this.contentSize; -// this.contentSize = Math.round(this.getContainerSize() / this.props.viewportRatio); -// if (_.isNaN(this.contentSize)) this.contentSize = 0; -// if (this.contentSize > 1000000 && detectBrowser() == BrowserType.Firefox) this.contentSize = 1000000; -// if (lastContentSize != this.contentSize) { -// this.updateContentSize(); -// } -// } - -// abstract getContainerSize(): number; -// abstract updateContentSize(); -// abstract getScrollPosition(): number; -// abstract setScrollPosition(value: number); -// } - -// export class HorizontalScrollBar extends ScrollBarBase { -// render() { -// this.setContentSizeField(); - -// return
this.domScrollContainer = x} style={this.props.containerStyle}> -//
this.domScrollContent = x} style={{ width: this.contentSize }}> -//   -//
-//
; -// } - -// getContainerSize(): number { -// return $(this.domScrollContainer).width(); -// } - -// updateContentSize() { -// $(this.domScrollContent).width(this.contentSize); -// } - -// getScrollPosition() { -// return $(this.domScrollContainer).scrollLeft(); -// } - -// setScrollPosition(value: number) { -// $(this.domScrollContainer).scrollLeft(value); -// } -// } - -// export class VerticalScrollBar extends ScrollBarBase { -// render() { -// this.setContentSizeField(); - -// return
this.domScrollContainer = x} style={this.props.containerStyle}> -//
this.domScrollContent = x} style={{ height: this.contentSize }}> -//   -//
-//
; -// } - -// getContainerSize(): number { -// return $(this.domScrollContainer).height(); -// } - -// updateContentSize() { -// $(this.domScrollContent).height(this.contentSize); -// } - -// getScrollPosition() { -// return $(this.domScrollContainer).scrollTop(); -// } - -// setScrollPosition(value: number) { -// $(this.domScrollContainer).scrollTop(value); -// } -// } diff --git a/packages/web/src/datagrid/SeriesSizes-old.js b/packages/web/src/datagrid/SeriesSizes-old.js deleted file mode 100644 index 6c8f98d85..000000000 --- a/packages/web/src/datagrid/SeriesSizes-old.js +++ /dev/null @@ -1,340 +0,0 @@ -import _ from 'lodash'; - -export class SeriesSizeItem { - constructor() { - this.scrollIndex = -1; - this.frozenIndex = -1; - this.modelIndex = 0; - this.size = 0; - this.position = 0; - } - - // modelIndex; - // size; - // position; - - get endPosition() { - return this.position + this.size; - } -} - -export class SeriesSizes { - constructor() { - this.scrollItems = []; - this.sizeOverridesByModelIndex = {}; - this.positions = []; - this.scrollIndexes = []; - this.frozenItems = []; - this.hiddenAndFrozenModelIndexes = null; - this.frozenModelIndexes = null; - - this.count = 0; - this.maxSize = 1000; - this.defaultSize = 50; - } - - // private sizeOverridesByModelIndex: { [id] } = {}; - // count; - // defaultSize; - // maxSize; - // private hiddenAndFrozenModelIndexes[] = []; - // private frozenModelIndexes[] = []; - // private hiddenModelIndexes[] = []; - // private scrollItems: SeriesSizeItem[] = []; - // private positions[] = []; - // private scrollIndexes[] = []; - // private frozenItems: SeriesSizeItem[] = []; - - get scrollCount() { - return this.count - (this.hiddenAndFrozenModelIndexes != null ? this.hiddenAndFrozenModelIndexes.length : 0); - } - get frozenCount() { - return this.frozenModelIndexes != null ? this.frozenModelIndexes.length : 0; - } - get frozenSize() { - return _.sumBy(this.frozenItems, x => x.size); - } - get realCount() { - return this.frozenCount + this.scrollCount; - } - - putSizeOverride(modelIndex, size, sizeByUser = false) { - if (this.maxSize && size > this.maxSize && !sizeByUser) { - size = this.maxSize; - } - - let currentSize = this.sizeOverridesByModelIndex[modelIndex]; - if (sizeByUser || !currentSize || size > currentSize) { - this.sizeOverridesByModelIndex[modelIndex] = size; - } - // if (!_.has(this.sizeOverridesByModelIndex, modelIndex)) - // this.sizeOverridesByModelIndex[modelIndex] = size; - // if (size > this.sizeOverridesByModelIndex[modelIndex]) - // this.sizeOverridesByModelIndex[modelIndex] = size; - } - buildIndex() { - this.scrollItems = []; - this.scrollIndexes = _.filter( - _.map(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelToReal(x) - this.frozenCount), - x => x >= 0 - ); - this.scrollIndexes.sort(); - let lastScrollIndex = -1; - let lastEndPosition = 0; - this.scrollIndexes.forEach(scrollIndex => { - let modelIndex = this.realToModel(scrollIndex + this.frozenCount); - let size = this.sizeOverridesByModelIndex[modelIndex]; - let item = new SeriesSizeItem(); - item.scrollIndex = scrollIndex; - item.modelIndex = modelIndex; - item.size = size; - item.position = lastEndPosition + (scrollIndex - lastScrollIndex - 1) * this.defaultSize; - this.scrollItems.push(item); - lastScrollIndex = scrollIndex; - lastEndPosition = item.endPosition; - }); - this.positions = _.map(this.scrollItems, x => x.position); - this.frozenItems = []; - let lastpos = 0; - for (let i = 0; i < this.frozenCount; i++) { - let modelIndex = this.frozenModelIndexes[i]; - let size = this.getSizeByModelIndex(modelIndex); - let item = new SeriesSizeItem(); - item.frozenIndex = i; - item.modelIndex = modelIndex; - item.size = size; - item.position = lastpos; - this.frozenItems.push(item); - lastpos += size; - } - } - - getScrollIndexOnPosition(position) { - let itemOrder = _.sortedIndex(this.positions, position); - if (this.positions[itemOrder] == position) return itemOrder; - if (itemOrder == 0) return Math.floor(position / this.defaultSize); - if (position <= this.scrollItems[itemOrder - 1].endPosition) return this.scrollItems[itemOrder - 1].scrollIndex; - return ( - Math.floor((position - this.scrollItems[itemOrder - 1].position) / this.defaultSize) + - this.scrollItems[itemOrder - 1].scrollIndex - ); - } - getFrozenIndexOnPosition(position) { - this.frozenItems.forEach(function (item) { - if (position >= item.position && position <= item.endPosition) return item.frozenIndex; - }); - return -1; - } - // getSizeSum(startScrollIndex, endScrollIndex) { - // let order1 = _.sortedIndexOf(this.scrollIndexes, startScrollIndex); - // let order2 = _.sortedIndexOf(this.scrollIndexes, endScrollIndex); - // let count = endScrollIndex - startScrollIndex; - // if (order1 < 0) - // order1 = ~order1; - // if (order2 < 0) - // order2 = ~order2; - // let result = 0; - // for (let i = order1; i <= order2; i++) { - // if (i < 0) - // continue; - // if (i >= this.scrollItems.length) - // continue; - // let item = this.scrollItems[i]; - // if (item.scrollIndex < startScrollIndex) - // continue; - // if (item.scrollIndex >= endScrollIndex) - // continue; - // result += item.size; - // count--; - // } - // result += count * this.defaultSize; - // return result; - // } - getSizeByModelIndex(modelIndex) { - if (_.has(this.sizeOverridesByModelIndex, modelIndex)) return this.sizeOverridesByModelIndex[modelIndex]; - return this.defaultSize; - } - getSizeByScrollIndex(scrollIndex) { - return this.getSizeByRealIndex(scrollIndex + this.frozenCount); - } - getSizeByRealIndex(realIndex) { - let modelIndex = this.realToModel(realIndex); - return this.getSizeByModelIndex(modelIndex); - } - removeSizeOverride(realIndex) { - let modelIndex = this.realToModel(realIndex); - delete this.sizeOverridesByModelIndex[modelIndex]; - } - getScroll(sourceScrollIndex, targetScrollIndex) { - if (sourceScrollIndex < targetScrollIndex) { - return -_.sum( - _.map(_.range(sourceScrollIndex, targetScrollIndex - sourceScrollIndex), x => this.getSizeByScrollIndex(x)) - ); - } else { - return _.sum( - _.map(_.range(targetScrollIndex, sourceScrollIndex - targetScrollIndex), x => this.getSizeByScrollIndex(x)) - ); - } - } - modelIndexIsInScrollArea(modelIndex) { - let realIndex = this.modelToReal(modelIndex); - return realIndex >= this.frozenCount; - } - getTotalScrollSizeSum() { - let scrollSizeOverrides = _.map( - _.filter(this.intKeys(this.sizeOverridesByModelIndex), x => this.modelIndexIsInScrollArea(x)), - x => this.sizeOverridesByModelIndex[x] - ); - return _.sum(scrollSizeOverrides) + (this.count - scrollSizeOverrides.length) * this.defaultSize; - } - getVisibleScrollSizeSum() { - let scrollSizeOverrides = _.map( - _.filter(this.intKeys(this.sizeOverridesByModelIndex), x => !_.includes(this.hiddenAndFrozenModelIndexes, x)), - x => this.sizeOverridesByModelIndex[x] - ); - return ( - _.sum(scrollSizeOverrides) + - (this.count - this.hiddenModelIndexes.length - scrollSizeOverrides.length) * this.defaultSize - ); - } - intKeys(value) { - return _.keys(value).map(x => _.parseInt(x)); - } - getPositionByRealIndex(realIndex) { - if (realIndex < 0) return 0; - if (realIndex < this.frozenCount) return this.frozenItems[realIndex].position; - return this.getPositionByScrollIndex(realIndex - this.frozenCount); - } - getPositionByScrollIndex(scrollIndex) { - let order = _.sortedIndex(this.scrollIndexes, scrollIndex); - if (this.scrollIndexes[order] == scrollIndex) return this.scrollItems[order].position; - order--; - if (order < 0) return scrollIndex * this.defaultSize; - return ( - this.scrollItems[order].endPosition + (scrollIndex - this.scrollItems[order].scrollIndex - 1) * this.defaultSize - ); - } - getVisibleScrollCount(firstVisibleIndex, viewportSize) { - let res = 0; - let index = firstVisibleIndex; - let count = 0; - while (res < viewportSize && index <= this.scrollCount) { - // console.log('this.getSizeByScrollIndex(index)', this.getSizeByScrollIndex(index)); - res += this.getSizeByScrollIndex(index); - index++; - count++; - } - // console.log('getVisibleScrollCount', firstVisibleIndex, viewportSize, count); - return count; - } - getVisibleScrollCountReversed(lastVisibleIndex, viewportSize) { - let res = 0; - let index = lastVisibleIndex; - let count = 0; - while (res < viewportSize && index >= 0) { - res += this.getSizeByScrollIndex(index); - index--; - count++; - } - return count; - } - invalidateAfterScroll(oldFirstVisible, newFirstVisible, invalidate, viewportSize) { - if (newFirstVisible > oldFirstVisible) { - let oldVisibleCount = this.getVisibleScrollCount(oldFirstVisible, viewportSize); - let newVisibleCount = this.getVisibleScrollCount(newFirstVisible, viewportSize); - for (let i = oldFirstVisible + oldVisibleCount - 1; i <= newFirstVisible + newVisibleCount; i++) { - invalidate(i + this.frozenCount); - } - } else { - for (let i = newFirstVisible; i <= oldFirstVisible; i++) { - invalidate(i + this.frozenCount); - } - } - } - isWholeInView(firstVisibleIndex, index, viewportSize) { - let res = 0; - let testedIndex = firstVisibleIndex; - while (res < viewportSize && testedIndex < this.count) { - res += this.getSizeByScrollIndex(testedIndex); - if (testedIndex == index) return res <= viewportSize; - testedIndex++; - } - return false; - } - scrollInView(firstVisibleIndex, scrollIndex, viewportSize) { - if (this.isWholeInView(firstVisibleIndex, scrollIndex, viewportSize)) { - return firstVisibleIndex; - } - if (scrollIndex < firstVisibleIndex) { - return scrollIndex; - } - let res = 0; - let testedIndex = scrollIndex; - while (res < viewportSize && testedIndex >= 0) { - let size = this.getSizeByScrollIndex(testedIndex); - if (res + size > viewportSize) return testedIndex + 1; - testedIndex--; - res += size; - } - if (res >= viewportSize && testedIndex < scrollIndex) return testedIndex + 1; - return firstVisibleIndex; - } - resize(realIndex, newSize) { - if (realIndex < 0) return; - let modelIndex = this.realToModel(realIndex); - if (modelIndex < 0) return; - this.sizeOverridesByModelIndex[modelIndex] = newSize; - this.buildIndex(); - } - setExtraordinaryIndexes(hidden, frozen) { - //this._hiddenAndFrozenModelIndexes = _.clone(hidden); - hidden = hidden.filter(x => x >= 0); - frozen = frozen.filter(x => x >= 0); - - hidden.sort((a, b) => a - b); - frozen.sort((a, b) => a - b); - this.frozenModelIndexes = _.filter(frozen, x => !_.includes(hidden, x)); - this.hiddenModelIndexes = _.filter(hidden, x => !_.includes(frozen, x)); - this.hiddenAndFrozenModelIndexes = _.concat(hidden, this.frozenModelIndexes); - this.frozenModelIndexes.sort((a, b) => a - b); - if (this.hiddenAndFrozenModelIndexes.length == 0) this.hiddenAndFrozenModelIndexes = null; - if (this.frozenModelIndexes.length == 0) this.frozenModelIndexes = null; - this.buildIndex(); - } - realToModel(realIndex) { - if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return realIndex; - if (realIndex < 0) return -1; - if (realIndex < this.frozenCount && this.frozenModelIndexes != null) return this.frozenModelIndexes[realIndex]; - if (this.hiddenAndFrozenModelIndexes == null) return realIndex; - realIndex -= this.frozenCount; - for (let hidItem of this.hiddenAndFrozenModelIndexes) { - if (realIndex < hidItem) return realIndex; - realIndex++; - } - return realIndex; - } - modelToReal(modelIndex) { - if (this.hiddenAndFrozenModelIndexes == null && this.frozenModelIndexes == null) return modelIndex; - if (modelIndex < 0) return -1; - let frozenIndex = this.frozenModelIndexes != null ? _.indexOf(this.frozenModelIndexes, modelIndex) : -1; - if (frozenIndex >= 0) return frozenIndex; - if (this.hiddenAndFrozenModelIndexes == null) return modelIndex; - let hiddenIndex = _.sortedIndex(this.hiddenAndFrozenModelIndexes, modelIndex); - if (this.hiddenAndFrozenModelIndexes[hiddenIndex] == modelIndex) return -1; - if (hiddenIndex >= 0) return modelIndex - hiddenIndex + this.frozenCount; - return modelIndex; - } - getFrozenPosition(frozenIndex) { - return this.frozenItems[frozenIndex].position; - } - hasSizeOverride(modelIndex) { - return _.has(this.sizeOverridesByModelIndex, modelIndex); - } - isVisible(testedRealIndex, firstVisibleScrollIndex, viewportSize) { - if (testedRealIndex < 0) return false; - if (testedRealIndex >= 0 && testedRealIndex < this.frozenCount) return true; - let scrollIndex = testedRealIndex - this.frozenCount; - let onPageIndex = scrollIndex - firstVisibleScrollIndex; - return onPageIndex >= 0 && onPageIndex < this.getVisibleScrollCount(firstVisibleScrollIndex, viewportSize); - } -} diff --git a/packages/web/src/datagrid/SqlDataGridCore.js b/packages/web/src/datagrid/SqlDataGridCore.js deleted file mode 100644 index fd3fc60af..000000000 --- a/packages/web/src/datagrid/SqlDataGridCore.js +++ /dev/null @@ -1,177 +0,0 @@ -import React from 'react'; -import axios from '../utility/axios'; -import { useSetOpenedTabs } from '../utility/globalState'; -import DataGridCore from './DataGridCore'; -import useSocket from '../utility/SocketProvider'; -import useShowModal from '../modals/showModal'; -import ImportExportModal from '../modals/ImportExportModal'; -import { changeSetToSql, createChangeSet, getChangeSetInsertedRows } from 'dbgate-datalib'; -import LoadingDataGridCore from './LoadingDataGridCore'; -import ChangeSetGrider from './ChangeSetGrider'; -import { scriptToSql } from 'dbgate-sqltree'; -import useModalState from '../modals/useModalState'; -import ConfirmSqlModal from '../modals/ConfirmSqlModal'; -import ErrorMessageModal from '../modals/ErrorMessageModal'; -import useOpenNewTab from '../utility/useOpenNewTab'; - -/** @param props {import('./types').DataGridProps} */ -async function loadDataPage(props, offset, limit) { - const { display, conid, database } = props; - - const sql = display.getPageQuery(offset, limit); - - const response = await axios.request({ - url: 'database-connections/query-data', - method: 'post', - params: { - conid, - database, - }, - data: { sql }, - }); - - if (response.data.errorMessage) return response.data; - return response.data.rows; -} - -function dataPageAvailable(props) { - const { display } = props; - const sql = display.getPageQuery(0, 1); - return !!sql; -} - -async function loadRowCount(props) { - const { display, conid, database } = props; - - const sql = display.getCountQuery(); - - const response = await axios.request({ - url: 'database-connections/query-data', - method: 'post', - params: { - conid, - database, - }, - data: { sql }, - }); - - return parseInt(response.data.rows[0].count); -} - -/** @param props {import('./types').DataGridProps} */ -export default function SqlDataGridCore(props) { - const { conid, database, display, changeSetState, dispatchChangeSet } = props; - const showModal = useShowModal(); - const openNewTab = useOpenNewTab(); - - const confirmSqlModalState = useModalState(); - const [confirmSql, setConfirmSql] = React.useState(''); - - const changeSet = changeSetState && changeSetState.value; - const changeSetRef = React.useRef(changeSet); - changeSetRef.current = changeSet; - - function exportGrid() { - const initialValues = {}; - initialValues.sourceStorageType = 'query'; - initialValues.sourceConnectionId = conid; - initialValues.sourceDatabaseName = database; - initialValues.sourceSql = display.getExportQuery(); - initialValues.sourceList = display.baseTable ? [display.baseTable.pureName] : []; - showModal(modalState => ); - } - function openActiveChart() { - openNewTab( - { - title: 'Chart #', - icon: 'img chart', - tabComponent: 'ChartTab', - props: { - conid, - database, - }, - }, - { - editor: { - config: { chartType: 'bar' }, - sql: display.getExportQuery(select => { - select.orderBy = null; - }), - }, - } - ); - } - function openQuery() { - openNewTab( - { - title: 'Query #', - icon: 'img sql-file', - tabComponent: 'QueryTab', - props: { - schemaName: display.baseTable.schemaName, - pureName: display.baseTable.pureName, - conid, - database, - }, - }, - { - editor: display.getExportQuery(), - } - ); - } - - function handleSave() { - const script = changeSetToSql(changeSetRef.current, display.dbinfo); - const sql = scriptToSql(display.driver, script); - setConfirmSql(sql); - confirmSqlModalState.open(); - } - - async function handleConfirmSql() { - const resp = await axios.request({ - url: 'database-connections/query-data', - method: 'post', - params: { - conid, - database, - }, - data: { sql: confirmSql }, - }); - const { errorMessage } = resp.data || {}; - if (errorMessage) { - showModal(modalState => ( - - )); - } else { - dispatchChangeSet({ type: 'reset', value: createChangeSet() }); - setConfirmSql(null); - display.reload(); - } - } - - // const grider = React.useMemo(()=>new ChangeSetGrider()) - - return ( - <> - - - - ); -} diff --git a/packages/web/src/datagrid/SqlDataGridCore.svelte b/packages/web/src/datagrid/SqlDataGridCore.svelte new file mode 100644 index 000000000..e57d35548 --- /dev/null +++ b/packages/web/src/datagrid/SqlDataGridCore.svelte @@ -0,0 +1,123 @@ + + + + + diff --git a/packages/web/src/datagrid/TableDataGrid.js b/packages/web/src/datagrid/TableDataGrid.js deleted file mode 100644 index e7ab591e8..000000000 --- a/packages/web/src/datagrid/TableDataGrid.js +++ /dev/null @@ -1,232 +0,0 @@ -import React from 'react'; -import _ from 'lodash'; -import DataGrid from './DataGrid'; -import styled from 'styled-components'; -import { TableGridDisplay, TableFormViewDisplay, createGridConfig, createGridCache } from 'dbgate-datalib'; -import { getFilterValueExpression } from 'dbgate-filterparser'; -import { findEngineDriver } from 'dbgate-tools'; -import { useConnectionInfo, getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders'; -import useSocket from '../utility/SocketProvider'; -import { VerticalSplitter } from '../widgets/Splitter'; -import stableStringify from 'json-stable-stringify'; -import ReferenceHeader from './ReferenceHeader'; -import SqlDataGridCore from './SqlDataGridCore'; -import useExtensions from '../utility/useExtensions'; -import SqlFormView from '../formview/SqlFormView'; - -const ReferenceContainer = styled.div` - position: absolute; - display: flex; - flex-direction: column; - top: 0; - left: 0; - right: 0; - bottom: 0; -`; - -const ReferenceGridWrapper = styled.div` - position: relative; - flex: 1; - display: flex; -`; - -export default function TableDataGrid({ - conid, - database, - schemaName, - pureName, - tabVisible, - toolbarPortalRef, - changeSetState, - dispatchChangeSet, - config = undefined, - setConfig = undefined, - cache = undefined, - setCache = undefined, - masterLoadedTime = undefined, - isDetailView = false, -}) { - // const [childConfig, setChildConfig] = React.useState(createGridConfig()); - const [myCache, setMyCache] = React.useState(createGridCache()); - const [childCache, setChildCache] = React.useState(createGridCache()); - const [refReloadToken, setRefReloadToken] = React.useState(0); - const [myLoadedTime, setMyLoadedTime] = React.useState(0); - const extensions = useExtensions(); - - const { childConfig } = config; - const setChildConfig = (value, reference = undefined) => { - if (_.isFunction(value)) { - setConfig(x => ({ - ...x, - childConfig: value(x.childConfig), - })); - } else { - setConfig(x => ({ - ...x, - childConfig: value, - reference: reference === undefined ? x.reference : reference, - })); - } - }; - const { reference } = config; - - const connection = useConnectionInfo({ conid }); - const dbinfo = useDatabaseInfo({ conid, database }); - // const [reference, setReference] = React.useState(null); - - function createDisplay() { - return connection - ? new TableGridDisplay( - { schemaName, pureName }, - findEngineDriver(connection, extensions), - config, - setConfig, - cache || myCache, - setCache || setMyCache, - dbinfo - ) - : null; - } - - function createFormDisplay() { - return connection - ? new TableFormViewDisplay( - { schemaName, pureName }, - findEngineDriver(connection, extensions), - config, - setConfig, - cache || myCache, - setCache || setMyCache, - dbinfo - ) - : null; - } - - const [display, setDisplay] = React.useState(createDisplay()); - const [formDisplay, setFormDisplay] = React.useState(createFormDisplay()); - - React.useEffect(() => { - setRefReloadToken(v => v + 1); - if (!reference && display && display.isGrouped) display.clearGrouping(); - }, [reference]); - - React.useEffect(() => { - const newDisplay = createDisplay(); - if (!newDisplay) return; - if (display && display.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return; - setDisplay(newDisplay); - }, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]); - - React.useEffect(() => { - const newDisplay = createFormDisplay(); - if (!newDisplay) return; - if (formDisplay && formDisplay.isLoadedCorrectly && !newDisplay.isLoadedCorrectly) return; - setFormDisplay(newDisplay); - }, [connection, config, cache || myCache, conid, database, schemaName, pureName, dbinfo, extensions]); - - const handleDatabaseStructureChanged = React.useCallback(() => { - (setCache || setMyCache)(createGridCache()); - }, []); - - const socket = useSocket(); - - React.useEffect(() => { - if (display && !display.isLoadedCorrectly) { - if (conid && socket) { - socket.on(`database-structure-changed-${conid}-${database}`, handleDatabaseStructureChanged); - return () => { - socket.off(`database-structure-changed-${conid}-${database}`, handleDatabaseStructureChanged); - }; - } - } - }, [conid, database, display]); - - const handleReferenceSourceChanged = React.useCallback( - (selectedRows, loadedTime) => { - setMyLoadedTime(loadedTime); - if (!reference) return; - - const filtersBase = display && display.isGrouped ? config.filters : childConfig.filters; - - const filters = { - ...filtersBase, - ..._.fromPairs( - reference.columns.map(col => [ - col.refName, - selectedRows.map(x => getFilterValueExpression(x[col.baseName], col.dataType)).join(', '), - ]) - ), - }; - if (stableStringify(filters) != stableStringify(childConfig.filters)) { - setChildConfig(cfg => ({ - ...cfg, - filters, - })); - setChildCache(ca => ({ - ...ca, - refreshTime: new Date().getTime(), - })); - } - }, - [childConfig, reference] - ); - - const handleCloseReference = () => { - setChildConfig(null, null); - }; - - if (!display) return null; - - return ( - - setChildConfig(createGridConfig(), reference)} - onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null} - refReloadToken={refReloadToken.toString()} - masterLoadedTime={masterLoadedTime} - GridCore={SqlDataGridCore} - FormView={SqlFormView} - isDetailView={isDetailView} - // tableInfo={ - // dbinfo && dbinfo.tables && dbinfo.tables.find((x) => x.pureName == pureName && x.schemaName == schemaName) - // } - /> - {reference && ( - - - - - - - )} - - ); -} diff --git a/packages/web/src/datagrid/TableDataGrid.svelte b/packages/web/src/datagrid/TableDataGrid.svelte new file mode 100644 index 000000000..d36db7df3 --- /dev/null +++ b/packages/web/src/datagrid/TableDataGrid.svelte @@ -0,0 +1,169 @@ + + + + + { + if (value && value.referenceId && reference && reference.referenceId == value.referenceId) { + // reference not changed + return; + } + setChildConfig(createGridConfig(), value); + }} + /> + +
+ {#if reference} + +
+ {#key `${reference.schemaName}.${reference.pureName}`} + + {/key} +
+ {/if} +
+
+ + diff --git a/packages/web/src/datagrid/VerticalScrollBar.svelte b/packages/web/src/datagrid/VerticalScrollBar.svelte new file mode 100644 index 000000000..b30f5a6c7 --- /dev/null +++ b/packages/web/src/datagrid/VerticalScrollBar.svelte @@ -0,0 +1,42 @@ + + +
+
 
+
+ + diff --git a/packages/web/src/datagrid/gridutil.ts b/packages/web/src/datagrid/gridutil.ts index bf64c2a94..d1bce1f50 100644 --- a/packages/web/src/datagrid/gridutil.ts +++ b/packages/web/src/datagrid/gridutil.ts @@ -89,7 +89,7 @@ export function countVisibleRealColumns(columnSizes, firstVisibleColumnScrollInd const visibleRealColumnIndexes = []; const modelIndexes = {}; - /** @type {(import('dbgate-datalib').DisplayColumn & {widthPx: string; colIndex: number})[]} */ + /** @type {(import('dbgate-datalib').DisplayColumn & {width: number; colIndex: number})[]} */ const realColumns = []; // frozen columns @@ -112,12 +112,11 @@ export function countVisibleRealColumns(columnSizes, firstVisibleColumnScrollInd let col = columns[modelColumnIndex]; if (!col) continue; - const widthNumber = columnSizes.getSizeByRealIndex(colIndex); + const width = columnSizes.getSizeByRealIndex(colIndex); realColumns.push({ ...col, colIndex, - widthNumber, - widthPx: `${widthNumber}px`, + width, }); } return realColumns; diff --git a/packages/web/src/designer/ColumnLine.svelte b/packages/web/src/designer/ColumnLine.svelte new file mode 100644 index 000000000..013801dc2 --- /dev/null +++ b/packages/web/src/designer/ColumnLine.svelte @@ -0,0 +1,144 @@ + + +
{ + const dragData = { + ...column, + designerId, + }; + sourceDragColumn$.set(dragData); + e.dataTransfer.setData('designer_column_drag_data', JSON.stringify(dragData)); + }} + on:dragend={e => { + sourceDragColumn$.set(null); + targetDragColumn$.set(null); + }} + on:dragover={e => { + if ($sourceDragColumn$) { + e.preventDefault(); + targetDragColumn$.set({ + ...column, + designerId, + }); + } + }} + on:drop={e => { + var data = e.dataTransfer.getData('designer_column_drag_data'); + e.preventDefault(); + if (!data) return; + onCreateReference($sourceDragColumn$, $targetDragColumn$); + sourceDragColumn$.set(null); + targetDragColumn$.set(null); + }} + class:isDragSource={$sourceDragColumn$ && + $sourceDragColumn$.designerId == designerId && + $sourceDragColumn$.columnName == column.columnName} + class:isDragTarget={$targetDragColumn$ && + $targetDragColumn$.designerId == designerId && + $targetDragColumn$.columnName == column.columnName} + on:mousedown={e => + onSelectColumn({ + ...column, + designerId, + })} + use:contextMenu={createMenu} +> + x.designerId == designerId && x.columnName == column.columnName && x.isOutput + )} + on:change={e => { + if (e.target.checked) { + onChangeColumn( + { + ...column, + designerId, + }, + col => ({ ...col, isOutput: true }) + ); + } else { + onChangeColumn( + { + ...column, + designerId, + }, + col => ({ ...col, isOutput: false }) + ); + } + }} + /> + + {#if designerColumn?.filter} + + {/if} + {#if designerColumn?.sortOrder > 0} + + {/if} + {#if designerColumn?.sortOrder < 0} + + {/if} + {#if designerColumn?.isGrouped} + + {/if} +
+ + diff --git a/packages/web/src/designer/Designer.js b/packages/web/src/designer/Designer.js deleted file mode 100644 index 076799aac..000000000 --- a/packages/web/src/designer/Designer.js +++ /dev/null @@ -1,352 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import DesignerTable from './DesignerTable'; -import uuidv1 from 'uuid/v1'; -import _ from 'lodash'; -import useTheme from '../theme/useTheme'; -import DesignerReference from './DesignerReference'; -import cleanupDesignColumns from './cleanupDesignColumns'; -import { isConnectedByReference } from './designerTools'; -import { getTableInfo } from '../utility/metadataLoaders'; - -const Wrapper = styled.div` - flex: 1; - background-color: ${props => props.theme.designer_background}; - overflow: scroll; -`; - -const Canvas = styled.div` - width: 3000px; - height: 3000px; - position: relative; -`; - -const EmptyInfo = styled.div` - margin: 50px; - font-size: 20px; -`; - -function fixPositions(tables) { - const minLeft = _.min(tables.map(x => x.left)); - const minTop = _.min(tables.map(x => x.top)); - if (minLeft < 0 || minTop < 0) { - const dLeft = minLeft < 0 ? -minLeft : 0; - const dTop = minTop < 0 ? -minTop : 0; - return tables.map(tbl => ({ - ...tbl, - left: tbl.left + dLeft, - top: tbl.top + dTop, - })); - } - return tables; -} - -export default function Designer({ value, onChange, conid, database }) { - const { tables, references } = value || {}; - const theme = useTheme(); - - const [sourceDragColumn, setSourceDragColumn] = React.useState(null); - const [targetDragColumn, setTargetDragColumn] = React.useState(null); - const domTablesRef = React.useRef({}); - const wrapperRef = React.useRef(); - const [changeToken, setChangeToken] = React.useState(0); - - const handleDrop = e => { - var data = e.dataTransfer.getData('app_object_drag_data'); - e.preventDefault(); - if (!data) return; - const rect = e.target.getBoundingClientRect(); - var json = JSON.parse(data); - const { objectTypeField } = json; - if (objectTypeField != 'tables' && objectTypeField != 'views') return; - json.designerId = uuidv1(); - json.left = e.clientX - rect.left; - json.top = e.clientY - rect.top; - - onChange(current => { - const foreignKeys = _.compact([ - ...(json.foreignKeys || []).map(fk => { - const tables = ((current || {}).tables || []).filter( - tbl => fk.refTableName == tbl.pureName && fk.refSchemaName == tbl.schemaName - ); - if (tables.length == 1) - return { - ...fk, - sourceId: json.designerId, - targetId: tables[0].designerId, - }; - return null; - }), - ..._.flatten( - ((current || {}).tables || []).map(tbl => - (tbl.foreignKeys || []).map(fk => { - if (fk.refTableName == json.pureName && fk.refSchemaName == json.schemaName) { - return { - ...fk, - sourceId: tbl.designerId, - targetId: json.designerId, - }; - } - return null; - }) - ) - ), - ]); - - return { - ...current, - tables: [...((current || {}).tables || []), json], - references: - foreignKeys.length == 1 - ? [ - ...((current || {}).references || []), - { - designerId: uuidv1(), - sourceId: foreignKeys[0].sourceId, - targetId: foreignKeys[0].targetId, - joinType: 'INNER JOIN', - columns: foreignKeys[0].columns.map(col => ({ - source: col.columnName, - target: col.refColumnName, - })), - }, - ] - : (current || {}).references, - }; - }); - }; - - const changeTable = React.useCallback( - table => { - onChange(current => ({ - ...current, - tables: fixPositions((current.tables || []).map(x => (x.designerId == table.designerId ? table : x))), - })); - }, - [onChange] - ); - - const bringToFront = React.useCallback( - table => { - onChange( - current => ({ - ...current, - tables: [...(current.tables || []).filter(x => x.designerId != table.designerId), table], - }), - true - ); - }, - [onChange] - ); - - const removeTable = React.useCallback( - table => { - onChange(current => ({ - ...current, - tables: (current.tables || []).filter(x => x.designerId != table.designerId), - references: (current.references || []).filter( - x => x.sourceId != table.designerId && x.targetId != table.designerId - ), - columns: (current.columns || []).filter(x => x.designerId != table.designerId), - })); - }, - [onChange] - ); - - const changeReference = React.useCallback( - ref => { - onChange(current => ({ - ...current, - references: (current.references || []).map(x => (x.designerId == ref.designerId ? ref : x)), - })); - }, - [onChange] - ); - - const removeReference = React.useCallback( - ref => { - onChange(current => ({ - ...current, - references: (current.references || []).filter(x => x.designerId != ref.designerId), - })); - }, - [onChange] - ); - - const handleCreateReference = (source, target) => { - onChange(current => { - const existingReference = (current.references || []).find( - x => - (x.sourceId == source.designerId && x.targetId == target.designerId) || - (x.sourceId == target.designerId && x.targetId == source.designerId) - ); - - return { - ...current, - references: existingReference - ? current.references.map(ref => - ref == existingReference - ? { - ...existingReference, - columns: [ - ...existingReference.columns, - existingReference.sourceId == source.designerId - ? { - source: source.columnName, - target: target.columnName, - } - : { - source: target.columnName, - target: source.columnName, - }, - ], - } - : ref - ) - : [ - ...(current.references || []), - { - designerId: uuidv1(), - sourceId: source.designerId, - targetId: target.designerId, - joinType: isConnectedByReference(current, source, target, null) ? 'CROSS JOIN' : 'INNER JOIN', - columns: [ - { - source: source.columnName, - target: target.columnName, - }, - ], - }, - ], - }; - }); - }; - - const handleAddReferenceByColumn = async (designerId, foreignKey) => { - const toTable = await getTableInfo({ - conid, - database, - pureName: foreignKey.refTableName, - schemaName: foreignKey.refSchemaName, - }); - const newTableDesignerId = uuidv1(); - onChange(current => { - const fromTable = (current.tables || []).find(x => x.designerId == designerId); - if (!fromTable) return; - return { - ...current, - tables: [ - ...(current.tables || []), - { - ...toTable, - left: fromTable.left + 300, - top: fromTable.top + 50, - designerId: newTableDesignerId, - }, - ], - references: [ - ...(current.references || []), - { - designerId: uuidv1(), - sourceId: fromTable.designerId, - targetId: newTableDesignerId, - joinType: 'INNER JOIN', - columns: foreignKey.columns.map(col => ({ - source: col.columnName, - target: col.refColumnName, - })), - }, - ], - }; - }); - }; - - const handleSelectColumn = React.useCallback( - column => { - onChange( - current => ({ - ...current, - columns: (current.columns || []).find( - x => x.designerId == column.designerId && x.columnName == column.columnName - ) - ? current.columns - : [...cleanupDesignColumns(current.columns), _.pick(column, ['designerId', 'columnName'])], - }), - true - ); - }, - [onChange] - ); - - const handleChangeColumn = React.useCallback( - (column, changeFunc) => { - onChange(current => { - const currentColumns = (current || {}).columns || []; - const existing = currentColumns.find( - x => x.designerId == column.designerId && x.columnName == column.columnName - ); - if (existing) { - return { - ...current, - columns: currentColumns.map(x => (x == existing ? changeFunc(existing) : x)), - }; - } else { - return { - ...current, - columns: [ - ...cleanupDesignColumns(currentColumns), - changeFunc(_.pick(column, ['designerId', 'columnName'])), - ], - }; - } - }); - }, - [onChange] - ); - - // React.useEffect(() => { - // setTimeout(() => setChangeToken((x) => x + 1), 100); - // }, [value]); - - return ( - - {(tables || []).length == 0 && Drag & drop tables or views from left panel here} - e.preventDefault()} onDrop={handleDrop} ref={wrapperRef}> - {(references || []).map(ref => ( - - ))} - {(tables || []).map(table => ( - { - domTablesRef.current[table.designerId] = table; - }} - designer={value} - /> - ))} - - - ); -} diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte new file mode 100644 index 000000000..fa51475d5 --- /dev/null +++ b/packages/web/src/designer/Designer.svelte @@ -0,0 +1,352 @@ + + + + +
+ {#if !(tables?.length > 0)} +
Drag & drop tables or views from left panel here
+ {/if} + +
e.preventDefault()} on:drop={handleDrop}> + {#each references || [] as ref (ref.designerId)} + + {/each} + + + {#each tables || [] as table (table.designerId)} + + {/each} +
+
+ + diff --git a/packages/web/src/designer/DesignerReference.js b/packages/web/src/designer/DesignerReference.js deleted file mode 100644 index 60a37bfdf..000000000 --- a/packages/web/src/designer/DesignerReference.js +++ /dev/null @@ -1,177 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import DomTableRef from './DomTableRef'; -import _ from 'lodash'; -import useTheme from '../theme/useTheme'; -import { useShowMenu } from '../modals/showMenu'; -import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu'; -import { isConnectedByReference } from './designerTools'; - -const StyledSvg = styled.svg` - position: absolute; - left: 0; - top: 0; - right: 0; - bottom: 0; - width: 100%; - height: 100%; -`; - -const ReferenceWrapper = styled.div` - position: absolute; - border: 1px solid ${props => props.theme.designer_line}; - background-color: ${props => props.theme.designer_background}; - z-index: 900; - border-radius: 10px; - width: 32px; - height: 32px; -`; - -const ReferenceText = styled.span` - position: relative; - float: left; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 900; - white-space: nowrap; - background-color: ${props => props.theme.designer_background}; -`; - -function ReferenceContextMenu({ remove, setJoinType, isConnected }) { - return ( - <> - Remove - {!isConnected && ( - <> - - setJoinType('INNER JOIN')}>Set INNER JOIN - setJoinType('LEFT JOIN')}>Set LEFT JOIN - setJoinType('RIGHT JOIN')}>Set RIGHT JOIN - setJoinType('FULL OUTER JOIN')}>Set FULL OUTER JOIN - setJoinType('CROSS JOIN')}>Set CROSS JOIN - setJoinType('WHERE EXISTS')}>Set WHERE EXISTS - setJoinType('WHERE NOT EXISTS')}>Set WHERE NOT EXISTS - - )} - - ); -} - -export default function DesignerReference({ - domTablesRef, - reference, - changeToken, - onRemoveReference, - onChangeReference, - designer, -}) { - const { designerId, sourceId, targetId, columns, joinType } = reference; - const theme = useTheme(); - const showMenu = useShowMenu(); - const domTables = domTablesRef.current; - /** @type {DomTableRef} */ - const sourceTable = domTables[sourceId]; - /** @type {DomTableRef} */ - const targetTable = domTables[targetId]; - if (!sourceTable || !targetTable) return null; - const sourceRect = sourceTable.getRect(); - const targetRect = targetTable.getRect(); - if (!sourceRect || !targetRect) return null; - - const buswi = 10; - const extwi = 25; - - const possibilities = []; - possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.left - buswi, dirdst: -1 }); - possibilities.push({ xsrc: sourceRect.left - buswi, dirsrc: -1, xdst: targetRect.right + buswi, dirdst: 1 }); - possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.left - buswi, dirdst: -1 }); - possibilities.push({ xsrc: sourceRect.right + buswi, dirsrc: 1, xdst: targetRect.right + buswi, dirdst: 1 }); - - let minpos = _.minBy(possibilities, p => Math.abs(p.xsrc - p.xdst)); - - let srcY = _.mean(columns.map(x => sourceTable.getColumnY(x.source))); - let dstY = _.mean(columns.map(x => targetTable.getColumnY(x.target))); - - if (columns.length == 0) { - srcY = sourceTable.getColumnY(''); - dstY = targetTable.getColumnY(''); - } - - const src = { x: minpos.xsrc, y: srcY }; - const dst = { x: minpos.xdst, y: dstY }; - - const lineStyle = { fill: 'none', stroke: theme.designer_line, strokeWidth: 2 }; - - const handleContextMenu = event => { - event.preventDefault(); - showMenu( - event.pageX, - event.pageY, - onRemoveReference({ designerId })} - isConnected={isConnectedByReference(designer, { designerId: sourceId }, { designerId: targetId }, reference)} - setJoinType={joinType => { - onChangeReference({ - ...reference, - joinType, - }); - }} - /> - ); - }; - - return ( - <> - - - {columns.map((col, colIndex) => { - let y1 = sourceTable.getColumnY(col.source); - let y2 = targetTable.getColumnY(col.target); - return ( - - - - - ); - })} - - - - {_.snakeCase(joinType || 'CROSS JOIN') - .replace('_', '\xa0') - .replace('_', '\xa0')} - - - - ); -} diff --git a/packages/web/src/designer/DesignerReference.svelte b/packages/web/src/designer/DesignerReference.svelte new file mode 100644 index 000000000..4349fadc5 --- /dev/null +++ b/packages/web/src/designer/DesignerReference.svelte @@ -0,0 +1,172 @@ + + +{#if src && dst && minpos} + + + {#each columnsY as coly} + + + {/each} + + +
+
+ {_.snakeCase(reference?.joinType || 'CROSS JOIN') + .replace('_', '\xa0') + .replace('_', '\xa0')} +
+
+{/if} + + diff --git a/packages/web/src/designer/DesignerTable.js b/packages/web/src/designer/DesignerTable.js deleted file mode 100644 index e9b996aa7..000000000 --- a/packages/web/src/designer/DesignerTable.js +++ /dev/null @@ -1,413 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import { findForeignKeyForColumn } from 'dbgate-tools'; -import ColumnLabel from '../datagrid/ColumnLabel'; -import { FontIcon } from '../icons'; -import useTheme from '../theme/useTheme'; -import DomTableRef from './DomTableRef'; -import _ from 'lodash'; -import { CheckboxField } from '../utility/inputs'; -import { useShowMenu } from '../modals/showMenu'; -import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu'; -import useShowModal from '../modals/showModal'; -import InputTextModal from '../modals/InputTextModal'; - -const Wrapper = styled.div` - position: absolute; - // background-color: white; - background-color: ${props => props.theme.designtable_background}; - border: 1px solid ${props => props.theme.border}; -`; - -const Header = styled.div` - font-weight: bold; - text-align: center; - padding: 2px; - background: ${props => - // @ts-ignore - props.objectTypeField == 'views' - ? props.theme.designtable_background_magenta[2] - : props.theme.designtable_background_blue[2]}; - border-bottom: 1px solid ${props => props.theme.border}; - cursor: pointer; - display: flex; - justify-content: space-between; -`; - -const ColumnsWrapper = styled.div` - max-height: 400px; - overflow-y: auto; - width: calc(100% - 10px); - padding: 5px; -`; - -const HeaderLabel = styled.div``; - -const CloseWrapper = styled.div` - ${props => - ` - background-color: ${props.theme.toolbar_background} ; - - &:hover { - background-color: ${props.theme.toolbar_background2} ; - } - - &:active:hover { - background-color: ${props.theme.toolbar_background3}; - } - `} -`; - -// &:hover { -// background-color: ${(props) => props.theme.designtable_background_gold[1]}; -// } - -const ColumnLine = styled.div` - ${props => - // @ts-ignore - !props.isDragSource && - // @ts-ignore - !props.isDragTarget && - ` - &:hover { - background-color: ${props.theme.designtable_background_gold[1]}; - } - `} - - ${props => - // @ts-ignore - props.isDragSource && - ` - background-color: ${props.theme.designtable_background_cyan[2]}; - `} - - ${props => - // @ts-ignore - props.isDragTarget && - ` - background-color: ${props.theme.designtable_background_cyan[2]}; - `} -`; - -function TableContextMenu({ remove, setTableAlias, removeTableAlias }) { - return ( - <> - Remove - - Set table alias - {!!removeTableAlias && Remove table alias} - - ); -} - -function ColumnContextMenu({ setSortOrder, addReference }) { - return ( - <> - setSortOrder(1)}>Sort ascending - setSortOrder(-1)}>Sort descending - setSortOrder(0)}>Unsort - {!!addReference && Add reference} - - ); -} - -function ColumnDesignerIcons({ column, designerId, designer }) { - const designerColumn = (designer.columns || []).find( - x => x.designerId == designerId && x.columnName == column.columnName - ); - if (!designerColumn) return null; - return ( - <> - {!!designerColumn.filter && } - {designerColumn.sortOrder > 0 && } - {designerColumn.sortOrder < 0 && } - {!!designerColumn.isGrouped && } - - ); -} - -export default function DesignerTable({ - table, - onChangeTable, - onBringToFront, - onRemoveTable, - onCreateReference, - onAddReferenceByColumn, - onSelectColumn, - onChangeColumn, - sourceDragColumn, - setSourceDragColumn, - targetDragColumn, - setTargetDragColumn, - onChangeDomTable, - wrapperRef, - setChangeToken, - designer, -}) { - const { pureName, columns, left, top, designerId, alias, objectTypeField } = table; - const [movingPosition, setMovingPosition] = React.useState(null); - const movingPositionRef = React.useRef(null); - const theme = useTheme(); - const domObjectsRef = React.useRef({}); - const showMenu = useShowMenu(); - const showModal = useShowModal(); - - const moveStartXRef = React.useRef(null); - const moveStartYRef = React.useRef(null); - - const handleMove = React.useCallback(e => { - let diffX = e.clientX - moveStartXRef.current; - let diffY = e.clientY - moveStartYRef.current; - moveStartXRef.current = e.clientX; - moveStartYRef.current = e.clientY; - - movingPositionRef.current = { - left: (movingPositionRef.current.left || 0) + diffX, - top: (movingPositionRef.current.top || 0) + diffY, - }; - setMovingPosition(movingPositionRef.current); - // setChangeToken((x) => x + 1); - changeTokenDebounced.current(); - // onChangeTable( - // { - // ...props, - // left: (left || 0) + diffX, - // top: (top || 0) + diffY, - // }, - // index - // ); - }, []); - - const changeTokenDebounced = React.useRef( - // @ts-ignore - _.debounce(() => setChangeToken(x => x + 1), 100) - ); - - const handleMoveEnd = React.useCallback( - e => { - if (movingPositionRef.current) { - onChangeTable({ - ...table, - left: movingPositionRef.current.left, - top: movingPositionRef.current.top, - }); - } - - movingPositionRef.current = null; - setMovingPosition(null); - changeTokenDebounced.current(); - // setChangeToken((x) => x + 1); - - // this.props.model.fixPositions(); - - // this.props.designer.changedModel(true); - }, - [onChangeTable, table] - ); - - React.useEffect(() => { - if (movingPosition) { - document.addEventListener('mousemove', handleMove, true); - document.addEventListener('mouseup', handleMoveEnd, true); - return () => { - document.removeEventListener('mousemove', handleMove, true); - document.removeEventListener('mouseup', handleMoveEnd, true); - }; - } - }, [movingPosition == null, handleMove, handleMoveEnd]); - - const headerMouseDown = React.useCallback( - e => { - e.preventDefault(); - moveStartXRef.current = e.clientX; - moveStartYRef.current = e.clientY; - movingPositionRef.current = { left, top }; - setMovingPosition(movingPositionRef.current); - // setIsMoving(true); - }, - [handleMove, handleMoveEnd] - ); - - const dispatchDomColumn = (columnName, dom) => { - domObjectsRef.current[columnName] = dom; - onChangeDomTable(new DomTableRef(table, domObjectsRef.current, wrapperRef.current)); - changeTokenDebounced.current(); - }; - - const handleSetTableAlias = () => { - showModal(modalState => ( - { - onChangeTable({ - ...table, - alias: newAlias, - }); - }} - /> - )); - }; - - const handleHeaderContextMenu = event => { - event.preventDefault(); - showMenu( - event.pageX, - event.pageY, - onRemoveTable({ designerId })} - setTableAlias={handleSetTableAlias} - removeTableAlias={ - alias - ? () => - onChangeTable({ - ...table, - alias: null, - }) - : null - } - /> - ); - }; - - const handleColumnContextMenu = column => event => { - event.preventDefault(); - const foreignKey = findForeignKeyForColumn(table, column); - showMenu( - event.pageX, - event.pageY, - { - onChangeColumn( - { - ...column, - designerId, - }, - col => ({ ...col, sortOrder }) - ); - }} - addReference={ - foreignKey - ? () => { - onAddReferenceByColumn(designerId, foreignKey); - } - : null - } - /> - ); - }; - - return ( - onBringToFront(table)} - ref={dom => dispatchDomColumn('', dom)} - > -
- {alias || pureName} - onRemoveTable(table)} theme={theme}> - - -
- - {(columns || []).map(column => ( - dispatchDomColumn(column.columnName, dom)} - // @ts-ignore - isDragSource={ - sourceDragColumn && - sourceDragColumn.designerId == designerId && - sourceDragColumn.columnName == column.columnName - } - // @ts-ignore - isDragTarget={ - targetDragColumn && - targetDragColumn.designerId == designerId && - targetDragColumn.columnName == column.columnName - } - onDragStart={e => { - const dragData = { - ...column, - designerId, - }; - setSourceDragColumn(dragData); - e.dataTransfer.setData('designer_column_drag_data', JSON.stringify(dragData)); - }} - onDragEnd={e => { - setTargetDragColumn(null); - setSourceDragColumn(null); - }} - onDragOver={e => { - if (sourceDragColumn) { - e.preventDefault(); - setTargetDragColumn({ - ...column, - designerId, - }); - } - }} - onDrop={e => { - var data = e.dataTransfer.getData('designer_column_drag_data'); - e.preventDefault(); - if (!data) return; - onCreateReference(sourceDragColumn, targetDragColumn); - setTargetDragColumn(null); - setSourceDragColumn(null); - }} - onMouseDown={e => - onSelectColumn({ - ...column, - designerId, - }) - } - > - x.designerId == designerId && x.columnName == column.columnName && x.isOutput - ) - } - onChange={e => { - if (e.target.checked) { - onChangeColumn( - { - ...column, - designerId, - }, - col => ({ ...col, isOutput: true }) - ); - } else { - onChangeColumn( - { - ...column, - designerId, - }, - col => ({ ...col, isOutput: false }) - ); - } - }} - /> - - - - ))} - -
- ); -} diff --git a/packages/web/src/designer/DesignerTable.svelte b/packages/web/src/designer/DesignerTable.svelte new file mode 100644 index 000000000..02cee0805 --- /dev/null +++ b/packages/web/src/designer/DesignerTable.svelte @@ -0,0 +1,177 @@ + + +
onBringToFront(table)} +> +
+
{alias || pureName}
+
onRemoveTable(table)}> + +
+
+
+ {#each columns || [] as column} + + {/each} +
+
+ + diff --git a/packages/web/src/designer/QueryDesignColumns.js b/packages/web/src/designer/QueryDesignColumns.js deleted file mode 100644 index 7ac113f96..000000000 --- a/packages/web/src/designer/QueryDesignColumns.js +++ /dev/null @@ -1,165 +0,0 @@ -import React from 'react'; -import DataFilterControl from '../datagrid/DataFilterControl'; -import { CheckboxField, SelectField, TextField } from '../utility/inputs'; -import TableControl, { TableColumn } from '../utility/TableControl'; -import InlineButton from '../widgets/InlineButton'; -import { findDesignerFilterType } from './designerTools'; - -function getTableDisplayName(column, tables) { - const table = (tables || []).find(x => x.designerId == column.designerId); - if (table) return table.alias || table.pureName; - return ''; -} - -export default function QueryDesignColumns({ value, onChange }) { - const { columns, tables } = value || {}; - - const changeColumn = React.useCallback( - col => { - onChange(current => ({ - ...current, - columns: (current.columns || []).map(x => - x.designerId == col.designerId && x.columnName == col.columnName ? col : x - ), - })); - }, - [onChange] - ); - - const removeColumn = React.useCallback( - col => { - onChange(current => ({ - ...current, - columns: (current.columns || []).filter(x => x.designerId != col.designerId || x.columnName != col.columnName), - })); - }, - [onChange] - ); - - const hasGroupedColumn = !!(columns || []).find(x => x.isGrouped); - - return ( - - - getTableDisplayName(row, tables)} /> - ( - { - if (e.target.checked) changeColumn({ ...row, isOutput: true }); - else changeColumn({ ...row, isOutput: false }); - }} - /> - )} - /> - ( - { - changeColumn({ ...row, alias: e.target.value }); - }} - /> - )} - /> - - ( - { - if (e.target.checked) changeColumn({ ...row, isGrouped: true }); - else changeColumn({ ...row, isGrouped: false }); - }} - /> - )} - /> - - !row.isGrouped && ( - { - changeColumn({ ...row, aggregate: e.target.value }); - }} - > - - - - - - - - - ) - } - /> - ( - { - changeColumn({ ...row, sortOrder: parseInt(e.target.value) }); - }} - > - - - - - - - , - - )} - /> - ( - { - changeColumn({ ...row, filter }); - }} - /> - )} - /> - {hasGroupedColumn && ( - ( - { - changeColumn({ ...row, groupFilter }); - }} - /> - )} - /> - )} - ( - <> - removeColumn(row)}>Remove - - )} - /> - - ); -} diff --git a/packages/web/src/designer/QueryDesignToolbar.js b/packages/web/src/designer/QueryDesignToolbar.js deleted file mode 100644 index c53092e69..000000000 --- a/packages/web/src/designer/QueryDesignToolbar.js +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import ToolbarButton from '../widgets/ToolbarButton'; - -export default function QueryDesignToolbar({ - execute, - isDatabaseDefined, - busy, - modelState, - dispatchModel, - isConnected, - kill, -}) { - return ( - <> - - Execute - - - Kill - - dispatchModel({ type: 'undo' })} icon="icon undo"> - Undo - - dispatchModel({ type: 'redo' })} icon="icon redo"> - Redo - - - ); -} diff --git a/packages/web/src/designer/QueryDesigner.js b/packages/web/src/designer/QueryDesigner.js deleted file mode 100644 index 031fe40ed..000000000 --- a/packages/web/src/designer/QueryDesigner.js +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import styled from 'styled-components'; -import Designer from './Designer'; - -export default function QueryDesigner({ value, conid, database, engine, onChange }) { - return ; -} diff --git a/packages/web/src/designer/QueryDesigner.svelte b/packages/web/src/designer/QueryDesigner.svelte new file mode 100644 index 000000000..782816e47 --- /dev/null +++ b/packages/web/src/designer/QueryDesigner.svelte @@ -0,0 +1,5 @@ + + + diff --git a/packages/web/src/elements/ColumnLabel.svelte b/packages/web/src/elements/ColumnLabel.svelte new file mode 100644 index 000000000..931f88cab --- /dev/null +++ b/packages/web/src/elements/ColumnLabel.svelte @@ -0,0 +1,46 @@ + + + + + + {#if icon} + + {/if} + {headerText || columnName} + {#if extInfo} + {extInfo} + {/if} + + + diff --git a/packages/web/src/elements/ConstraintLabel.svelte b/packages/web/src/elements/ConstraintLabel.svelte new file mode 100644 index 000000000..cedec2ce9 --- /dev/null +++ b/packages/web/src/elements/ConstraintLabel.svelte @@ -0,0 +1,17 @@ + + + + + {constraintName} + diff --git a/packages/web/src/elements/DropDownButton.svelte b/packages/web/src/elements/DropDownButton.svelte new file mode 100644 index 000000000..e60c92f69 --- /dev/null +++ b/packages/web/src/elements/DropDownButton.svelte @@ -0,0 +1,22 @@ + + + + + diff --git a/packages/web/src/elements/ErrorInfo.svelte b/packages/web/src/elements/ErrorInfo.svelte new file mode 100644 index 000000000..a2c21c7fc --- /dev/null +++ b/packages/web/src/elements/ErrorInfo.svelte @@ -0,0 +1,38 @@ + + +{#if isSmall} +
+ + {message} +
+{:else} +
+
+ +
+ {message} +
+{/if} + + diff --git a/packages/web/src/elements/ForeignKeyObjectListControl.svelte b/packages/web/src/elements/ForeignKeyObjectListControl.svelte new file mode 100644 index 000000000..4ca6a70ce --- /dev/null +++ b/packages/web/src/elements/ForeignKeyObjectListControl.svelte @@ -0,0 +1,43 @@ + + + + + {row?.columns.map(x => x.columnName).join(', ')} + {row?.columns.map(x => x.refColumnName).join(', ')} + diff --git a/packages/web/src/elements/FormStyledButton.svelte b/packages/web/src/elements/FormStyledButton.svelte new file mode 100644 index 000000000..501dcdbb5 --- /dev/null +++ b/packages/web/src/elements/FormStyledButton.svelte @@ -0,0 +1,38 @@ + + + + + diff --git a/packages/web/src/elements/FormStyledButtonLikeLabel.svelte b/packages/web/src/elements/FormStyledButtonLikeLabel.svelte new file mode 100644 index 000000000..4adec94fe --- /dev/null +++ b/packages/web/src/elements/FormStyledButtonLikeLabel.svelte @@ -0,0 +1,30 @@ + + + + + diff --git a/packages/web/src/elements/HorizontalSplitter.svelte b/packages/web/src/elements/HorizontalSplitter.svelte new file mode 100644 index 000000000..ec58177e1 --- /dev/null +++ b/packages/web/src/elements/HorizontalSplitter.svelte @@ -0,0 +1,59 @@ + + + + +
+
+ +
+ {#if isSplitter} +
(size += e.detail)} /> +
+ +
+ {/if} +
+ + diff --git a/packages/web/src/elements/InlineButton.svelte b/packages/web/src/elements/InlineButton.svelte new file mode 100644 index 000000000..2ea2a9088 --- /dev/null +++ b/packages/web/src/elements/InlineButton.svelte @@ -0,0 +1,59 @@ + + +
+
+ +
+
+ + diff --git a/packages/web/src/elements/LargeButton.svelte b/packages/web/src/elements/LargeButton.svelte new file mode 100644 index 000000000..d92f35abd --- /dev/null +++ b/packages/web/src/elements/LargeButton.svelte @@ -0,0 +1,54 @@ + + +
+
+ +
+
+ +
+
+ + diff --git a/packages/web/src/elements/Link.svelte b/packages/web/src/elements/Link.svelte new file mode 100644 index 000000000..ec5a49053 --- /dev/null +++ b/packages/web/src/elements/Link.svelte @@ -0,0 +1,28 @@ + + +{#if electron} + electron.shell.openExternal(href)}> + + +{:else} + + + +{/if} + + diff --git a/packages/web/src/elements/LoadingInfo.svelte b/packages/web/src/elements/LoadingInfo.svelte new file mode 100644 index 000000000..cef57d80a --- /dev/null +++ b/packages/web/src/elements/LoadingInfo.svelte @@ -0,0 +1,54 @@ + + +{#if wrapper} +
+
+
+
+ +
+ {message} +
+
+
+{:else} +
+
+ +
+ {message} +
+{/if} + + diff --git a/packages/web/src/elements/ManagerInnerContainer.svelte b/packages/web/src/elements/ManagerInnerContainer.svelte new file mode 100644 index 000000000..33fedb63a --- /dev/null +++ b/packages/web/src/elements/ManagerInnerContainer.svelte @@ -0,0 +1,15 @@ + + +
+ +
+ + diff --git a/packages/web/src/elements/Markdown.svelte b/packages/web/src/elements/Markdown.svelte new file mode 100644 index 000000000..31b5770b0 --- /dev/null +++ b/packages/web/src/elements/Markdown.svelte @@ -0,0 +1,8 @@ + + + diff --git a/packages/web/src/elements/ObjectListControl.svelte b/packages/web/src/elements/ObjectListControl.svelte new file mode 100644 index 000000000..f6fbe692e --- /dev/null +++ b/packages/web/src/elements/ObjectListControl.svelte @@ -0,0 +1,78 @@ + + +{#if collection?.length > 0 || showIfEmpty} +
+
+ {title} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +{/if} diff --git a/packages/web/src/elements/QueryDesignColumns.svelte b/packages/web/src/elements/QueryDesignColumns.svelte new file mode 100644 index 000000000..1ab79fa9e --- /dev/null +++ b/packages/web/src/elements/QueryDesignColumns.svelte @@ -0,0 +1,136 @@ + + + + + getTableDisplayName(row, tables) }, + { fieldName: 'isOutput', header: 'Output', slot: 0 }, + { fieldName: 'alias', header: 'Alias', slot: 1 }, + { fieldName: 'isGrouped', header: 'Group by', slot: 2 }, + { fieldName: 'aggregate', header: 'Aggregate', slot: 3 }, + { fieldName: 'sortOrder', header: 'Sort order', slot: 4 }, + { fieldName: 'filter', header: 'Filter', slot: 5 }, + hasGroupedColumn && { fieldName: 'groupFilter', header: 'Group filter', slot: 6 }, + { fieldName: 'actions', header: '', slot: 7 }, + ]} +> + + { + if (e.target.checked) changeColumn({ ...row, isOutput: true }); + else changeColumn({ ...row, isOutput: false }); + }} + /> + + + { + changeColumn({ ...row, alias: e.target.value }); + }} + /> + + + { + if (e.target.checked) changeColumn({ ...row, isGrouped: true }); + else changeColumn({ ...row, isGrouped: false }); + }} + /> + + + {#if !row.isGrouped} + { + changeColumn({ ...row, aggregate: e.detail }); + }} + options={['---', 'MIN', 'MAX', 'COUNT', 'COUNT DISTINCT', 'SUM', 'AVG'].map(x => ({ label: x, value: x }))} + /> + {/if} + + + { + changeColumn({ ...row, sortOrder: parseInt(e.detail) }); + }} + options={[ + { label: '---', value: '0' }, + { label: '1st, ascending', value: '1' }, + { label: '1st, descending', value: '-1' }, + { label: '2nd, ascending', value: '2' }, + { label: '2nd, descending', value: '-2' }, + { label: '3rd, ascending', value: '3' }, + { label: '3rd, descending', value: '-3' }, + ]} + /> + + + { + changeColumn({ ...row, filter }); + }} + /> + + + { + changeColumn({ ...row, groupFilter }); + }} + /> + + + removeColumn(row)}>Remove + + diff --git a/packages/web/src/elements/SearchBoxWrapper.svelte b/packages/web/src/elements/SearchBoxWrapper.svelte new file mode 100644 index 000000000..d71019413 --- /dev/null +++ b/packages/web/src/elements/SearchBoxWrapper.svelte @@ -0,0 +1,9 @@ +
+ + diff --git a/packages/web/src/elements/SearchInput.svelte b/packages/web/src/elements/SearchInput.svelte new file mode 100644 index 000000000..19b931210 --- /dev/null +++ b/packages/web/src/elements/SearchInput.svelte @@ -0,0 +1,32 @@ + + + domInput.select()} +/> + + diff --git a/packages/web/src/elements/TabControl.svelte b/packages/web/src/elements/TabControl.svelte new file mode 100644 index 000000000..47950df8c --- /dev/null +++ b/packages/web/src/elements/TabControl.svelte @@ -0,0 +1,105 @@ + + +
+
+ {#each _.compact(tabs) as tab, index} +
(value = index)}> + + {tab.label} + +
+ {/each} +
+ +
+ {#each _.compact(tabs) as tab, index} +
+ + {#if tab.slot != null} + {#if tab.slot == 0} + {:else if tab.slot == 1} + {:else if tab.slot == 2} + {:else if tab.slot == 3} + {:else if tab.slot == 4} + {:else if tab.slot == 5} + {:else if tab.slot == 6} + {:else if tab.slot == 7} + {/if} + {/if} +
+ {/each} +
+
+ + diff --git a/packages/web/src/elements/TableControl.svelte b/packages/web/src/elements/TableControl.svelte new file mode 100644 index 000000000..a9691fc5a --- /dev/null +++ b/packages/web/src/elements/TableControl.svelte @@ -0,0 +1,118 @@ + + + + + + + + {#each columnList as col} + + {/each} + + + + {#each rows as row, index} + { + if (selectable) { + selectedIndex = index; + domTable.focus(); + } + }} + > + {#each columnList as col} + + {/each} + + {/each} + +
{col.header}
+ {#if col.component} + + {:else if col.formatter} + {col.formatter(row)} + {:else if col.slot != null} + {#if col.slot == -1} + {:else if col.slot == 0} + {:else if col.slot == 1} + {:else if col.slot == 2} + {:else if col.slot == 3} + {:else if col.slot == 4} + {:else if col.slot == 5} + {:else if col.slot == 6} + {:else if col.slot == 7} + {/if} + {:else} + {row[col.fieldName] || ''} + {/if} +
+ + diff --git a/packages/web/src/elements/VerticalSplitter.svelte b/packages/web/src/elements/VerticalSplitter.svelte new file mode 100644 index 000000000..6caea4d3a --- /dev/null +++ b/packages/web/src/elements/VerticalSplitter.svelte @@ -0,0 +1,52 @@ + + +
+
+ +
+ {#if isSplitter} +
(size += e.detail)} /> +
+ +
+ {/if} +
+ + diff --git a/packages/web/src/forms/CheckboxField.svelte b/packages/web/src/forms/CheckboxField.svelte new file mode 100644 index 000000000..301fff3c4 --- /dev/null +++ b/packages/web/src/forms/CheckboxField.svelte @@ -0,0 +1 @@ + diff --git a/packages/web/src/forms/FormArchiveFilesSelect.svelte b/packages/web/src/forms/FormArchiveFilesSelect.svelte new file mode 100644 index 000000000..caa9f77eb --- /dev/null +++ b/packages/web/src/forms/FormArchiveFilesSelect.svelte @@ -0,0 +1,39 @@ + + +
+ +
+ setFieldValue(name, _.uniq([...($values[name] || []), ...($files && $files.map(x => x.name))]))} + /> + setFieldValue(name, [])} /> +
+
+ + diff --git a/packages/web/src/forms/FormArchiveFolderSelect.svelte b/packages/web/src/forms/FormArchiveFolderSelect.svelte new file mode 100644 index 000000000..313c63103 --- /dev/null +++ b/packages/web/src/forms/FormArchiveFolderSelect.svelte @@ -0,0 +1,52 @@ + + + diff --git a/packages/web/src/forms/FormArgument.svelte b/packages/web/src/forms/FormArgument.svelte new file mode 100644 index 000000000..6882a78cd --- /dev/null +++ b/packages/web/src/forms/FormArgument.svelte @@ -0,0 +1,27 @@ + + +{#if arg.type == 'text'} + +{:else if arg.type == 'checkbox'} + +{:else if arg.type == 'select'} + + _.isString(opt) ? { label: opt, value: opt } : { label: opt.name, value: opt.value } + )} + /> +{/if} diff --git a/packages/web/src/forms/FormArgumentList.svelte b/packages/web/src/forms/FormArgumentList.svelte new file mode 100644 index 000000000..c191ad833 --- /dev/null +++ b/packages/web/src/forms/FormArgumentList.svelte @@ -0,0 +1,12 @@ + + +
+ {#each args as arg (arg.name)} + + {/each} +
diff --git a/packages/web/src/forms/FormButton.svelte b/packages/web/src/forms/FormButton.svelte new file mode 100644 index 000000000..22ddf3495 --- /dev/null +++ b/packages/web/src/forms/FormButton.svelte @@ -0,0 +1,15 @@ + + + diff --git a/packages/web/src/forms/FormCheckboxField.svelte b/packages/web/src/forms/FormCheckboxField.svelte new file mode 100644 index 000000000..652d806b1 --- /dev/null +++ b/packages/web/src/forms/FormCheckboxField.svelte @@ -0,0 +1,22 @@ + + + setFieldValue(name, !$values[name]) }} +> + + diff --git a/packages/web/src/forms/FormCheckboxFieldRaw.svelte b/packages/web/src/forms/FormCheckboxFieldRaw.svelte new file mode 100644 index 000000000..b381e071e --- /dev/null +++ b/packages/web/src/forms/FormCheckboxFieldRaw.svelte @@ -0,0 +1,13 @@ + + + diff --git a/packages/web/src/forms/FormElectronFileSelector.svelte b/packages/web/src/forms/FormElectronFileSelector.svelte new file mode 100644 index 000000000..2a0639d6d --- /dev/null +++ b/packages/web/src/forms/FormElectronFileSelector.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/web/src/forms/FormElectronFileSelectorRaw.svelte b/packages/web/src/forms/FormElectronFileSelectorRaw.svelte new file mode 100644 index 000000000..e3993ca5a --- /dev/null +++ b/packages/web/src/forms/FormElectronFileSelectorRaw.svelte @@ -0,0 +1,29 @@ + + +
+ + Browse +
diff --git a/packages/web/src/forms/FormFieldTemplateLarge.svelte b/packages/web/src/forms/FormFieldTemplateLarge.svelte new file mode 100644 index 000000000..4a5abb9b4 --- /dev/null +++ b/packages/web/src/forms/FormFieldTemplateLarge.svelte @@ -0,0 +1,32 @@ + + +
+ {#if type == 'checkbox'} + + {label} + {:else} +
+ {label} +
+ + {/if} +
+ + diff --git a/packages/web/src/forms/FormFieldTemplateRow.svelte b/packages/web/src/forms/FormFieldTemplateRow.svelte new file mode 100644 index 000000000..1afdacc4b --- /dev/null +++ b/packages/web/src/forms/FormFieldTemplateRow.svelte @@ -0,0 +1,20 @@ + + +
+
{label}
+
+
+ + diff --git a/packages/web/src/forms/FormFieldTemplateTiny.svelte b/packages/web/src/forms/FormFieldTemplateTiny.svelte new file mode 100644 index 000000000..dff6b6486 --- /dev/null +++ b/packages/web/src/forms/FormFieldTemplateTiny.svelte @@ -0,0 +1,40 @@ + + +
+ {#if type == 'checkbox'} + + {label} + {:else} +
+
+ {label} +
+
+
+ +
+ {/if} +
+ + diff --git a/packages/web/src/forms/FormPasswordField.svelte b/packages/web/src/forms/FormPasswordField.svelte new file mode 100644 index 000000000..6f5e2b137 --- /dev/null +++ b/packages/web/src/forms/FormPasswordField.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/web/src/forms/FormPasswordFieldRaw.svelte b/packages/web/src/forms/FormPasswordFieldRaw.svelte new file mode 100644 index 000000000..9b0b6f14a --- /dev/null +++ b/packages/web/src/forms/FormPasswordFieldRaw.svelte @@ -0,0 +1,34 @@ + + +
+ setFieldValue(name, e.target['value'])} + placeholder={isCrypted ? '(Password is encrypted)' : undefined} + type={isCrypted || showPassword ? 'text' : 'password'} + /> + {#if !isCrypted} + (showPassword = !showPassword)} {disabled}> + + + {/if} +
diff --git a/packages/web/src/forms/FormProvider.svelte b/packages/web/src/forms/FormProvider.svelte new file mode 100644 index 000000000..073f26da7 --- /dev/null +++ b/packages/web/src/forms/FormProvider.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/web/src/forms/FormProviderCore.svelte b/packages/web/src/forms/FormProviderCore.svelte new file mode 100644 index 000000000..bb76456f1 --- /dev/null +++ b/packages/web/src/forms/FormProviderCore.svelte @@ -0,0 +1,43 @@ + + + + + + + diff --git a/packages/web/src/forms/FormRadioGroupItem.svelte b/packages/web/src/forms/FormRadioGroupItem.svelte new file mode 100644 index 000000000..905a9416b --- /dev/null +++ b/packages/web/src/forms/FormRadioGroupItem.svelte @@ -0,0 +1,19 @@ + + +
+ setFieldValue(name, value)} + /> + setFieldValue(name, value)}>{text} +
diff --git a/packages/web/src/forms/FormSelectField.svelte b/packages/web/src/forms/FormSelectField.svelte new file mode 100644 index 000000000..a97a8b4c4 --- /dev/null +++ b/packages/web/src/forms/FormSelectField.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/packages/web/src/forms/FormSelectFieldRaw.svelte b/packages/web/src/forms/FormSelectFieldRaw.svelte new file mode 100644 index 000000000..1ff62105c --- /dev/null +++ b/packages/web/src/forms/FormSelectFieldRaw.svelte @@ -0,0 +1,22 @@ + + + { + setFieldValue(name, e.detail); + dispatch('change', e.detail); + }} + {isClearable} +/> diff --git a/packages/web/src/forms/FormSubmit.svelte b/packages/web/src/forms/FormSubmit.svelte new file mode 100644 index 000000000..d7643970a --- /dev/null +++ b/packages/web/src/forms/FormSubmit.svelte @@ -0,0 +1,20 @@ + + + diff --git a/packages/web/src/forms/FormTextField.svelte b/packages/web/src/forms/FormTextField.svelte new file mode 100644 index 000000000..dbcdfb9d9 --- /dev/null +++ b/packages/web/src/forms/FormTextField.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/packages/web/src/forms/FormTextFieldRaw.svelte b/packages/web/src/forms/FormTextFieldRaw.svelte new file mode 100644 index 000000000..6e148ceec --- /dev/null +++ b/packages/web/src/forms/FormTextFieldRaw.svelte @@ -0,0 +1,10 @@ + + + setFieldValue(name, e.target['value'])} /> diff --git a/packages/web/src/forms/LargeFormButton.svelte b/packages/web/src/forms/LargeFormButton.svelte new file mode 100644 index 000000000..4369b5103 --- /dev/null +++ b/packages/web/src/forms/LargeFormButton.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/web/src/forms/SelectField.svelte b/packages/web/src/forms/SelectField.svelte new file mode 100644 index 000000000..f09a07788 --- /dev/null +++ b/packages/web/src/forms/SelectField.svelte @@ -0,0 +1,57 @@ + + +{#if isNative} + +{:else} + options.find(x => x.value == item)) + : options.find(x => x.value == value)} + on:select={e => { + if (isMulti) { + dispatch( + 'change', + e.detail?.map(x => x.value) + ); + } else { + dispatch('change', e.detail.value); + } + }} + showIndicator={!isMulti} + isClearable={isMulti} + {isMulti} + bind:listOpen + bind:isFocused + /> +{/if} diff --git a/packages/web/src/forms/TextAreaField.svelte b/packages/web/src/forms/TextAreaField.svelte new file mode 100644 index 000000000..f20711775 --- /dev/null +++ b/packages/web/src/forms/TextAreaField.svelte @@ -0,0 +1,12 @@ + + +