From 18ea670558dafff8c168e4c66e7cb9435995f69a Mon Sep 17 00:00:00 2001 From: Matthew Mulholland Date: Mon, 1 Jul 2019 09:30:02 +1000 Subject: [PATCH] Closes #206: Ensure locked, not toggled for import. Add Tab behaviour to lock. Separated logic from main's url to frictionless for easier re-use. Added submenu items for both file and url. Improved style to suit open. Allowed title to change. Closes #742. Closes #731. Added Import Column Properties to file disable/enable list. Closes #924: Enabled difference between case-insensitive (with diacritics) and case-sensitive (without diacritics). --- package.json | 1 - src/main/file.js | 26 +++- src/main/loadFrictionless.js | 115 ++++++++++++++ src/main/menu.js | 22 ++- src/main/menuUtils.js | 4 +- src/main/rendererToMain.js | 4 +- src/main/url.js | 141 +++--------------- src/main/windows.js | 10 +- src/renderer/components/Home.vue | 17 ++- src/renderer/components/KeyboardHelp.vue | 5 - src/renderer/components/UrlDialog.vue | 43 ++++-- src/renderer/hot.js | 33 ++-- src/renderer/lockProperties.js | 5 + src/renderer/partials/FindReplace.vue | 137 ++++++++++------- src/urldialog.ejs | 3 +- static/css/url-dialog.styl | 18 ++- .../file/open-comma-separated-file.feature | 1 + .../support/page-objects/dimensions.js | 3 + yarn.lock | 9 +- 19 files changed, 353 insertions(+), 244 deletions(-) create mode 100644 src/main/loadFrictionless.js diff --git a/package.json b/package.json index c4fad63d3..6386d8b62 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,6 @@ "vue-async-computed": "^3.3.1", "vue-directive-tooltip": "^1.4.5", "vue-electron": "^1.0.6", - "vue-focus": "^2.1.0", "vue-good-table": "^2.15.3", "vue-router": "^3.0.1", "vue-rx": "^5.0.0", diff --git a/src/main/file.js b/src/main/file.js index 78f0d865e..1f1c3d220 100644 --- a/src/main/file.js +++ b/src/main/file.js @@ -3,6 +3,7 @@ import Fs from 'fs' import { createWindowTabWithFormattedDataFile, focusMainWindow } from './windows' import _ from 'lodash' import { disableOpenFileItems, enableOpenFileItems } from './menuUtils.js' +import { loadResourceSchemaFromJson } from './loadFrictionless' export function saveFileAs(format) { let currentWindow = focusMainWindow() @@ -43,7 +44,7 @@ export function saveFile() { currentWindow.webContents.send('saveData', currentWindow.format, global.tab.activeFilename) } -export function importDataPackage() { +export function importDataPackageFromFile() { disableOpenFileItems() let window = focusMainWindow() Dialog.showOpenDialog({ @@ -62,7 +63,28 @@ export function importDataPackage() { if (_.isArray(filename)) { filename = filename[0] } - window.webContents.send('importDataPackage', filename) + window.webContents.send('importDataPackageFromFile', filename) + }) +} + +export function importTableResourceSchemaFromFile() { + let window = focusMainWindow() + Dialog.showOpenDialog({ + filters: [ + { + name: '*', + extensions: ['json'] + } + ], + properties: ['openFile'] + }, function(filename) { + if (filename === undefined) { + return + } + if (_.isArray(filename)) { + filename = filename[0] + } + loadResourceSchemaFromJson(filename) }) } diff --git a/src/main/loadFrictionless.js b/src/main/loadFrictionless.js new file mode 100644 index 000000000..865f40b28 --- /dev/null +++ b/src/main/loadFrictionless.js @@ -0,0 +1,115 @@ +// datapackage-js does not support loading url in browser +// atm, 'types' are only : Data Package or Resource Schema +import { focusMainWindow } from './windows' +import _ from 'lodash' +import { dataResourceToFormat } from '../renderer/file-formats' +import { dialog } from 'electron' +import { Package } from 'datapackage' +import { Schema } from 'tableschema' + +export async function loadPackageFromJson (json) { + const mainWindow = focusMainWindow() + const dataPackageJson = await loadGenericFrictionlessFromJsonSource(json, loadPackageJson, 'Data Package') + try { + await loadResources(dataPackageJson, mainWindow) + } catch (error) { + console.error('There was a problem loading package from json', error) + } +} + +export async function loadResourceSchemaFromJson (json) { + try { + const resourceSchema = await loadGenericFrictionlessFromJsonSource(json, loadTableResourceSchemaJson, 'Table Resource Schema') + const mainWindow = focusMainWindow() + mainWindow.webContents.send('closeLoadingScreen') + if (resourceSchema && resourceSchema.descriptor) { + const mainWindow = focusMainWindow() + mainWindow.webContents.send('addSchemaToTabAndLock', resourceSchema.descriptor) + } + } catch (error) { + console.error('There was a problem loading resource schema from json', error) + } +} + +async function loadGenericFrictionlessFromJsonSource (jsonSource, callback, frictionlessType) { + const mainWindow = focusMainWindow() + mainWindow.webContents.send('closeAndshowLoadingScreen', `Loading ${frictionlessType}..`) + const frictionlessTypeJson = await callback(jsonSource, mainWindow) + if (!frictionlessTypeJson) { + dialog.showMessageBox(mainWindow, { + type: 'warning', + title: `Unable to load ${frictionlessType}`, + message: + `The ${frictionlessType}, ${jsonSource}, could not be loaded. + If the ${frictionlessType} is a URL or file, please check that it exists in JSON format.` + }) + mainWindow.webContents.send('closeLoadingScreen') + return + } + if (!frictionlessTypeJson.valid) { + showInvalidMessage(jsonSource, mainWindow, frictionlessType) + mainWindow.webContents.send('closeLoadingScreen') + return + } + return frictionlessTypeJson +} + +function showInvalidMessage (source, mainWindow, frictionlessType) { + dialog.showMessageBox(mainWindow, { + type: 'warning', + title: `Invalid ${frictionlessType}`, + message: + `The ${frictionlessType}, at ${source}, is not valid. Please refer to + https://frictionlessdata.io/specs/ + for more information.` + }) +} + +export async function loadPackageJson (source) { + try { + const dataPackage = await Package.load(source) + return dataPackage + } catch (error) { + console.error(`There was a problem loading the package: ${source}`, error) + } +} + +async function loadResources (dataPackageJson) { + const mainWindow = focusMainWindow() + let packageProperties = _.assign({}, dataPackageJson.descriptor) + _.unset(packageProperties, 'resources') + mainWindow.webContents.send('resetPackagePropertiesToObject', packageProperties) + for (const resource of dataPackageJson.resourceNames) { + mainWindow.webContents.send('closeAndshowLoadingScreen', 'Loading next resource...') + const dataResource = dataPackageJson.getResource(resource) + const format = dataResourceToFormat(dataResource.descriptor) + let data = await dataResource.read() + mainWindow.webContents.send('closeLoadingScreen') + // datapackage-js separates headers - add back to use default DC behaviour + let dataWithHeaders = _.concat([dataResource.headers], data) + mainWindow.webContents.send('addTabWithFormattedDataAndDescriptor', dataWithHeaders, format, dataResource.descriptor) + } +} + +async function loadTableResourceSchemaJson (source) { + try { + const resourceSchema = await Schema.load(source) + return resourceSchema + } catch (error) { + console.error(`There was a problem loading the table resource schema: ${source}`, error) + } +} + +export async function loadResourceDataFromPackageSource (source, resourceName) { + const dataPackage = await loadPackageJson(source) + const rowOfObjects = [] + if (dataPackage && _.indexOf(dataPackage.resourceNames, resourceName) > -1) { + const dataResource = dataPackage.getResource(resourceName) + const data = await dataResource.read() + const headers = dataResource.headers + for (const row of data) { + rowOfObjects.push(_.zipObject(headers, row)) + } + } + return rowOfObjects +} diff --git a/src/main/menu.js b/src/main/menu.js index 338154b6c..d8ba97f83 100644 --- a/src/main/menu.js +++ b/src/main/menu.js @@ -1,4 +1,4 @@ -import { openFile, saveFileAs, saveFile, importDataPackage } from './file.js' +import { openFile, saveFileAs, saveFile, importDataPackageFromFile, importTableResourceSchemaFromFile } from './file.js' import { showUrlDialogForPackage, showUrlDialogForResourceSchema } from './url.js' import { createWindowTab, focusMainWindow } from './windows.js' import { importExcel } from './excel.js' @@ -57,10 +57,20 @@ class AppMenu { type: 'separator' }, { label: 'Import Column Properties', - accelerator: 'Shift+CmdOrCtrl+I', - click () { - showUrlDialogForResourceSchema() - } + submenu: [ + { + label: 'json from URL...', + click () { + showUrlDialogForResourceSchema() + } + }, + { + label: 'json from file...', + click () { + importTableResourceSchemaFromFile() + } + } + ] }, { type: 'separator' }, { @@ -348,7 +358,7 @@ class AppMenu { label: 'zip from file...', enabled: true, click () { - importDataPackage() + importDataPackageFromFile() } }, { label: 'json from URL...', diff --git a/src/main/menuUtils.js b/src/main/menuUtils.js index 55807bcb4..ec283f055 100644 --- a/src/main/menuUtils.js +++ b/src/main/menuUtils.js @@ -70,7 +70,7 @@ export function getSubMenuLabelsFromMenu (menuLabel) { } export function disableOpenFileItems() { - disableMenuItems('File', ['Open Excel Sheet...', 'Open', 'Open Data Package']) + disableMenuItems('File', ['Open Excel Sheet...', 'Open', 'Open Data Package', 'Import Column Properties']) } export function disableMenuItems(menuLabel, subMenuLabels) { @@ -78,7 +78,7 @@ export function disableMenuItems(menuLabel, subMenuLabels) { } export function enableOpenFileItems() { - enableMenuItems('File', ['Open Excel Sheet...', 'Open', 'Open Data Package']) + enableMenuItems('File', ['Open Excel Sheet...', 'Open', 'Open Data Package', 'Import Column Properties']) } export function enableMenuItems(menuLabel, subMenuLabels) { diff --git a/src/main/rendererToMain.js b/src/main/rendererToMain.js index 85f7dfcd2..1bf549e98 100644 --- a/src/main/rendererToMain.js +++ b/src/main/rendererToMain.js @@ -10,7 +10,7 @@ import { disableEnableBasedOnAttributeAndConditionFromLabels } from './menuUtils.js' import { focusMainWindow, closeSecondaryWindow } from './windows.js' -import { loadPackageJson, loadResourceDataFromPackageUrl } from './url.js' +import { loadPackageJson, loadResourceDataFromPackageSource } from './loadFrictionless' ipc.on('toggleSaveMenu', (event, arg) => { let saveSubMenu = getSubMenuFromMenu('File', 'Save') @@ -78,7 +78,7 @@ ipc.on('loadPackageUrl', async function(event, index, hotId, url) { ipc.on('loadPackageUrlResourcesAsFkRelations', async function(event, url, resourceName) { try { - const rows = await loadResourceDataFromPackageUrl(url, resourceName) + const rows = await loadResourceDataFromPackageSource(url, resourceName) event.returnValue = rows } catch (error) { const errorMessage = 'There was a problem collating data from url resources' diff --git a/src/main/url.js b/src/main/url.js index 6a17d3245..e99b0f0c4 100644 --- a/src/main/url.js +++ b/src/main/url.js @@ -1,33 +1,41 @@ +// datapackage-js does not support loading url in browser import axios from 'axios' import fs from 'fs-extra' import path from 'path' -import { ipcMain as ipc, dialog } from 'electron' -import { focusOrNewSecondaryWindow, focusMainWindow, closeWindowSafely } from './windows' +import { dialog, ipcMain as ipc } from 'electron' +import { closeWindowSafely, focusMainWindow, focusOrNewSecondaryWindow } from './windows' import { disableOpenFileItems, enableOpenFileItems } from './menuUtils.js' -import { Package } from 'datapackage' -import { Schema } from 'tableschema' import tmp from 'tmp' import _ from 'lodash' -import { dataResourceToFormat } from '../renderer/file-formats.js' +import { loadPackageFromJson, loadResourceSchemaFromJson } from './loadFrictionless' // auto cleanup tmp.setGracefulCleanup() +const defaultDialogTitle = 'Data Curator - ' + export function showUrlDialogForResourceSchema() { - showUrlDialogForCallback(handleJsonForResourceSchema) + let browserWindow = createUrlDialogWindow('Open Table Resource Schema URL') + processUrlDialogForCallback(browserWindow, handleJsonForResourceSchema) } // TODO: handle errors by rejecting promises and throwing back up stack export function showUrlDialogForPackage() { - showUrlDialogForCallback(handleZipOrJsonForPackage) + let browserWindow = createUrlDialogWindow('Open Data Package URL') + processUrlDialogForCallback(browserWindow, handleZipOrJsonForPackage) } -export function showUrlDialogForCallback(callback, errorMessage='There was a problem loading package or resource(s)') { +function createUrlDialogWindow(titleExtension) { disableOpenFileItems() - let browserWindow = focusOrNewSecondaryWindow('urldialog', { width: 300, height: 150, modal: true, alwaysOnTop: true }) + const fullTitle = titleExtension ? `${defaultDialogTitle} ${titleExtension}` : defaultDialogTitle + let browserWindow = focusOrNewSecondaryWindow('urldialog', { title: fullTitle, width: 450, height: 150, modal: true, alwaysOnTop: true }) browserWindow.on('closed', () => { enableOpenFileItems() }) + return browserWindow +} + +export function processUrlDialogForCallback(browserWindow, callback, errorMessage='There was a problem loading package or resource(s)', dialogCallbacks) { browserWindow.webContents.on('did-finish-load', () => { ipc.once('urlCancelled', () => { closeWindowSafely(browserWindow) @@ -47,7 +55,7 @@ export function showUrlDialogForCallback(callback, errorMessage='There was a pro function handleZipOrJsonForPackage(urlText) { if (_.endsWith(urlText, '.json')) { - loadPackageFromJsonUrl(urlText) + loadPackageFromJson(urlText) } else if (_.endsWith(urlText, '.zip')) { importDataPackageZipFromUrl(urlText) } else { @@ -57,7 +65,7 @@ function handleZipOrJsonForPackage(urlText) { function handleJsonForResourceSchema(urlText) { if (_.endsWith(urlText, '.json')) { - loadResourceSchemaFromJsonUrl(urlText) + loadResourceSchemaFromJson(urlText) } else { showUrlPathNotSupportedMessage(urlText, '".json"') } @@ -106,66 +114,7 @@ export async function importDataPackageZipFromUrl(urlText) { } function handleDownloadedZip(zipPath, mainWindow) { - mainWindow.webContents.send('importDataPackage', zipPath, true) -} - -async function loadPackageFromJsonUrl(urlText) { - const mainWindow = focusMainWindow() - const dataPackageJson = await loadGenericFrictionlessFromJsonUrl(urlText, loadPackageJson, 'Data Package') - try { - await loadResources(dataPackageJson, mainWindow) - } catch (error) { - console.error('There was a problem loading package from json', error) - } -} - -async function loadResourceSchemaFromJsonUrl(urlText) { - try { - const resourceSchema = await loadGenericFrictionlessFromJsonUrl(urlText, loadTableResourceSchemaJson, 'Table Resource Schema') - const mainWindow = focusMainWindow() - mainWindow.webContents.send('closeLoadingScreen') - if (resourceSchema && resourceSchema.descriptor) { - const mainWindow = focusMainWindow() - mainWindow.webContents.send('addSchemaToTabAndLock', resourceSchema.descriptor) - } - } catch (error) { - console.error('There was a problem loading resource schema from json', error) - } -} - -// atm, 'types' are only : Data Package or Resource Schema -async function loadGenericFrictionlessFromJsonUrl(urlText, callback, frictionlessType) { - const mainWindow = focusMainWindow() - mainWindow.webContents.send('closeAndshowLoadingScreen', `Loading ${frictionlessType} URL..`) - const frictionlessTypeJson = await callback(urlText, mainWindow) - if (!frictionlessTypeJson) { - dialog.showMessageBox(mainWindow, { - type: 'warning', - title: `Unable to load ${frictionlessType}`, - message: - `The ${frictionlessType}, ${urlText}, could not be loaded. - If the ${frictionlessType} is a URL, please check that the URL exists.` - }) - mainWindow.webContents.send('closeLoadingScreen') - return - } - if (!frictionlessTypeJson.valid) { - showInvalidMessage(urlText, mainWindow, frictionlessType) - mainWindow.webContents.send('closeLoadingScreen') - return - } - return frictionlessTypeJson -} - -function showInvalidMessage(urlText, mainWindow, frictionlessType) { - dialog.showMessageBox(mainWindow, { - type: 'warning', - title: `Invalid ${frictionlessType}`, - message: - `The ${frictionlessType}, at ${urlText}, is not valid. Please refer to - https://frictionlessdata.io/specs/ - for more information.` - }) + mainWindow.webContents.send('importDataPackageFromFile', zipPath, true) } function showUrlPathNotSupportedMessage(urlText, supportedFileExtensions) { @@ -176,53 +125,3 @@ function showUrlPathNotSupportedMessage(urlText, supportedFileExtensions) { `Data Curator, does not support downloading ${urlText}, as the path does not end in ${supportedFileExtensions}` }) } - -// datapackage-js does not support loading url in browser -export async function loadPackageJson(json) { - try { - const dataPackage = await Package.load(json) - return dataPackage - } catch (error) { - console.error(`There was a problem loading the package: ${json}`, error) - } -} - -async function loadResources(dataPackageJson) { - const mainWindow = focusMainWindow() - let packageProperties = _.assign({}, dataPackageJson.descriptor) - _.unset(packageProperties, 'resources') - mainWindow.webContents.send('resetPackagePropertiesToObject', packageProperties) - for (const resource of dataPackageJson.resourceNames) { - mainWindow.webContents.send('closeAndshowLoadingScreen', 'Loading next resource...') - const dataResource = dataPackageJson.getResource(resource) - const format = dataResourceToFormat(dataResource.descriptor) - let data = await dataResource.read() - mainWindow.webContents.send('closeLoadingScreen') - // datapackage-js separates headers - add back to use default DC behaviour - let dataWithHeaders = _.concat([dataResource.headers], data) - mainWindow.webContents.send('addTabWithFormattedDataAndDescriptor', dataWithHeaders, format, dataResource.descriptor) - } -} - -async function loadTableResourceSchemaJson(json) { - try { - const resourceSchema = await Schema.load(json) - return resourceSchema - } catch (error) { - console.error(`There was a problem loading the table resource schema: ${json}`, error) - } -} - -export async function loadResourceDataFromPackageUrl(url, resourceName) { - const dataPackage = await loadPackageJson(url) - const rowOfObjects = [] - if (dataPackage && _.indexOf(dataPackage.resourceNames, resourceName) > -1) { - const dataResource = dataPackage.getResource(resourceName) - const data = await dataResource.read() - const headers = dataResource.headers - for (const row of data) { - rowOfObjects.push(_.zipObject(headers, row)) - } - } - return rowOfObjects -} diff --git a/src/main/windows.js b/src/main/windows.js index cbb88150d..5ec5d5f87 100644 --- a/src/main/windows.js +++ b/src/main/windows.js @@ -49,10 +49,10 @@ export function focusMainWindow() { return focusWindow('home') } -export function focusOrNewSecondaryWindow(id, dimensions) { +export function focusOrNewSecondaryWindow(id, config) { let browserWindow = focusWindow(id) if (!browserWindow) { - browserWindow = newWindow(id, dimensions) + browserWindow = newWindow(id, config) browserWindow.setMenu(null) } return browserWindow @@ -72,11 +72,11 @@ export function focusWindow(id) { return browserWindow } -export function newWindow(id, dimensions, url) { +export function newWindow(id, config, url) { if (process.env.NODE_ENV === 'production' && process.env.BABEL_ENV !== 'test') { - dimensions.nodeIntegration = false + config.nodeIntegration = false } - let browserWindow = new BrowserWindow(dimensions) + let browserWindow = new BrowserWindow(config) if (!url) { url = process.env.NODE_ENV === 'development' ? `http://localhost:9080/${id}.html` diff --git a/src/renderer/components/Home.vue b/src/renderer/components/Home.vue index 254461baf..980be5c89 100644 --- a/src/renderer/components/Home.vue +++ b/src/renderer/components/Home.vue @@ -270,7 +270,8 @@ import { import { HotRegister, getColumnCount, - getCurrentColumnIndexOrMin + getCurrentColumnIndexOrMin, + resetTabMoves } from '../hot.js' import about from '../partials/About' import preferences from '../partials/Preferences' @@ -308,7 +309,8 @@ import { provenanceErrors$, errorFeedback$, updateHotDimensions$, - allTableLocks$ + allTableLocks$, + allTablesAllColumnNames$ } from '@/rxSubject.js' import VueRx from 'vue-rx' import { @@ -412,7 +414,7 @@ export default { activeTab: 'getActiveTab', tabIndex: 'getTabIndex' }), - ...mapGetters(['getPreviousTabId', 'tabTitle', 'getHotIdFromTabId', 'getHotSelection']), + ...mapGetters(['getPreviousTabId', 'tabTitle', 'getHotIdFromTabId', 'getHotSelection', 'getAllHotTablesColumnNames']), sideNavPropertiesForMain() { return this.sideNavStatus === 'closed' ? this.sideNavStatus : this.sideNavPosition }, @@ -461,6 +463,7 @@ export default { this.$subscribeTo(allTableLocks$, async function(allTablesLocks) { self.isActiveTabLocked = _.includes(allTablesLocks, self.currentHotId) disableEnableContextMenu(self.isActiveTabLocked) + resetTabMoves(self.isActiveTabLocked) ipc.send('hasLockedActiveTable', self.isActiveTabLocked) }) // request may be coming from another page - get focus first @@ -571,7 +574,7 @@ export default { ipc.on('guessColumnProperties', function(event, arg) { self.inferColumnProperties() }) - ipc.on('importDataPackage', function(event, filePath, isTransient = false) { + ipc.on('importDataPackageFromFile', function(event, filePath, isTransient = false) { self.importDataPackage(filePath, isTransient) }) ipc.on('validateTable', function(event, arg) { @@ -925,7 +928,9 @@ export default { // hot rendering problem when tabs opened quickly - https://github.com/ODIQueensland/data-curator/issues/803- workaround as selecting table re-renders getCurrentColumnIndexOrMin() updateHotDimensions$.next() - LockProperties.toggleLockColumnProperties() + LockProperties.lockColumnProperties() + // trigger column properties refresh (columns might already be opened) + allTablesAllColumnNames$.next(this.getAllHotTablesColumnNames()) this.addImportDataPropertiesError('Import Column properties success', `${schemaFieldsCount} schema fields were imported.`) } else { const errorMessage = `Unable to import ${schemaFieldsCount} schema fields to a ${columnCount}-column table` @@ -943,7 +948,7 @@ export default { this.messagesType = 'feedback' }, initHotColumnPropertiesFromSchema: function(hotId, schema) { - // TODO : move this to similar logic in importDataPackage to tidy up + // TODO : move this to similar logic in importDataPackageFromFile to tidy up if (!_.isEmpty(schema)) { let columnHotIdProperties = {} columnHotIdProperties[hotId] = [...schema.fields] diff --git a/src/renderer/components/KeyboardHelp.vue b/src/renderer/components/KeyboardHelp.vue index 412c02233..738189cea 100644 --- a/src/renderer/components/KeyboardHelp.vue +++ b/src/renderer/components/KeyboardHelp.vue @@ -236,11 +236,6 @@ Ctrl O Command ⌘ O - - Import Column Properties - Import a table schema JSON file. - Shift ⇧ Ctrl I - Shift ⇧ Command ⌘ I - Save the active data tab as a CSV file Ctrl S diff --git a/src/renderer/components/UrlDialog.vue b/src/renderer/components/UrlDialog.vue index 307c9dd52..d78716cf8 100644 --- a/src/renderer/components/UrlDialog.vue +++ b/src/renderer/components/UrlDialog.vue @@ -3,59 +3,72 @@ id="container" class="container-fluid">
-

+

+ -

+
{{ errors.first('url-dialog') }}
+ + @click.prevent="submit">{{ submitText }} +
diff --git a/src/renderer/hot.js b/src/renderer/hot.js index 5ba85cf2b..f4c0b1f71 100644 --- a/src/renderer/hot.js +++ b/src/renderer/hot.js @@ -5,6 +5,18 @@ import { allTablesAllColumnsFromSchema$, allTablesAllColumnNames$, afterSetDataA const _hots = {} +function defaultTabFunction({ shiftKey }) { + let hot = HotRegister.getActiveInstance() + if (!shiftKey) { + const selection = hot.getSelectedLast() + let next = hot.getCell(selection[0], selection[1] + 1) + if (next == null) { + hot.alter('insert_col', selection[1] + 1) + } + } + return { row: 0, col: 1 } +} + const HotRegister = { register(container, listeners={}, searchParameters = false) { let hot = new Handsontable(container, { @@ -33,16 +45,7 @@ const HotRegister = { }, undo: true, search: searchParameters, - tabMoves({ shiftKey }) { - if (!shiftKey) { - const selection = hot.getSelectedLast() - let next = hot.getCell(selection[0], selection[1] + 1) - if (next == null) { - hot.alter('insert_col', selection[1] + 1) - } - } - return { row: 0, col: 1 } - }, + tabMoves: defaultTabFunction, afterInit() { if (typeof listeners.loadingStartListener !== 'undefined') { listeners.loadingStartListener('Loading data. Please wait...') @@ -129,6 +132,16 @@ const HotRegister = { } } +function lockedTabFunction({ shiftKey }) { + return { row: 0, col: 1 } +} + +export function resetTabMoves(isActiveTabLocked) { + let hot = HotRegister.getActiveInstance() + hot.updateSettings({ tabMoves: isActiveTabLocked ? lockedTabFunction + : defaultTabFunction }) +} + export function getCurrentColumnIndexOrMin() { let activeHot = HotRegister.getActiveInstance() let currentCell = activeHot.getSelectedLast() diff --git a/src/renderer/lockProperties.js b/src/renderer/lockProperties.js index 7e7ea6834..cb4ab2979 100644 --- a/src/renderer/lockProperties.js +++ b/src/renderer/lockProperties.js @@ -14,6 +14,11 @@ const LockProperties = { this.updateStoredTableLock(hotId, currentLock) }, + lockColumnProperties () { + const hotId = HotRegister.getActiveInstance().guid + this.updateStoredTableLock(hotId, true) + }, + getLockedTables() { return store.getters.hasPropertyFromAllTables(this.storeName) }, diff --git a/src/renderer/partials/FindReplace.vue b/src/renderer/partials/FindReplace.vue index 0d3531e40..c795fb4d0 100644 --- a/src/renderer/partials/FindReplace.vue +++ b/src/renderer/partials/FindReplace.vue @@ -23,7 +23,7 @@ :name="formprop.key" class="pull-left form-control input-sm col-sm-9" type="text" - @input="setText(formprop.key, $event.target.value)" > + @input="setText(formprop.key, $event.target.value)"> - + @@ -93,6 +93,7 @@ import { } from '@/rxSubject.js' import Sifter from 'sifter/sifter.min.js' import { ipcRenderer as ipc } from 'electron' + Vue.use(AsyncComputed) Vue.use(VueRx, { Subscription @@ -101,11 +102,15 @@ Vue.use(VueRx, { let _lastRowIndicies = [] let _currentHotPos = [-1, -1] let _previousSearchClear = true +let _findTextValue = '' +let _isCaseSensitive = true + // cannot access DEFAULT anymore - must copy (https://docs.handsontable.com/3.0.0/demo-searching.html#page-custom-callback) -const _defaultCallback = function(instance, row, col, data, testResult) { - instance.getCellMeta(row, col).isSearchResult = testResult +const _defaultCallback = function (instance, row, col, data, testResult) { + let callbackQuery = _isCaseSensitive ? testResult && _.includes(data, _findTextValue) : testResult + instance.getCellMeta(row, col).isSearchResult = callbackQuery } -const _searchCallback = function(instance, row, col, value, result) { +const _searchCallback = function (instance, row, col, value, result) { // const defaultCallback = instance.getCallback() if (!_previousSearchClear && _.indexOf(_lastRowIndicies, row) > -1) { _defaultCallback.apply(this, arguments) @@ -116,10 +121,9 @@ const _searchCallback = function(instance, row, col, value, result) { export default { name: 'FindReplace', - components: { - }, + components: {}, extends: SideNav, - data() { + data () { return { activeHotId: null, findTypePicked: 'findInColumn', @@ -175,34 +179,34 @@ export default { computed: { ...mapGetters(['getHotSelection']) }, - mounted: async function() { + mounted: async function () { this.activeHotId = await this.currentHotId() const vueUpdateActiveHotId = this.updateActiveHotId const vueResetOnColumnChange = this.resetOnColumnChange const vueResetSearchResult = this.resetSearchResultWrapper const vueResetRowIndex = this.resetRowIndex - this.$subscribeTo(hotIdFromTab$, function(hotId) { + this.$subscribeTo(hotIdFromTab$, function (hotId) { vueUpdateActiveHotId(hotId) vueResetSearchResult() }) - this.$subscribeTo(currentPos$, function(currentPos) { + this.$subscribeTo(currentPos$, function (currentPos) { vueResetOnColumnChange() vueResetRowIndex() }) - ipc.on('clickFindButton', function(event, arg) { + ipc.on('clickFindButton', function (event, arg) { let el = document.querySelector(`button .${arg}`).parentNode el.click() el.classList.add('active', 'focus') }) }, methods: { - getReplaceResultIcon: function() { + getReplaceResultIcon: function () { return (this.replacesRemaining > 0) ? 'glyphicon-ok' : 'glyphicon-remove' }, - getFindResultIcon: function() { + getFindResultIcon: function () { return (this.foundCount && this.foundCount.length > 0) ? 'glyphicon-ok' : 'glyphicon-remove' }, - findResults: function(key) { + findResults: function (key) { // show result at either find or replace view if (key === this.clickedFindOrReplace) { // TODO: tidy use cases for updatedRowIndex, so updatedCount not needed @@ -216,7 +220,7 @@ export default { } } }, - replaceResults: function(key) { + replaceResults: function (key) { // show result at either find or replace view if (key === this.clickedFindOrReplace) { if (this.replacesRemaining > -1) { @@ -228,28 +232,28 @@ export default { } } }, - inputFoundSuccessFeedback: function(key) { + inputFoundSuccessFeedback: function (key) { let element = this.initFeedbackContainer(key) element.classList.add('has-success') }, - inputFoundFailureFeedback: function(key) { + inputFoundFailureFeedback: function (key) { let element = this.initFeedbackContainer(key) element.classList.add('has-error') }, - initFeedbackContainer: function(key) { + initFeedbackContainer: function (key) { this.inputFoundRemoveFeedback() return document.querySelector(`#findAndReplace .placeholder.${key}`) }, - inputFoundRemoveFeedback: function() { - _.forEach(document.querySelectorAll('#findAndReplace .placeholder'), function(el, index) { + inputFoundRemoveFeedback: function () { + _.forEach(document.querySelectorAll('#findAndReplace .placeholder'), function (el, index) { el.classList.remove('has-success') el.classList.remove('has-error') }) }, - getText: function(key) { + getText: function (key) { return key === 'find' ? this.findTextValue : this.replaceTextValue }, - setText: function(key, value) { + setText: function (key, value) { if (key === 'find') { this.findTextValue = value } else { @@ -261,11 +265,11 @@ export default { // this.clickedFindOrReplace = null // wait for the css to update from resetting counters then remove all const vueInputFoundRemoveFeedback = this.inputFoundRemoveFeedback - _.delay(function() { + _.delay(function () { vueInputFoundRemoveFeedback() }, 10) }, - replaceText: function(direction) { + replaceText: function (direction) { // this.inputFoundRemoveFeedback() if (this.clickedFindOrReplace === 'find') { this.resetSearchResultWrapper() @@ -301,7 +305,7 @@ export default { // this.resetOnColumnChange() this.clickedFindOrReplace = 'replace' }, - replaceAllText: function(direction) { + replaceAllText: function (direction) { if (this.clickedFindOrReplace === 'find') { this.resetSearchResultWrapper() } @@ -319,22 +323,25 @@ export default { hot.loadData(data) this.clickedFindOrReplace = 'replace' }, - // Ensure replace, like find, is case insensitive - getReplacedAllFindTextFromCell: function(data, row, col) { + // Ensure replace, like find, is case insensitive or case sensitive + getReplacedAllFindTextFromCell: function (data, row, col) { let cellText = data[row][col] - let caseInsensitiveCellText = cellText.toLowerCase() - let caseInsensitiveFindTextValue = this.findTextValue.toLowerCase() + let findText = this.findTextValue + if (!_isCaseSensitive) { + cellText = cellText.toLowerCase() + findText = findText.toLowerCase() + } // ensure any special characters in find text are treated as ordinary text - const escapedFindText = _.escapeRegExp(caseInsensitiveFindTextValue) + const escapedFindText = _.escapeRegExp(findText) const regExp = new RegExp(escapedFindText, 'g') - let updatedCellText = _.replace(caseInsensitiveCellText, regExp, this.replaceTextValue) + let updatedCellText = _.replace(cellText, regExp, this.replaceTextValue) return updatedCellText }, - previousFn: function(index, arrayLength) { + previousFn: function (index, arrayLength) { index = index > 0 ? index - 1 : arrayLength - 1 return index }, - nextFn: function(index, arrayLength) { + nextFn: function (index, arrayLength) { if (index >= arrayLength - 1) { index = 0 } else { @@ -342,7 +349,7 @@ export default { } return index }, - findText: function(direction) { + findText: function (direction) { if (this.clickedFindOrReplace === 'replace') { this.resetSearchResultWrapper() } @@ -351,7 +358,7 @@ export default { // a new index calculation will reset this.clickedFindOrReplace so ensure for first run it is referenced again for feedback this.clickedFindOrReplace = 'find' }, - findNextOrPrevious: function(direction) { + findNextOrPrevious: function (direction) { console.time() let directionFn if (direction === 'previous') { @@ -371,7 +378,7 @@ export default { let colData = hot.getDataAtCol(currentCol) // indexer won't work with a header labelled: 0 let tempHeader = currentCol + 1 - let colObject = _.map(colData, function(item) { + let colObject = _.map(colData, function (item) { return { [tempHeader]: item } }) this.currentCol = currentCol @@ -395,13 +402,14 @@ export default { this.currentCol = currentCol this.updatedRowIndex = tempUpdatedRowIndex }, - hotSearch: function(hot) { + hotSearch: function (hot) { // TODO : add loading screen here const search = hot.getPlugin('search') + _findTextValue = this.findTextValue search.query(this.findTextValue) hot.render() }, - clearPreviousHotSearch: function(hot) { + clearPreviousHotSearch: function (hot) { _previousSearchClear = false const search = hot.getPlugin('search') search.query() @@ -419,10 +427,10 @@ export default { // console.timeEnd() // return rowIndicies // }, - hotSiftWithoutTransform: function(arrayOfObjects, headers) { + hotSiftWithoutTransform: function (arrayOfObjects, headers) { let rowIndicies = this.sift(arrayOfObjects, headers) // sort in ascending order - rowIndicies.sort(function(a, b) { + rowIndicies.sort(function (a, b) { return a - b }) return rowIndicies @@ -459,8 +467,9 @@ export default { // }, // Note: sift will return 1 match for multiple matches in row // sift gives flexibility for reasonably fast access, but allows to search across table rows too if needed later + diacritics support - sift: function(arrayOfObjects, headers) { - var sifter = new Sifter(arrayOfObjects) + sift: function (arrayOfObjects, headers) { + // case-sensitive means no diacritics search + var sifter = new Sifter(arrayOfObjects, { diacritics: !_isCaseSensitive }) let result let ids = [] if (this.findTextValue) { @@ -468,13 +477,31 @@ export default { fields: headers, conjunction: 'and' }) - for (const item of result.items) { + ids = _isCaseSensitive + ? this.caseSensitiveSiftResult(arrayOfObjects, headers, result) + : this.caseInSensitiveSiftResult(arrayOfObjects, headers, result) + } + return ids + }, + caseSensitiveSiftResult: function(arrayOfObjects, headers, result) { + let ids = [] + let header = headers[0] + for (const item of result.items) { + let nextValue = arrayOfObjects[item.id][header] + if (nextValue.includes(this.findTextValue)) { ids.push(item.id) } } return ids }, - determineStartingRowIndex: function(currentRow, direction, directionFn) { + caseInSensitiveSiftResult: function(arrayOfObjects, headers, result) { + let ids = [] + for (const item of result.items) { + ids.push(item.id) + } + return ids + }, + determineStartingRowIndex: function (currentRow, direction, directionFn) { let index = _.sortedIndex(this.rowIndicies, currentRow) if (this.rowIndicies[index] === currentRow) { // starting row should never be the current selected row itself @@ -493,16 +520,16 @@ export default { } return index }, - updateActiveHotId: function(hotId) { + updateActiveHotId: function (hotId) { this.activeHotId = hotId }, - resetOnColumnChange: function() { + resetOnColumnChange: function () { let coords = this.getHotSelection(this.activeHotId) if (coords && coords[1] != this.currentCol) { this.resetSearchResultWrapper() } }, - resetSearchResultWrapper: function() { + resetSearchResultWrapper: function () { // reset can be called for multiple behaviours - ensure don't overwrite with previously reset/null rowIndicies if (!_.isEmpty(this.rowIndicies)) { _lastRowIndicies = this.rowIndicies @@ -516,12 +543,12 @@ export default { this.replacesRemaining = -1 this.updatedRowIndex = -1 }, - resetRowIndex() { + resetRowIndex () { this.updatedRowIndex = -1 } } } diff --git a/src/urldialog.ejs b/src/urldialog.ejs index 7c1d4ee91..5720a868c 100644 --- a/src/urldialog.ejs +++ b/src/urldialog.ejs @@ -2,7 +2,6 @@ - Data Curator - URL Dialog <% if (htmlWebpackPlugin.options.nodeModules) { %>