From 8985bbb4f83ce644fca8c6903d1c6e784ce2adc6 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:57:04 -0400 Subject: [PATCH 001/153] Upgrade handsontable to 12.1.0 and add react wrapper --- specifyweb/frontend/js_src/package-lock.json | 124 +++++++++++-------- specifyweb/frontend/js_src/package.json | 3 +- 2 files changed, 74 insertions(+), 53 deletions(-) diff --git a/specifyweb/frontend/js_src/package-lock.json b/specifyweb/frontend/js_src/package-lock.json index 205bd49b754..f714e1145d5 100644 --- a/specifyweb/frontend/js_src/package-lock.json +++ b/specifyweb/frontend/js_src/package-lock.json @@ -14,6 +14,7 @@ "@codemirror/language": "^6.2.0", "@codemirror/legacy-modes": "^6.1.0", "@floating-ui/react": "^0.19.1", + "@handsontable/react": "^12.4.0", "@headlessui/react": "^1.6.6", "@shopify/draggable": "^1.1.3", "@uiw/codemirror-theme-okaidia": "^4.10.4", @@ -26,7 +27,7 @@ "d3": "^7.6.1", "dayjs": "^1.10.7", "globals": "^13.19.0", - "handsontable": "9.0.0", + "handsontable": "^12.1.0", "jquery": "~1.12.0", "leaflet": "^1.7.1", "leaflet-gesture-handling": "^1.2.2", @@ -2686,6 +2687,14 @@ "react-dom": ">=16.8.0" } }, + "node_modules/@handsontable/react": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@handsontable/react/-/react-12.4.0.tgz", + "integrity": "sha512-qCjZq5TuJTvYKIl3B111rw5X3vlmNckW7520izSJKdDOVxlPr6aLCr13yYL+0eFtBIMYl6M3logzkFFh7z72ew==", + "peerDependencies": { + "handsontable": ">=12.0.0" + } + }, "node_modules/@headlessui/react": { "version": "1.6.6", "license": "MIT", @@ -6364,9 +6373,10 @@ } }, "node_modules/core-js": { - "version": "3.23.4", + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", + "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==", "hasInstallScript": true, - "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/core-js" @@ -9257,23 +9267,25 @@ } }, "node_modules/handsontable": { - "version": "9.0.0", - "license": "SEE LICENSE IN LICENSE.txt", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.1.0.tgz", + "integrity": "sha512-MdplUt3MNc1Ir6dTIlwHUOjCm0GTvB3RpKvvBgb9Jzp/TYVZ3YbfpS6II6Q5sYpUh40ILgOkDtS9gdZTM/OFsQ==", "dependencies": { "@types/pikaday": "1.7.4", "core-js": "^3.0.0", "dompurify": "^2.1.1", - "moment": "2.24.0", + "moment": "2.29.3", "numbro": "2.1.2", - "pikaday": "1.8.0" + "pikaday": "1.8.2" }, "optionalDependencies": { - "hyperformula": "0.6.2" + "hyperformula": "^2.0.0" } }, "node_modules/handsontable/node_modules/moment": { - "version": "2.24.0", - "license": "MIT", + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==", "engines": { "node": "*" } @@ -9492,23 +9504,19 @@ } }, "node_modules/hyperformula": { - "version": "0.6.2", - "license": "SEE LICENSE IN LICENSE.txt", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-2.6.2.tgz", + "integrity": "sha512-PtrYbEi+qyXEc1GSN8bhQqdOeDh6wajWpZajAMhJiJT/XRlXNm7LhSbkvi0cCCBGJHu+8zP3Y5qDmrzbdCh0QA==", "optional": true, "dependencies": { "chevrotain": "^6.5.0", - "core-js": "^3.6.4", - "regenerator-runtime": "^0.13.3", - "tiny-emitter": "^2.1.0", - "unorm": "^1.6.0" - }, - "optionalDependencies": { - "gpu.js": "2.3.0" + "tiny-emitter": "^2.1.0" } }, "node_modules/hyperformula/node_modules/chevrotain": { "version": "6.5.0", - "license": "Apache-2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz", + "integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==", "optional": true, "dependencies": { "regexp-to-ast": "0.4.0" @@ -9516,7 +9524,8 @@ }, "node_modules/hyperformula/node_modules/regexp-to-ast": { "version": "0.4.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz", + "integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==", "optional": true }, "node_modules/iconv-lite": { @@ -12028,8 +12037,9 @@ } }, "node_modules/moment": { - "version": "2.29.1", - "license": "MIT", + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", "engines": { "node": "*" } @@ -12712,8 +12722,9 @@ } }, "node_modules/pikaday": { - "version": "1.8.0", - "license": "(0BSD OR MIT)" + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.2.tgz", + "integrity": "sha512-TNtsE+34BIax3WtkB/qqu5uepV1McKYEgvL3kWzU7aqPCpMEN6rBF3AOwu4WCwAealWlBGobXny/9kJb49C1ew==" }, "node_modules/pirates": { "version": "4.0.6", @@ -15573,7 +15584,8 @@ }, "node_modules/tiny-emitter": { "version": "2.1.0", - "license": "MIT", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "optional": true }, "node_modules/tmpl": { @@ -16120,14 +16132,6 @@ "node": ">= 4.0.0" } }, - "node_modules/unorm": { - "version": "1.6.0", - "license": "MIT or GPL-2.0", - "optional": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", @@ -18622,6 +18626,12 @@ "@floating-ui/dom": "^1.1.1" } }, + "@handsontable/react": { + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@handsontable/react/-/react-12.4.0.tgz", + "integrity": "sha512-qCjZq5TuJTvYKIl3B111rw5X3vlmNckW7520izSJKdDOVxlPr6aLCr13yYL+0eFtBIMYl6M3logzkFFh7z72ew==", + "requires": {} + }, "@headlessui/react": { "version": "1.6.6", "requires": {} @@ -21368,7 +21378,9 @@ "dev": true }, "core-js": { - "version": "3.23.4" + "version": "3.36.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.0.tgz", + "integrity": "sha512-mt7+TUBbTFg5+GngsAxeKBTl5/VS0guFeJacYge9OmHb+m058UwwIm41SE9T4Den7ClatV57B6TYTuJ0CX1MAw==" }, "core-js-compat": { "version": "3.23.4", @@ -23372,19 +23384,23 @@ } }, "handsontable": { - "version": "9.0.0", + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/handsontable/-/handsontable-12.1.0.tgz", + "integrity": "sha512-MdplUt3MNc1Ir6dTIlwHUOjCm0GTvB3RpKvvBgb9Jzp/TYVZ3YbfpS6II6Q5sYpUh40ILgOkDtS9gdZTM/OFsQ==", "requires": { "@types/pikaday": "1.7.4", "core-js": "^3.0.0", "dompurify": "^2.1.1", - "hyperformula": "0.6.2", - "moment": "2.24.0", + "hyperformula": "^2.0.0", + "moment": "2.29.3", "numbro": "2.1.2", - "pikaday": "1.8.0" + "pikaday": "1.8.2" }, "dependencies": { "moment": { - "version": "2.24.0" + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" } } }, @@ -23543,19 +23559,19 @@ "dev": true }, "hyperformula": { - "version": "0.6.2", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/hyperformula/-/hyperformula-2.6.2.tgz", + "integrity": "sha512-PtrYbEi+qyXEc1GSN8bhQqdOeDh6wajWpZajAMhJiJT/XRlXNm7LhSbkvi0cCCBGJHu+8zP3Y5qDmrzbdCh0QA==", "optional": true, "requires": { "chevrotain": "^6.5.0", - "core-js": "^3.6.4", - "gpu.js": "2.3.0", - "regenerator-runtime": "^0.13.3", - "tiny-emitter": "^2.1.0", - "unorm": "^1.6.0" + "tiny-emitter": "^2.1.0" }, "dependencies": { "chevrotain": { "version": "6.5.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-6.5.0.tgz", + "integrity": "sha512-BwqQ/AgmKJ8jcMEjaSnfMybnKMgGTrtDKowfTP3pX4jwVy0kNjRsT/AP6h+wC3+3NC+X8X15VWBnTCQlX+wQFg==", "optional": true, "requires": { "regexp-to-ast": "0.4.0" @@ -23563,6 +23579,8 @@ }, "regexp-to-ast": { "version": "0.4.0", + "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.4.0.tgz", + "integrity": "sha512-4qf/7IsIKfSNHQXSwial1IFmfM1Cc/whNBQqRwe0V2stPe7KmN1U0tWQiIx6JiirgSrisjE0eECdNf7Tav1Ntw==", "optional": true } } @@ -25386,7 +25404,9 @@ } }, "moment": { - "version": "2.29.1" + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" }, "mrmime": { "version": "1.0.1", @@ -25863,7 +25883,9 @@ "dev": true }, "pikaday": { - "version": "1.8.0" + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/pikaday/-/pikaday-1.8.2.tgz", + "integrity": "sha512-TNtsE+34BIax3WtkB/qqu5uepV1McKYEgvL3kWzU7aqPCpMEN6rBF3AOwu4WCwAealWlBGobXny/9kJb49C1ew==" }, "pirates": { "version": "4.0.6", @@ -27720,6 +27742,8 @@ }, "tiny-emitter": { "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "optional": true }, "tmpl": { @@ -28106,10 +28130,6 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true }, - "unorm": { - "version": "1.6.0", - "optional": true - }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", diff --git a/specifyweb/frontend/js_src/package.json b/specifyweb/frontend/js_src/package.json index e16fd5dc234..3a58b51556a 100644 --- a/specifyweb/frontend/js_src/package.json +++ b/specifyweb/frontend/js_src/package.json @@ -34,6 +34,7 @@ "@codemirror/language": "^6.2.0", "@codemirror/legacy-modes": "^6.1.0", "@floating-ui/react": "^0.19.1", + "@handsontable/react": "^12.4.0", "@headlessui/react": "^1.6.6", "@shopify/draggable": "^1.1.3", "@uiw/codemirror-theme-okaidia": "^4.10.4", @@ -46,7 +47,7 @@ "d3": "^7.6.1", "dayjs": "^1.10.7", "globals": "^13.19.0", - "handsontable": "9.0.0", + "handsontable": "^12.1.0", "jquery": "~1.12.0", "leaflet": "^1.7.1", "leaflet-gesture-handling": "^1.2.2", From b1fc448375035f23ce52b6395f8a6393348813f1 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:14:23 -0400 Subject: [PATCH 002/153] Fix type errors after upgrading handsontable --- .../lib/components/WorkBench/WbUtils.tsx | 6 +-- .../lib/components/WorkBench/WbView.tsx | 4 +- .../lib/components/WorkBench/handsontable.ts | 9 ++--- .../js_src/lib/components/WorkBench/hooks.ts | 40 +++++++++++++------ .../js_src/lib/utils/cache/definitions.ts | 12 +++--- 5 files changed, 42 insertions(+), 29 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index a7df7136d24..312065c6cbc 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -587,7 +587,7 @@ export class WbUtils extends Backbone.View { // Context menu item definitions (common for fillUp and fillDown) fillCellsContextMenuItem( mode: 'down' | 'up' - ): Handsontable.contextMenu.MenuItemConfig { + ): Handsontable.plugins.ContextMenu.MenuItemConfig { return { name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), disabled: () => @@ -598,8 +598,8 @@ export class WbUtils extends Backbone.View { ?.getSelected() ?.every((selection) => selection[0] === selection[2]) ?? false), - callback: (_, selections) => - selections.forEach((selection) => + callback: (_: any, selections: any) => + selections.forEach((selection: any) => Array.from( new Array(selection.end.col + 1 - selection.start.col).keys() ).forEach((index) => { diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 5ea8aaaef1d..e4e5e308eed 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -418,8 +418,8 @@ export class WbView extends Backbone.View { contextMenu: { items: ensure< IR< - | Handsontable.contextMenu.MenuItemConfig - | Handsontable.contextMenu.PredefinedMenuItemKey + | Handsontable.plugins.ContextMenu.MenuItemConfig + | Handsontable.plugins.ContextMenu.PredefinedMenuItemKey > >()( this.isUploaded diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts index 2b0d783a422..ef055e93faa 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts @@ -1,4 +1,5 @@ import type Handsontable from 'handsontable'; +import { Plugins } from 'handsontable/plugins'; import { getCache } from '../../utils/cache'; import { writable } from '../../utils/types'; @@ -73,16 +74,14 @@ const hotPlugins = new WeakMap< Handsontable, // eslint-disable-next-line functional/prefer-readonly-type { - [key in keyof Handsontable.PluginsCollection]?: - | Handsontable.PluginsCollection[key] - | undefined; + [key in keyof Plugins]?: Plugins[key] | undefined; } >(); -export function getHotPlugin( +export function getHotPlugin( hot: Handsontable, pluginName: NAME -): Handsontable.PluginsCollection[NAME] { +): Plugins[NAME] { let plugins = hotPlugins.get(hot); if (plugins === undefined) { plugins = {}; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 087cd2e1a3f..7045a161125 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -1,4 +1,6 @@ import type Handsontable from 'handsontable'; +import { Events } from 'handsontable/pluginHooks'; +import { Action } from 'handsontable/plugins/undoRedo'; import { backEndText } from '../../localization/backEnd'; import { whitespaceSensitive } from '../../localization/utils'; @@ -21,7 +23,7 @@ export function getHotHooks(wbView: WbView) { | ReturnType | undefined = undefined; - return ensure>()({ + return ensure>()({ /* * After cell is rendered, we need to reApply metaData classes * NOTE: @@ -30,7 +32,13 @@ export function getHotHooks(wbView: WbView) { * cases * */ - afterRenderer: (td, visualRow, visualCol, property, _value) => { + afterRenderer: ( + td: any, + visualRow: number, + visualCol: number, + property: string | number, + _value: string + ) => { if (wbView.hot === undefined) { td.classList.add('text-gray-500'); return; @@ -73,11 +81,15 @@ export function getHotHooks(wbView: WbView) { }, // Make HOT use defaultValues for validation if cell is empty - beforeValidate: (value, _visualRow, property) => { + beforeValidate: ( + value: any, + _visualRow: number, + property: string | number + ) => { if (Boolean(value) || wbView.hot === undefined) return value; const visualCol = wbView.hot.propToCol(property); - const physicalCol = wbView.hot.toPhysicalColumn(visualCol); + const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); return wbView.mappings?.defaultValues[physicalCol] ?? value; }, @@ -92,7 +104,7 @@ export function getHotHooks(wbView: WbView) { const visualCol = wbView.hot.propToCol(property); const physicalRow = wbView.hot.toPhysicalRow(visualRow); - const physicalCol = wbView.hot.toPhysicalColumn(visualCol); + const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); const issues = wbView.cells.getCellMeta( physicalRow, physicalCol, @@ -155,7 +167,7 @@ export function getHotHooks(wbView: WbView) { if (source !== 'CopyPaste.paste') return true; const filteredChanges = unfilteredChanges.filter( - ([, property]) => property < wbView.dataset.columns.length + ([, property]) => (property as number) < wbView.dataset.columns.length ); if ( filteredChanges.length === unfilteredChanges.length || @@ -165,7 +177,7 @@ export function getHotHooks(wbView: WbView) { wbView.hot.setDataAtCell( filteredChanges.map(([visualRow, property, _oldValue, newValue]) => [ visualRow, - wbView.hot!.propToCol(property), + wbView.hot!.propToCol(property as number), newValue, ]), 'CopyPaste.paste' @@ -191,12 +203,14 @@ export function getHotHooks(wbView: WbView) { const changes = unfilteredChanges .map(([visualRow, property, oldValue, newValue]) => ({ visualRow, - visualCol: wbView.hot!.propToCol(property), + visualCol: wbView.hot!.propToCol(property as string | number), physicalRow: wbView.hot!.toPhysicalRow(visualRow), physicalCol: typeof property === 'number' ? property - : wbView.hot!.toPhysicalColumn(wbView.hot!.propToCol(property)), + : wbView.hot!.toPhysicalColumn( + wbView.hot!.propToCol(property as string | number) + ), oldValue, newValue, })) @@ -211,7 +225,7 @@ export function getHotHooks(wbView: WbView) { // Or where value changed from null to empty (oldValue !== null || newValue !== '') && // Or the column does not exist (that can happen on paste) - visualCol < wbView.dataset.columns.length + (visualCol as number) < wbView.dataset.columns.length ); if (changes.length === 0) return; @@ -368,8 +382,8 @@ export function getHotHooks(wbView: WbView) { return true; const findTreeColumns = ( - sortConfig: RA, - deltaSearchConfig: RA + sortConfig: RA, + deltaSearchConfig: RA ) => sortConfig .map(({ column: visualCol, sortOrder }) => ({ @@ -596,7 +610,7 @@ export function getHotHooks(wbView: WbView) { function afterUndoRedo( wbView: WbView, type: 'redo' | 'undo', - data: Handsontable.plugins.UndoRedoAction + data: Action ): void { if ( wbView.undoRedoIsHandled || diff --git a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts index 09ac9f96fcf..fb6e876c290 100644 --- a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts +++ b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts @@ -4,8 +4,8 @@ * @module */ -import type hot from 'handsontable'; import type { State } from 'typesafe-reducer'; +import { SortOrderType } from 'handsontable/plugins/columnSorting'; import type { AppResourcesConformation } from '../../components/AppResources/Aside'; import type { AppResourceFilters } from '../../components/AppResources/filtersHelpers'; @@ -89,11 +89,11 @@ export type CacheDefinitions = { * WorkBench column sort setting in a given dataset * {Collection ID}_{Dataset ID} */ - [key in `${number}_${number}`]: RA< - hot.columnSorting.Config & { - readonly physicalCol: number; - } - >; + [key in `${number}_${number}`]: RA<{ + readonly column: number; + readonly sortOrder: SortOrderType; + readonly physicalCol: number; + }>; }; readonly sortConfig: { readonly [KEY in keyof SortConfigs]: SortConfig; From eaf1b8823524c4674e0db709537cb616b3a5a8c6 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 11 Mar 2024 20:20:22 +0000 Subject: [PATCH 003/153] Lint code with ESLint and Prettier Triggered by b1fc448375035f23ce52b6395f8a6393348813f1 on branch refs/heads/issue-4611 --- .../lib/components/WorkBench/handsontable.ts | 2 +- .../js_src/lib/components/WorkBench/hooks.ts | 18 +++++++++--------- .../js_src/lib/utils/cache/definitions.ts | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts index ef055e93faa..392d24b0a4e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts @@ -1,5 +1,5 @@ import type Handsontable from 'handsontable'; -import { Plugins } from 'handsontable/plugins'; +import type { Plugins } from 'handsontable/plugins'; import { getCache } from '../../utils/cache'; import { writable } from '../../utils/types'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 7045a161125..7d4bb2a6089 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -1,6 +1,6 @@ import type Handsontable from 'handsontable'; -import { Events } from 'handsontable/pluginHooks'; -import { Action } from 'handsontable/plugins/undoRedo'; +import type { Events } from 'handsontable/pluginHooks'; +import type { Action } from 'handsontable/plugins/undoRedo'; import { backEndText } from '../../localization/backEnd'; import { whitespaceSensitive } from '../../localization/utils'; @@ -36,7 +36,7 @@ export function getHotHooks(wbView: WbView) { td: any, visualRow: number, visualCol: number, - property: string | number, + property: number | string, _value: string ) => { if (wbView.hot === undefined) { @@ -84,12 +84,12 @@ export function getHotHooks(wbView: WbView) { beforeValidate: ( value: any, _visualRow: number, - property: string | number + property: number | string ) => { if (Boolean(value) || wbView.hot === undefined) return value; const visualCol = wbView.hot.propToCol(property); - const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); + const physicalCol = wbView.hot.toPhysicalColumn(visualCol); return wbView.mappings?.defaultValues[physicalCol] ?? value; }, @@ -104,7 +104,7 @@ export function getHotHooks(wbView: WbView) { const visualCol = wbView.hot.propToCol(property); const physicalRow = wbView.hot.toPhysicalRow(visualRow); - const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); + const physicalCol = wbView.hot.toPhysicalColumn(visualCol); const issues = wbView.cells.getCellMeta( physicalRow, physicalCol, @@ -203,13 +203,13 @@ export function getHotHooks(wbView: WbView) { const changes = unfilteredChanges .map(([visualRow, property, oldValue, newValue]) => ({ visualRow, - visualCol: wbView.hot!.propToCol(property as string | number), + visualCol: wbView.hot!.propToCol(property), physicalRow: wbView.hot!.toPhysicalRow(visualRow), physicalCol: typeof property === 'number' ? property : wbView.hot!.toPhysicalColumn( - wbView.hot!.propToCol(property as string | number) + wbView.hot!.propToCol(property as number | string) ), oldValue, newValue, @@ -225,7 +225,7 @@ export function getHotHooks(wbView: WbView) { // Or where value changed from null to empty (oldValue !== null || newValue !== '') && // Or the column does not exist (that can happen on paste) - (visualCol as number) < wbView.dataset.columns.length + visualCol < wbView.dataset.columns.length ); if (changes.length === 0) return; diff --git a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts index fb6e876c290..83596d134b5 100644 --- a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts +++ b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts @@ -4,8 +4,8 @@ * @module */ +import type { SortOrderType } from 'handsontable/plugins/columnSorting'; import type { State } from 'typesafe-reducer'; -import { SortOrderType } from 'handsontable/plugins/columnSorting'; import type { AppResourcesConformation } from '../../components/AppResources/Aside'; import type { AppResourceFilters } from '../../components/AppResources/filtersHelpers'; From aa1837b172756ac59c97e88dec339a151346a942 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 13 Mar 2024 18:00:51 -0400 Subject: [PATCH 004/153] Add component for workbench spreadsheet --- .../components/WorkBench/WbSpreadsheet.tsx | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx new file mode 100644 index 00000000000..548f2bfce7e --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -0,0 +1,150 @@ +/** + * Component for the Handsontable React wrapper + * + * TODO: + * - Add context menu settings + * - Upgrade to handsontable14 to fix copyPasteEnabled error + */ + +import { HotTable } from '@handsontable/react'; +import React from 'react'; + +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { RA } from '../../utils/types'; +import { f } from '../../utils/functools'; +import { LANGUAGE } from '../../localization/utils/config'; +import { wbPlanText } from '../../localization/wbPlan'; +import { userPreferences } from '../Preferences/userPreferences'; +import { hasPermission } from '../Permissions/helpers'; +import { parseWbMappings } from './mapping'; +import { getTable } from '../DataModel/tables'; +import { legacyNonJsxIcons } from '../Atoms/Icons'; + +export function WbSpreadsheet({ + dataset, + hotRef, + isUploaded, +}: { + readonly dataset: Dataset; + readonly hotRef: any; + readonly isUploaded: boolean; +}): JSX.Element { + const data = React.useMemo>>( + () => + dataset.rows.length === 0 + ? [Array.from(dataset.columns).fill(null)] + : dataset.rows, + [dataset.rows] + ); + const mappings = parseWbMappings(dataset); + + const physicalColToMappingCol = (physicalCol: number): number | undefined => { + return mappings?.lines.findIndex( + ({ headerName }) => headerName === dataset.columns[physicalCol] + ); + }; + + return ( + <> + ({ + // Get data from nth column for nth column + data: physicalCol, + }) + )} + colHeaders={(physicalCol: number) => { + const tableIcon = mappings?.mappedHeaders?.[physicalCol]; + const isMapped = tableIcon !== undefined; + const mappingCol = physicalColToMappingCol(physicalCol); + const tableName = + (typeof mappingCol === 'number' + ? mappings?.tableNames[mappingCol] + : undefined) ?? + tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; + const tableLabel = isMapped + ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' + : ''; + // REFACTOR: use new table icons + return `
+ ${ + isMapped + ? `${tableLabel}` + : `${legacyNonJsxIcons.ban}` + } + + ${dataset.columns[physicalCol]} + +
`; + }} + hiddenColumns={{ + // Hide the disambiguation column + columns: [dataset.columns.length], + indicators: false, + // @ts-expect-error Typing doesn't match for handsontable 12.1.0, fixed in 14 + copyPasteEnabled: false, + }} + hiddenRows={{ + rows: [], + indicators: false, + // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // Typing doesn't match for handsontable 12.1.0, fixed in 14 + // copyPasteEnabled: false, + }} + minSpareRows={userPreferences.get( + 'workBench', + 'editor', + 'minSpareRows' + )} + comments={{ + displayDelay: 100, + }} + commentedCellClassName="htCommentCell" + placeholderCellClassName="htPlaceholder" + invalidCellClassName="-" + rowHeaders={true} + autoWrapCol={userPreferences.get('workBench', 'editor', 'autoWrapCol')} + autoWrapRow={userPreferences.get('workBench', 'editor', 'autoWrapRow')} + enterBeginsEditing={userPreferences.get( + 'workBench', + 'editor', + 'enterBeginsEditing' + )} + enterMoves={ + userPreferences.get('workBench', 'editor', 'enterMoveDirection') === + 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 } + } + tabMoves={ + userPreferences.get('workBench', 'editor', 'tabMoveDirection') === + 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 } + } + manualColumnResize={true} + manualColumnMove={true} + outsideClickDeselects={false} + multiColumnSorting={true} + sortIndicator={true} + language={LANGUAGE} + licenseKey="non-commercial-and-evaluation" + stretchH="all" + readOnly={isUploaded || !hasPermission('/workbench/dataset', 'update')} + // contextMenu={contextMenuConfig as Settings} + /> + + ); +} From c48b039fb031ad036d8651adde8b5b26d22ea9a9 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 13 Mar 2024 22:04:44 +0000 Subject: [PATCH 005/153] Lint code with ESLint and Prettier Triggered by aa1837b172756ac59c97e88dec339a151346a942 on branch refs/heads/issue-4611 --- .../components/WorkBench/WbSpreadsheet.tsx | 178 +++++++++--------- 1 file changed, 85 insertions(+), 93 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 548f2bfce7e..f2b831489c8 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -9,16 +9,16 @@ import { HotTable } from '@handsontable/react'; import React from 'react'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import type { RA } from '../../utils/types'; -import { f } from '../../utils/functools'; import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; -import { userPreferences } from '../Preferences/userPreferences'; +import { f } from '../../utils/functools'; +import type { RA } from '../../utils/types'; +import { legacyNonJsxIcons } from '../Atoms/Icons'; +import { getTable } from '../DataModel/tables'; import { hasPermission } from '../Permissions/helpers'; +import { userPreferences } from '../Preferences/userPreferences'; +import type { Dataset } from '../WbPlanView/Wrapped'; import { parseWbMappings } from './mapping'; -import { getTable } from '../DataModel/tables'; -import { legacyNonJsxIcons } from '../Atoms/Icons'; export function WbSpreadsheet({ dataset, @@ -38,39 +38,29 @@ export function WbSpreadsheet({ ); const mappings = parseWbMappings(dataset); - const physicalColToMappingCol = (physicalCol: number): number | undefined => { - return mappings?.lines.findIndex( + const physicalColToMappingCol = (physicalCol: number): number | undefined => + mappings?.lines.findIndex( ({ headerName }) => headerName === dataset.columns[physicalCol] ); - }; return ( - <> - ({ - // Get data from nth column for nth column - data: physicalCol, - }) - )} - colHeaders={(physicalCol: number) => { - const tableIcon = mappings?.mappedHeaders?.[physicalCol]; - const isMapped = tableIcon !== undefined; - const mappingCol = physicalColToMappingCol(physicalCol); - const tableName = - (typeof mappingCol === 'number' - ? mappings?.tableNames[mappingCol] - : undefined) ?? - tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; - const tableLabel = isMapped - ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' - : ''; - // REFACTOR: use new table icons - return `
+ { + const tableIcon = mappings?.mappedHeaders?.[physicalCol]; + const isMapped = tableIcon !== undefined; + const mappingCol = physicalColToMappingCol(physicalCol); + const tableName = + (typeof mappingCol === 'number' + ? mappings?.tableNames[mappingCol] + : undefined) ?? + tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; + const tableLabel = isMapped + ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' + : ''; + // REFACTOR: use new table icons + return `
${ isMapped ? `
`; - }} - hiddenColumns={{ - // Hide the disambiguation column - columns: [dataset.columns.length], - indicators: false, - // @ts-expect-error Typing doesn't match for handsontable 12.1.0, fixed in 14 - copyPasteEnabled: false, - }} - hiddenRows={{ - rows: [], - indicators: false, - // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error - // Typing doesn't match for handsontable 12.1.0, fixed in 14 - // copyPasteEnabled: false, - }} - minSpareRows={userPreferences.get( - 'workBench', - 'editor', - 'minSpareRows' - )} - comments={{ - displayDelay: 100, - }} - commentedCellClassName="htCommentCell" - placeholderCellClassName="htPlaceholder" - invalidCellClassName="-" - rowHeaders={true} - autoWrapCol={userPreferences.get('workBench', 'editor', 'autoWrapCol')} - autoWrapRow={userPreferences.get('workBench', 'editor', 'autoWrapRow')} - enterBeginsEditing={userPreferences.get( - 'workBench', - 'editor', - 'enterBeginsEditing' - )} - enterMoves={ - userPreferences.get('workBench', 'editor', 'enterMoveDirection') === - 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 } - } - tabMoves={ - userPreferences.get('workBench', 'editor', 'tabMoveDirection') === - 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 } - } - manualColumnResize={true} - manualColumnMove={true} - outsideClickDeselects={false} - multiColumnSorting={true} - sortIndicator={true} - language={LANGUAGE} - licenseKey="non-commercial-and-evaluation" - stretchH="all" - readOnly={isUploaded || !hasPermission('/workbench/dataset', 'update')} - // contextMenu={contextMenuConfig as Settings} - /> - + }} + columns={Array.from( + // Last column is invisible and contains disambiguation metadata + { length: dataset.columns.length + 1 }, + (_, physicalCol) => ({ + // Get data from nth column for nth column + data: physicalCol, + }) + )} + commentedCellClassName="htCommentCell" + comments={{ + displayDelay: 100, + }} + data={data as (string | null)[][]} + enterBeginsEditing={userPreferences.get( + 'workBench', + 'editor', + 'enterBeginsEditing' + )} + enterMoves={ + userPreferences.get('workBench', 'editor', 'enterMoveDirection') === + 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 } + } + hiddenColumns={{ + // Hide the disambiguation column + columns: [dataset.columns.length], + indicators: false, + // @ts-expect-error Typing doesn't match for handsontable 12.1.0, fixed in 14 + copyPasteEnabled: false, + }} + hiddenRows={{ + rows: [], + indicators: false, + // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // Typing doesn't match for handsontable 12.1.0, fixed in 14 + // copyPasteEnabled: false, + }} + invalidCellClassName="-" + language={LANGUAGE} + licenseKey="non-commercial-and-evaluation" + manualColumnMove={true} + manualColumnResize={true} + minSpareRows={userPreferences.get('workBench', 'editor', 'minSpareRows')} + multiColumnSorting={true} + outsideClickDeselects={false} + placeholderCellClassName="htPlaceholder" + ref={hotRef} + rowHeaders={true} + sortIndicator={true} + stretchH="all" + tabMoves={ + userPreferences.get('workBench', 'editor', 'tabMoveDirection') === 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 } + } + readOnly={isUploaded || !hasPermission('/workbench/dataset', 'update')} + // ContextMenu={contextMenuConfig as Settings} + /> ); } From b74299384dfcf106d7b1fb2d30b4e205a72ba350 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 15 Mar 2024 17:30:17 -0400 Subject: [PATCH 006/153] Add ability to export to DataSetMeta components --- .../frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx index ed46be83fe2..f56026d9aa7 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx @@ -350,7 +350,7 @@ export function DataSetMeta({ ); } -function DataSetName({ +export function DataSetName({ dataset, getRowCount, }: { @@ -405,7 +405,7 @@ const fetchListOfUsers = async (): Promise< ({ records: users }) => users.filter(({ id }) => id !== userInformation.id) ); -function ChangeOwner({ +export function ChangeOwner({ dataset, onClose: handleClose, }: { From c66360219fbe9efbf4c6775ed36222b23ca2b98c Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 18 Mar 2024 12:58:15 -0400 Subject: [PATCH 007/153] Add data as a prop to WbSpreadsheet --- .../components/WorkBench/WbSpreadsheet.tsx | 145 +++++++++++++++++- 1 file changed, 137 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index f2b831489c8..59bb8652691 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -4,6 +4,7 @@ * TODO: * - Add context menu settings * - Upgrade to handsontable14 to fix copyPasteEnabled error + * - Fix font size and color in the table */ import { HotTable } from '@handsontable/react'; @@ -12,7 +13,7 @@ import React from 'react'; import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; import { f } from '../../utils/functools'; -import type { RA } from '../../utils/types'; +import { ensure, type IR, type RA } from '../../utils/types'; import { legacyNonJsxIcons } from '../Atoms/Icons'; import { getTable } from '../DataModel/tables'; import { hasPermission } from '../Permissions/helpers'; @@ -24,18 +25,14 @@ export function WbSpreadsheet({ dataset, hotRef, isUploaded, + data }: { readonly dataset: Dataset; readonly hotRef: any; readonly isUploaded: boolean; + readonly data: RA>; }): JSX.Element { - const data = React.useMemo>>( - () => - dataset.rows.length === 0 - ? [Array.from(dataset.columns).fill(null)] - : dataset.rows, - [dataset.rows] - ); + const mappings = parseWbMappings(dataset); const physicalColToMappingCol = (physicalCol: number): number | undefined => @@ -43,6 +40,138 @@ export function WbSpreadsheet({ ({ headerName }) => headerName === dataset.columns[physicalCol] ); + // const contextMenuConfig = { + // items: ensure< + // IR< + // | Handsontable.plugins.ContextMenu.MenuItemConfig + // | Handsontable.plugins.ContextMenu.PredefinedMenuItemKey + // > + // >()( + // isUploaded + // ? ({ + // // Display uploaded record + // upload_results: { + // disableSelection: true, + // isCommand: false, + // renderer: (_hot, wrapper) => { + // const { endRow: visualRow, endCol: visualCol } = + // getSelectedRegions(hotRef.current).at(-1) ?? {}; + // const physicalRow = hotRef?.current.toPhysicalRow(visualRow ?? 0); + // const physicalCol = hotRef?.current.toPhysicalColumn(visualCol ?? 0); + + // const createdRecords = + // this.validation.uploadResults.newRecords[physicalRow]?.[ + // physicalCol + // ]; + + // if ( + // visualRow === undefined || + // visualCol === undefined || + // createdRecords === undefined || + // !this.cells.getCellMeta(physicalRow, physicalCol, 'isNew') + // ) { + // wrapper.textContent = wbText.noUploadResultsAvailable(); + // wrapper.parentElement?.classList.add('htDisabled'); + // const span = document.createElement('span'); + // span.style.display = 'none'; + // return span; + // } + + // wrapper.setAttribute( + // 'class', + // `${wrapper.getAttribute('class')} flex flex-col !m-0 + // pb-1 wb-uploaded-view-context-menu` + // ); + // wrapper.innerHTML = createdRecords + // .map(([tableName, recordId, label]) => { + // const tableLabel = + // label === '' + // ? strictGetTable(tableName).label + // : label; + // // REFACTOR: use new table icons + // const tableIcon = getIcon(tableName) ?? unknownIcon; + + // return ` + // + // ${tableLabel} + // ${legacyNonJsxIcons.link} + // `; + // }) + // .join(''); + + // const div = document.createElement('div'); + // div.style.display = 'none'; + // return div; + // }, + // }, + // } as const) + // : ({ + // row_above: { + // disabled: () => + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + // !hasPermission('/workbench/dataset', 'update'), + // }, + // row_below: { + // disabled: () => + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + // !hasPermission('/workbench/dataset', 'update'), + // }, + // remove_row: { + // disabled: () => { + // if ( + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + // !hasPermission('/workbench/dataset', 'update') + // ) + // return true; + // // Or if called on the last row + // const selectedRegions = getSelectedRegions(hot); + // return ( + // selectedRegions.length === 1 && + // selectedRegions[0].startRow === this.data.length - 1 && + // selectedRegions[0].startRow === selectedRegions[0].endRow + // ); + // }, + // }, + // disambiguate: { + // name: wbText.disambiguate(), + // disabled: (): boolean => + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + // !this.disambiguation.isAmbiguousCell() || + // !hasPermission('/workbench/dataset', 'update'), + // callback: () => + // this.disambiguation.openDisambiguationDialog(), + // }, + // separator_1: '---------', + // fill_down: this.wbUtils.fillCellsContextMenuItem('down'), + // fill_up: this.wbUtils.fillCellsContextMenuItem('up'), + // separator_2: '---------', + // undo: { + // disabled: () => + // typeof this.uploadedView === 'function' || + // this.hot?.isUndoAvailable() !== true || + // !hasPermission('/workbench/dataset', 'update'), + // }, + // redo: { + // disabled: () => + // typeof this.uploadedView === 'function' || + // this.hot?.isRedoAvailable() !== true || + // !hasPermission('/workbench/dataset', 'update'), + // }, + // } as const) + // ), + // } + return ( Date: Mon, 18 Mar 2024 12:58:41 -0400 Subject: [PATCH 008/153] Add component for workbench toolkit --- .../lib/components/WorkBench/WbToolkit.tsx | 167 ++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx new file mode 100644 index 00000000000..9c588eeb1ed --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx @@ -0,0 +1,167 @@ +import React from 'react'; + +import { hasPermission, hasTablePermission } from '../Permissions/helpers'; +import { ChangeOwner } from './DataSetMeta'; +import { localityText } from '../../localization/locality'; +import { getSelectedLocalities, WbGeoLocate } from './GeoLocate'; +import { getLocalitiesDataFromSpreadsheet } from '../Leaflet/wbLocalityDataExtractor'; +import { getSelectedLast, getVisualHeaders } from './hotHelpers'; +import { LeafletMap } from '../Leaflet/Map'; +import { CoordinateConverter } from './CoordinateConverter'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; +import type { RA } from '../../utils/types'; +import { Button } from '../Atoms/Button'; +import { raise } from '../Errors/Crash'; +import { userPreferences } from '../Preferences/userPreferences'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import { downloadDataSet } from './helpers'; +import type { WbMapping } from './mapping'; +import type { HandlersObject } from './WbView'; + +export function WbToolkit({ + dataset, + hotRef, + toolkitOptions, + mappings, + data, +}: { + readonly dataset: Dataset; + readonly hotRef: any; + readonly toolkitOptions: HandlersObject; + readonly mappings: WbMapping; + readonly data: RA>; +}): JSX.Element { + const hot = hotRef.current.hotInstance; + + const handleExport = React.useCallback((): void => { + const delimiter = userPreferences.get( + 'workBench', + 'editor', + 'exportFileDelimiter' + ); + + downloadDataSet( + dataset.name, + dataset.rows, + dataset.columns, + delimiter + ).catch(raise); + }, [dataset]); + + const localityPoints = React.useMemo(() => { + const selection = getSelectedLocalities( + hot, + dataset.columns, + mappings.localityColumns, + false + ); + + if (!selection) return undefined; + + return getLocalitiesDataFromSpreadsheet( + mappings.localityColumns, + selection.visualRows.map((visualRow) => hot!.getDataAtRow(visualRow)), + getVisualHeaders(hot, dataset.columns), + selection.visualRows + ); + }, [mappings.localityColumns]); + + const hasLocality = mappings ? mappings.localityColumns.length > 0 : false; + return ( +
+ {hasPermission('/workbench/dataset', 'transfer') && + hasTablePermission('SpecifyUser', 'read') ? ( + + {wbText.changeOwner()} + + ) : undefined} + + {commonText.export()} + + + {hasPermission('/workbench/dataset', 'update') && ( + <> + + {wbText.convertCoordinates()} + + + {localityText.geoLocate()} + + + )} + + {localityText.geoMap()} + + {toolkitOptions.changeOwner.show && ( + + )} + {toolkitOptions.coordinatesConversion.show && ( + + )} + {toolkitOptions.geoLocate.show && mappings && ( + + )} + {toolkitOptions.leafletMap.show && ( + { + if (localityPoints === undefined) return; + const rowNumber = localityPoints[localityPoint].rowNumber.value; + if (typeof rowNumber !== 'number') + throw new Error('rowNumber must be a number'); + const [_currentRow, currentCol] = getSelectedLast(hot!); + hot?.scrollViewportTo(rowNumber, currentCol); + // Select entire row + hot?.selectRows(rowNumber); + }} + /> + )} +
+ ); +} From 26b65d424d691643aad8de2d62e585ad74b7ef17 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:00:10 -0400 Subject: [PATCH 009/153] Add a React component that mirrors WbView --- .../lib/components/WorkBench/WbView.tsx | 301 +++++++++++++++++- 1 file changed, 300 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index e4e5e308eed..57e431559f1 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -53,9 +53,308 @@ import { wbViewTemplate } from './Template'; import { WbActions } from './WbActions'; import { WbUtils } from './WbUtils'; import { WbValidation } from './WbValidation'; +import type { LocalizedString } from 'typesafe-i18n'; +import { className } from '../Atoms/className'; +import { Input } from '../Atoms/Form'; +import { DataSetName } from './DataSetMeta'; +import { WbSpreadsheet } from './WbSpreadsheet'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { WbToolkit } from './WbToolkit'; export type WbStatus = 'unupload' | 'upload' | 'validate'; +function Navigation({ + name, + label, +}: { + readonly name: string; + readonly label: LocalizedString; +}): JSX.Element { + return ( + + + {'<'} + + + {label} (0/ + 0) + + + {'>'} + + + ); +} + +export type HandlersObject = { + [key: string]: { + show: boolean; + open: () => void; + close: () => void; + toggle: () => void; + }; +}; + +// Returns an object consisting of handler objects for dialog actions in the Workbench +function useWbViewHandlers() { + const handlers = { + toolkit: useBooleanState(), + devPlan: useBooleanState(), + changeOwner: useBooleanState(), + geoLocate: useBooleanState(), + leafletMap: useBooleanState(), + coordinatesConversion: useBooleanState(), + }; + + // convert useBooleanState()'s resulting array to an object + const handlersObject: HandlersObject = {}; + for (const [name, state] of Object.entries(handlers)) { + const [show, open, close, toggle] = state; + handlersObject[name] = { show, open, close, toggle }; + } + + return handlersObject; +} + +export function WbViewReact({ + dataset, + hotRef, + handleDatasetDelete, +}: { + readonly dataset: Dataset; + readonly hotRef: any; + handleDatasetDelete: () => void; +}): JSX.Element { + const data = React.useMemo>>( + () => + dataset.rows.length === 0 + ? [Array.from(dataset.columns).fill(null)] + : dataset.rows, + [dataset.rows] + ); + const isMapped = Boolean(dataset.uploadplan); + const isUploaded = + dataset.uploadresult !== null && dataset.uploadresult.success; + const canUpdate = hasPermission('/workbench/dataset', 'update'); + const [canLiveValidate] = userPreferences.use( + 'workBench', + 'general', + 'liveValidation' + ); + const { toolkit, devPlan, ...toolkitOptions } = useWbViewHandlers(); + const mappings = React.useMemo( + (): WbMapping | undefined => parseWbMappings(dataset), + [dataset] + ); + const hot = hotRef.current ? hotRef.current.hotInstance : undefined; + return ( + <> +
+
+ + hot + ? dataset.rows.length + : hot.countRows() - hot.countEmptyRows(true) + } + /> +
+ + {commonText.tools()} + + + {/* This button is here for debugging only */} + + [DEV] Show Plan + + {canUpdate || isMapped ? ( + + {wbPlanText.dataMapper()} + + ) : undefined} + {!isUploaded && hasPermission('/workbench/dataset', 'validate') && ( + <> + + {wbText.dataCheck()} + + + {wbText.validate()} + + + )} + + {commonText.results()} + + {isUploaded ? ( + hasPermission('/workbench/dataset', 'unupload') && ( + + {wbText.rollback()} + + ) + ) : ( + <> + {hasPermission('/workbench/dataset', 'upload') && ( + + {wbText.upload()} + + )} + {hasPermission('/workbench/dataset', 'update') && ( + <> + + {wbText.revert()} + + + {commonText.save()} + + + )} + + )} +
+ {toolkit.show && ( + + )} + {devPlan.show && ( + { + overwriteReadOnly(dataset, 'uploadplan', plan); + // TODO: figure out this trigger + // trigger('refresh'); + }} + onClose={devPlan.close} + onDeleted={handleDatasetDelete} + /> + )} +
+
+ +
+
+
+ +
+ +
+ {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( +
+ +
+ ) : undefined} + + + + {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( + + ) : undefined} + + {!isUploaded && ( + + )} +
+ + ); +} + // REFACTOR: when rewriting to React, add ErrorBoundaries // REFACTOR: rewrite to React @@ -119,7 +418,7 @@ export class WbView extends Backbone.View { public refreshInitiatedBy: WbStatus | undefined, // eslint-disable-next-line functional/prefer-readonly-type public refreshInitiatorAborted: boolean, - public readonly element: HTMLElement, + public readonly element: HTMLElement, // shouldn't be needed in React public readonly options: { readonly onSetUnloadProtect: (unloadProtect: boolean) => void; readonly onDeleted: () => void; From 85d9e1e365f3f91c2bbbf4d504a41ad00af6c16b Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 18 Mar 2024 13:43:14 -0400 Subject: [PATCH 010/153] Move upload plan into toolkit --- .../lib/components/WorkBench/WbToolkit.tsx | 45 +++++++++++++++---- .../lib/components/WorkBench/WbView.tsx | 26 +---------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx index 9c588eeb1ed..b3632f78698 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx @@ -18,6 +18,9 @@ import type { Dataset } from '../WbPlanView/Wrapped'; import { downloadDataSet } from './helpers'; import type { WbMapping } from './mapping'; import type { HandlersObject } from './WbView'; +import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; +import { DevShowPlan } from './DevShowPlan'; +import { overwriteReadOnly } from '../../utils/types'; export function WbToolkit({ dataset, @@ -25,12 +28,14 @@ export function WbToolkit({ toolkitOptions, mappings, data, + handleDatasetDelete, }: { readonly dataset: Dataset; readonly hotRef: any; readonly toolkitOptions: HandlersObject; readonly mappings: WbMapping; readonly data: RA>; + handleDatasetDelete: () => void; }): JSX.Element { const hot = hotRef.current.hotInstance; @@ -76,14 +81,24 @@ export function WbToolkit({ > {hasPermission('/workbench/dataset', 'transfer') && hasTablePermission('SpecifyUser', 'read') ? ( - - {wbText.changeOwner()} - + <> + + {wbText.changeOwner()} + + + {wbText.uploadPlan()} + + ) : undefined} {commonText.export()} @@ -129,6 +144,20 @@ export function WbToolkit({ onClose={toolkitOptions.changeOwner.close} /> )} + {toolkitOptions.devPlan.show && ( + { + overwriteReadOnly(dataset, 'uploadplan', plan); + // TODO: figure out this trigger + // trigger('refresh'); + }} + onClose={toolkitOptions.devPlan.close} + onDeleted={handleDatasetDelete} + /> + )} {toolkitOptions.coordinatesConversion.show && ( parseWbMappings(dataset), [dataset] @@ -196,16 +196,6 @@ export function WbViewReact({ {commonText.tools()} - {/* This button is here for debugging only */} - - [DEV] Show Plan - {canUpdate || isMapped ? ( {wbPlanText.dataMapper()} @@ -286,19 +276,7 @@ export function WbViewReact({ toolkitOptions={toolkitOptions} mappings={mappings as WbMapping} data={data} - /> - )} - {devPlan.show && ( - { - overwriteReadOnly(dataset, 'uploadplan', plan); - // TODO: figure out this trigger - // trigger('refresh'); - }} - onClose={devPlan.close} - onDeleted={handleDatasetDelete} + handleDatasetDelete={handleDatasetDelete} /> )}
From 70a7e90aa00e1388caa78603bbf042582b3bab87 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 18 Mar 2024 14:18:07 -0400 Subject: [PATCH 011/153] Add advanced search button --- .../lib/components/WorkBench/WbView.tsx | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 735bb941e17..f42306adc01 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -60,6 +60,9 @@ import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; import { WbToolkit } from './WbToolkit'; +import { getCache } from '../../utils/cache'; +import { WbAdvancedSearch } from './AdvancedSearch'; +import type { WbSearchPreferences } from './AdvancedSearch'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -171,6 +174,23 @@ export function WbViewReact({ [dataset] ); const hot = hotRef.current ? hotRef.current.hotInstance : undefined; + const defaultSearchPreferences: WbSearchPreferences = { + navigation: { + direction: 'columnFirst', + }, + search: { + fullMatch: true, + caseSensitive: true, + useRegex: false, + liveUpdate: true, + }, + replace: { + replaceMode: 'replaceAll', + }, + }; + let searchPreferences = + getCache('workbench', 'searchProperties') ?? defaultSearchPreferences; + const initialNavigationDirection = searchPreferences.navigation.direction; return ( <>
) : undefined} - + + { + // searchPreferences = newSearchPreferences; + // if ( + // searchPreferences.navigation.direction !== + // initialNavigationDirection + // ) { + // this.wbView.cells.flushIndexedCellData = true; + // initialNavigationDirection = + // searchPreferences.navigation.direction; + // } + // if (searchPreferences.search.liveUpdate) + // this.searchCells({ + // key: 'SettingsChange', + // }).catch(softFail); + }} + /> + {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( From 8cc8012fad47078e997a0a803d9eb07ff0326e94 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:58:17 -0400 Subject: [PATCH 012/153] Add rollback dialog box --- .../js_src/lib/components/WorkBench/WbView.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index f42306adc01..99c43caf9bf 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -63,6 +63,7 @@ import { WbToolkit } from './WbToolkit'; import { getCache } from '../../utils/cache'; import { WbAdvancedSearch } from './AdvancedSearch'; import type { WbSearchPreferences } from './AdvancedSearch'; +import { RollbackConfirmation } from './Components'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -126,6 +127,7 @@ export type HandlersObject = { function useWbViewHandlers() { const handlers = { toolkit: useBooleanState(), + unupload: useBooleanState(), devPlan: useBooleanState(), changeOwner: useBooleanState(), geoLocate: useBooleanState(), @@ -168,7 +170,7 @@ export function WbViewReact({ 'general', 'liveValidation' ); - const { toolkit, ...toolkitOptions } = useWbViewHandlers(); + const { toolkit, unupload, ...toolkitOptions } = useWbViewHandlers(); const mappings = React.useMemo( (): WbMapping | undefined => parseWbMappings(dataset), [dataset] @@ -250,8 +252,9 @@ export function WbViewReact({ hasPermission('/workbench/dataset', 'unupload') && ( {wbText.rollback()} @@ -299,6 +302,13 @@ export function WbViewReact({ handleDatasetDelete={handleDatasetDelete} /> )} + {unupload.show && ( + {}} + /> + )}
Date: Tue, 19 Mar 2024 11:12:41 -0400 Subject: [PATCH 013/153] Add index file for Workbench --- .../js_src/lib/components/WorkBench/index.tsx | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx new file mode 100644 index 00000000000..932bb8f777e --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -0,0 +1,103 @@ +import React from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import type { LocalizedString } from 'typesafe-i18n'; + +import { useUnloadProtect } from '../../hooks/navigation'; +import { useAsyncState } from '../../hooks/useAsyncState'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { useErrorContext } from '../../hooks/useErrorContext'; +import { commonText } from '../../localization/common'; +import { localityText } from '../../localization/locality'; +import { wbPlanText } from '../../localization/wbPlan'; +import { wbText } from '../../localization/workbench'; +import { ajax } from '../../utils/ajax'; +import { f } from '../../utils/functools'; +import type { GetSet, RA } from '../../utils/types'; +import { replaceItem } from '../../utils/utils'; +import { Button } from '../Atoms/Button'; +import { className } from '../Atoms/className'; +import { Input } from '../Atoms/Form'; +import { Link } from '../Atoms/Link'; +import { LoadingContext } from '../Core/Contexts'; +import { useMenuItem } from '../Header/MenuContext'; +import { treeRanksPromise } from '../InitialContext/treeRanks'; +import { Dialog } from '../Molecules/Dialog'; +import { Portal } from '../Molecules/Portal'; +import { hasPermission, hasTablePermission } from '../Permissions/helpers'; +import { userPreferences } from '../Preferences/userPreferences'; +import { NotFoundView } from '../Router/NotFoundView'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { WbStatus } from './WbView'; +import { WbView as WbViewClass, WbViewReact } from './WbView'; +import { DataSetName } from './DataSetMeta'; +import { WbSpreadsheet } from './WbSpreadsheet'; +import Handsontable from 'handsontable'; + +const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); + +const fetchDataSet = async ( + dataSetId: number | undefined + ): Promise => + typeof dataSetId === 'number' + ? ajax(`/api/workbench/dataset/${dataSetId}/`, { + headers: { Accept: 'application/json' }, + }).then(({ data }) => data) + : undefined; + +// BUG: intercept 403 (if dataset has been transferred to another user) +function useDataSet( + dataSetId: number | undefined + ): GetSet { + return useAsyncState( + React.useCallback(async () => fetchDataSet(dataSetId), [dataSetId]), + true + ); + } + +export function WorkBenchReact(): JSX.Element | null { + useMenuItem('workBench'); + + const [treeRanksLoaded = false] = useAsyncState(fetchTreeRanks, true); + const { id } = useParams(); + const dataSetId = f.parseInt(id); + + const [dataSet, setDataSet] = useDataSet(dataSetId); + useErrorContext('dataSet', dataSet); + const loading = React.useContext(LoadingContext); + const [isDeleted, handleDeleted] = useBooleanState(); + const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); + + const navigate = useNavigate(); + const hotRef = React.useRef(null); + + // temporary null check, replace with loading screen? + if (!dataSet) return null; + + return dataSetId === undefined ? ( + + ) : isDeleted ? ( + <>{wbText.dataSetDeletedOrNotFound()} + ) : isDeletedConfirmation ? ( + navigate('/specify/', { replace: true })} + > + {wbText.dataSetDeletedDescription()} + + ) : ( + <> +
+
+ +
+
+ + ); +} From 2ba5158bb75ebc547bd5fc59469437627ab2bfa8 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 19 Mar 2024 13:47:14 -0400 Subject: [PATCH 014/153] Add upload button dialogs --- .../lib/components/WorkBench/WbView.tsx | 79 ++++++++++++++++--- 1 file changed, 69 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 99c43caf9bf..12c523e7b6f 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -50,7 +50,7 @@ import { parseWbMappings } from './mapping'; import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; import { wbViewTemplate } from './Template'; -import { WbActions } from './WbActions'; +import { WbActions, WbActionsReact } from './WbActions'; import { WbUtils } from './WbUtils'; import { WbValidation } from './WbValidation'; import type { LocalizedString } from 'typesafe-i18n'; @@ -114,19 +114,23 @@ function Navigation({ ); } +export type DialogHandlers = { + show: boolean; + open: () => void; + close: () => void; + toggle: () => void; +}; + export type HandlersObject = { - [key: string]: { - show: boolean; - open: () => void; - close: () => void; - toggle: () => void; - }; + [key: string]: DialogHandlers }; // Returns an object consisting of handler objects for dialog actions in the Workbench function useWbViewHandlers() { const handlers = { toolkit: useBooleanState(), + upload: useBooleanState(), + noUploadPlan: useBooleanState(), unupload: useBooleanState(), devPlan: useBooleanState(), changeOwner: useBooleanState(), @@ -170,7 +174,8 @@ export function WbViewReact({ 'general', 'liveValidation' ); - const { toolkit, unupload, ...toolkitOptions } = useWbViewHandlers(); + const { toolkit, upload, noUploadPlan, unupload, ...toolkitOptions } = + useWbViewHandlers(); const mappings = React.useMemo( (): WbMapping | undefined => parseWbMappings(dataset), [dataset] @@ -193,6 +198,19 @@ export function WbViewReact({ let searchPreferences = getCache('workbench', 'searchProperties') ?? defaultSearchPreferences; const initialNavigationDirection = searchPreferences.navigation.direction; + const actionsRef = React.useRef( + new WbActionsReact(dataset, mappings as WbMapping, noUploadPlan, upload) + ); + const [mode, setMode] = React.useState(undefined); + React.useEffect(() => { + if ( + !isUploaded && + (mappings?.lines ?? []).length === 0 && + hasPermission('/workbench/dataset', 'update') + ) { + noUploadPlan.open(); + } + }, []); return ( <>
{ + const mode = "upload"; + setMode(mode); + actionsRef.current.upload(mode); + }} > {wbText.upload()} @@ -307,7 +329,44 @@ export function WbViewReact({ dataSetId={dataset.id} onClose={unupload.close} onRollback={() => {}} - /> + /> + )} + {mode === 'upload' && upload.show && ( + + {commonText.cancel()} + { + actionsRef.current.startUpload(mode); + upload.close(); + }} + > + {wbText.upload()} + + + } + header={wbText.startUpload()} + onClose={upload.close} + > + {wbText.startUploadDescription()} + + )} + {noUploadPlan.show && ( + + {commonText.close()} + + {commonText.create()} + + + } + header={wbPlanText.noUploadPlan()} + onClose={noUploadPlan.close} + > + {wbPlanText.noUploadPlanDescription()} + )}
From f529f043c4a2c9f112bf81f8f4e19b5d58f9edda Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:26:56 -0400 Subject: [PATCH 015/153] Add status component --- .../lib/components/WorkBench/WbView.tsx | 57 +++++++++++++++++-- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 12c523e7b6f..74957926818 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -64,6 +64,8 @@ import { getCache } from '../../utils/cache'; import { WbAdvancedSearch } from './AdvancedSearch'; import type { WbSearchPreferences } from './AdvancedSearch'; import { RollbackConfirmation } from './Components'; +import { WbStatus as WbStatusComponent } from './Status'; +import type { Status } from '../WbPlanView/Wrapped'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -122,7 +124,7 @@ export type DialogHandlers = { }; export type HandlersObject = { - [key: string]: DialogHandlers + [key: string]: DialogHandlers; }; // Returns an object consisting of handler objects for dialog actions in the Workbench @@ -132,6 +134,7 @@ function useWbViewHandlers() { upload: useBooleanState(), noUploadPlan: useBooleanState(), unupload: useBooleanState(), + statusComponent: useBooleanState(), devPlan: useBooleanState(), changeOwner: useBooleanState(), geoLocate: useBooleanState(), @@ -174,8 +177,14 @@ export function WbViewReact({ 'general', 'liveValidation' ); - const { toolkit, upload, noUploadPlan, unupload, ...toolkitOptions } = - useWbViewHandlers(); + const { + toolkit, + upload, + noUploadPlan, + unupload, + statusComponent, + ...toolkitOptions + } = useWbViewHandlers(); const mappings = React.useMemo( (): WbMapping | undefined => parseWbMappings(dataset), [dataset] @@ -198,8 +207,19 @@ export function WbViewReact({ let searchPreferences = getCache('workbench', 'searchProperties') ?? defaultSearchPreferences; const initialNavigationDirection = searchPreferences.navigation.direction; + const checkDeletedFail = (statusCode: number): boolean => { + if (statusCode === Http.NOT_FOUND) handleDatasetDelete(); + return statusCode === Http.NOT_FOUND; + }; const actionsRef = React.useRef( - new WbActionsReact(dataset, mappings as WbMapping, noUploadPlan, upload) + new WbActionsReact( + dataset, + mappings as WbMapping, + noUploadPlan, + upload, + statusComponent, + checkDeletedFail + ) ); const [mode, setMode] = React.useState(undefined); React.useEffect(() => { @@ -284,7 +304,7 @@ export function WbViewReact({ aria-haspopup="dialog" className="wb-upload" onClick={() => { - const mode = "upload"; + const mode = 'upload'; setMode(mode); actionsRef.current.upload(mode); }} @@ -368,6 +388,33 @@ export function WbViewReact({ {wbPlanText.noUploadPlanDescription()} )} + {mode && statusComponent.show && ( + { + console.log('Aborted:', wasAborted); + statusComponent.close(); + // this.wbView.trigger('refresh', mode, wasAborted); + }} + /> + )}
Date: Tue, 19 Mar 2024 14:28:16 -0400 Subject: [PATCH 016/153] Add a function for triggering WbView refresh --- .../js_src/lib/components/WorkBench/WbView.tsx | 4 +++- .../js_src/lib/components/WorkBench/index.tsx | 14 +++++++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 74957926818..934f622faac 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -156,10 +156,12 @@ export function WbViewReact({ dataset, hotRef, handleDatasetDelete, + triggerRefresh }: { readonly dataset: Dataset; readonly hotRef: any; - handleDatasetDelete: () => void; + readonly handleDatasetDelete: () => void; + readonly triggerRefresh: () => void; }): JSX.Element { const data = React.useMemo>>( () => diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 932bb8f777e..3564ba285c9 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -66,12 +66,21 @@ export function WorkBenchReact(): JSX.Element | null { const loading = React.useContext(LoadingContext); const [isDeleted, handleDeleted] = useBooleanState(); const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); + const [refresh, setRefresh] = React.useState(0); const navigate = useNavigate(); const hotRef = React.useRef(null); // temporary null check, replace with loading screen? - if (!dataSet) return null; + if (!dataSet || !treeRanksLoaded) return null; + + const triggerRefresh = () => { + setRefresh((previous) => previous + 1); + } + + React.useEffect(() => { + loading(fetchDataSet(dataSet!.id).then(setDataSet)); + }, [refresh]); return dataSetId === undefined ? ( @@ -92,9 +101,8 @@ export function WorkBenchReact(): JSX.Element | null {
From 4a3b9b9f9995b95b0b3c5d39d3e7830bc4021247 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 19 Mar 2024 18:01:28 -0400 Subject: [PATCH 017/153] Update routes to point to new index file - Component will be renamed at the end after cleaning up Backbone.js components --- specifyweb/frontend/js_src/lib/components/Router/Routes.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index 70b41e9eb52..00f668560ec 100644 --- a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx +++ b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx @@ -200,7 +200,7 @@ export const routes: RA = [ { path: ':id', element: () => - import('../WorkBench/Template').then(({ WorkBench }) => WorkBench), + import('../WorkBench/index').then(({ WorkBenchReact }) => WorkBenchReact), }, { path: 'import', From 95c29210a4faee0636fa9dadb9c1a1bfc54d3178 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 19 Mar 2024 18:05:51 -0400 Subject: [PATCH 018/153] Refactor WbActions to make upload and rollback work --- .../lib/components/WorkBench/WbActions.tsx | 181 +++++++++++++++++- .../lib/components/WorkBench/WbToolkit.tsx | 14 +- .../lib/components/WorkBench/WbView.tsx | 136 +++++++++++-- .../js_src/lib/components/WorkBench/index.tsx | 7 +- 4 files changed, 314 insertions(+), 24 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx index f87c427a05e..a261e5bc90e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx @@ -15,7 +15,186 @@ import type { WbCellCounts } from './CellMeta'; import { RollbackConfirmation } from './Components'; import { CreateRecordSetButton } from './RecordSet'; import { WbStatus as WbStatusComponent } from './Status'; -import type { WbStatus, WbView } from './WbView'; +import type { DialogHandlers, WbStatus, WbView } from './WbView'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { WbMapping } from './mapping'; +import type { RA } from '../../utils/types'; + +/* eslint-disable functional/no-this-expression */ +export class WbActionsReact { + // eslint-disable-next-line functional/prefer-readonly-type + public hasUnSavedChanges: boolean = false; + + public constructor( + private readonly data: RA>, + private readonly dataset: Dataset, + private readonly mappings: WbMapping, + private readonly noUploadPlan: DialogHandlers, + private readonly uploadDialog: DialogHandlers, + private readonly statusComponent: DialogHandlers, + private readonly checkDeletedFail: (statusCode: number) => boolean, + private readonly triggerRefresh: () => void, + private readonly saveProgressBar: DialogHandlers, + private refreshInitiatedBy: React.MutableRefObject, + private refreshInitiatorAborted: React.MutableRefObject, + private readonly operationCompleted: DialogHandlers, + private readonly operationAborted: DialogHandlers + ) {} + + // BUG: disable the button if there is nothing to upload + upload(mode: WbStatus): void { + if ((this.mappings?.lines ?? []).length > 0) { + if (mode === 'upload') { + this.uploadDialog.open(); + } else this.startUpload(mode); + } else { + this.noUploadPlan.open(); + } + } + + startUpload(mode: WbStatus): void { + // this.wbView.validation.stopLiveValidation(); + // this.wbView.validation.updateValidationButton(); + ping(`/api/workbench/${mode}/${this.dataset.id}/`, { + method: 'POST', + expectedErrors: [Http.CONFLICT], + }) + .then((statusCode): void => { + this.checkDeletedFail(statusCode); + this.checkConflictFail(statusCode); + }) + .then(() => this.openStatus(mode)); + } + + openStatus(mode: WbStatus): void { + this.refreshInitiatedBy.current = mode; + this.statusComponent.open(); + } + + async save() { + // Clear validation + overwriteReadOnly(this.dataset, 'rowresults', null); + // this.wbView.validation.stopLiveValidation(); + // this.wbView.validation.updateValidationButton(); + + // Show saving progress bar + this.saveProgressBar.open(); + + // Send data + return ping(`/api/workbench/rows/${this.dataset.id}/`, { + method: 'PUT', + body: this.data, + expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], + }) + .then((status) => this.checkDeletedFail(status)) + .then(() => { + this.spreadSheetUpToDate(); + // this.wbView.cells.cellMeta = []; + // this.wbView.wbUtils.searchCells({ key: 'SettingsChange' }); + // this.wbView.hot?.render(); + }) + .finally(this.saveProgressBar.close); + } + + // Check if AJAX failed because Data Set was modified by other session + checkConflictFail(statusCode: number): boolean { + if (statusCode === Http.CONFLICT) + /* + * Upload/Validation/Un-Upload has been initialized by another session + * Need to reload the page to display the new state + */ + this.triggerRefresh(); + return statusCode === Http.CONFLICT; + } + + spreadSheetUpToDate(): void { + if (!this.hasUnSavedChanges) return; + this.hasUnSavedChanges = false; + } + + public operationCompletedMessage(cellCounts: WbCellCounts) { + if (this.refreshInitiatedBy.current === undefined) return; + + const messages = { + validate: + cellCounts.invalidCells === 0 + ? { + header: wbText.validationNoErrors(), + message: ( + <> + {wbText.validationNoErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + } + : { + header: wbText.validationErrors(), + message: ( + <> + {wbText.validationErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + }, + upload: + cellCounts.invalidCells === 0 + ? { + header: wbText.uploadSuccessful(), + message: wbText.uploadSuccessfulDescription(), + } + : { + header: wbText.uploadErrors(), + message: ( + <> + {wbText.uploadErrorsDescription()} +
+
+ {wbText.uploadErrorsSecondDescription()} + + ), + }, + unupload: { + header: wbText.dataSetRollback(), + message: wbText.dataSetRollbackDescription(), + }, + }; + + // refactor to set this as a state or ref + const messageToShow = messages[this.refreshInitiatedBy.current]; + this.operationCompleted.open(); + + // set this in onClose + this.refreshInitiatedBy.current = undefined; + this.refreshInitiatorAborted.current = false; + + return messageToShow; + } + + operationAbortedMessage(): void { + if ( + this.refreshInitiatedBy.current === undefined || + this.refreshInitiatorAborted.current + ) + return; + + this.operationAborted.open(); + + // set this in onClose + this.refreshInitiatedBy.current = undefined; + this.refreshInitiatorAborted.current = false; + } + + spreadSheetChanged(): void { + if (this.hasUnSavedChanges) return; + this.hasUnSavedChanges = true; + } +} + +/* eslint-enable functional/no-this-expression */ /* eslint-disable functional/no-this-expression */ export class WbActions { diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx index b3632f78698..75a83633198 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx @@ -21,6 +21,7 @@ import type { HandlersObject } from './WbView'; import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; import { DevShowPlan } from './DevShowPlan'; import { overwriteReadOnly } from '../../utils/types'; +import { WbActionsReact } from './WbActions'; export function WbToolkit({ dataset, @@ -29,13 +30,15 @@ export function WbToolkit({ mappings, data, handleDatasetDelete, + actions, }: { readonly dataset: Dataset; readonly hotRef: any; readonly toolkitOptions: HandlersObject; readonly mappings: WbMapping; readonly data: RA>; - handleDatasetDelete: () => void; + readonly handleDatasetDelete: () => void; + readonly actions: WbActionsReact; }): JSX.Element { const hot = hotRef.current.hotInstance; @@ -87,6 +90,8 @@ export function WbToolkit({ aria-pressed={toolkitOptions.changeOwner.show} className="wb-change-data-set-owner" onClick={toolkitOptions.changeOwner.open} + disabled={actions.hasUnSavedChanges} + title={actions.hasUnSavedChanges ? wbText.unavailableWhileEditing() : ""} > {wbText.changeOwner()} @@ -100,7 +105,12 @@ export function WbToolkit({ ) : undefined} - + {commonText.export()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 934f622faac..e0c94ca1f36 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -66,6 +66,8 @@ import type { WbSearchPreferences } from './AdvancedSearch'; import { RollbackConfirmation } from './Components'; import { WbStatus as WbStatusComponent } from './Status'; import type { Status } from '../WbPlanView/Wrapped'; +import { loadingBar } from '../Molecules'; +import { CreateRecordSetButton } from './RecordSet'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -135,6 +137,10 @@ function useWbViewHandlers() { noUploadPlan: useBooleanState(), unupload: useBooleanState(), statusComponent: useBooleanState(), + revertChanges: useBooleanState(), + saveProgressBar: useBooleanState(), + operationCompleted: useBooleanState(), + operationAborted: useBooleanState(), devPlan: useBooleanState(), changeOwner: useBooleanState(), geoLocate: useBooleanState(), @@ -156,7 +162,7 @@ export function WbViewReact({ dataset, hotRef, handleDatasetDelete, - triggerRefresh + triggerRefresh, }: { readonly dataset: Dataset; readonly hotRef: any; @@ -185,6 +191,10 @@ export function WbViewReact({ noUploadPlan, unupload, statusComponent, + revertChanges, + saveProgressBar, + operationCompleted, + operationAborted, ...toolkitOptions } = useWbViewHandlers(); const mappings = React.useMemo( @@ -213,17 +223,26 @@ export function WbViewReact({ if (statusCode === Http.NOT_FOUND) handleDatasetDelete(); return statusCode === Http.NOT_FOUND; }; + const mode = React.useRef(undefined); + const refreshInitiatorAborted = React.useRef(false); + const actionsRef = React.useRef( new WbActionsReact( + data, dataset, mappings as WbMapping, noUploadPlan, upload, statusComponent, - checkDeletedFail + checkDeletedFail, + triggerRefresh, + saveProgressBar, + mode, + refreshInitiatorAborted, + operationCompleted, + operationAborted ) ); - const [mode, setMode] = React.useState(undefined); React.useEffect(() => { if ( !isUploaded && @@ -275,6 +294,12 @@ export function WbViewReact({ aria-haspopup="dialog" className="wb-validate" onClick={undefined} + disabled={actionsRef.current.hasUnSavedChanges} + title={ + actionsRef.current.hasUnSavedChanges + ? wbText.unavailableWhileEditing() + : '' + } > {wbText.validate()} @@ -283,7 +308,12 @@ export function WbViewReact({ {commonText.results()} @@ -306,10 +336,15 @@ export function WbViewReact({ aria-haspopup="dialog" className="wb-upload" onClick={() => { - const mode = 'upload'; - setMode(mode); - actionsRef.current.upload(mode); + mode.current = 'upload'; + actionsRef.current.upload(mode.current); }} + disabled={actionsRef.current.hasUnSavedChanges} + title={ + actionsRef.current.hasUnSavedChanges + ? wbText.unavailableWhileEditing() + : '' + } > {wbText.upload()} @@ -319,7 +354,8 @@ export function WbViewReact({ {wbText.revert()} @@ -328,6 +364,7 @@ export function WbViewReact({ className="wb-save" variant={className.saveButton} onClick={undefined} + disabled={!actionsRef.current.hasUnSavedChanges} > {commonText.save()} @@ -344,23 +381,24 @@ export function WbViewReact({ mappings={mappings as WbMapping} data={data} handleDatasetDelete={handleDatasetDelete} + actions={actionsRef.current} /> )} {unupload.show && ( {}} + onRollback={() => actionsRef.current.openStatus('unupload')} /> )} - {mode === 'upload' && upload.show && ( + {mode.current === 'upload' && upload.show && ( {commonText.cancel()} { - actionsRef.current.startUpload(mode); + actionsRef.current.startUpload(mode.current as WbStatus); upload.close(); }} > @@ -390,7 +428,7 @@ export function WbViewReact({ {wbPlanText.noUploadPlanDescription()} )} - {mode && statusComponent.show && ( + {mode.current && statusComponent.show && ( { - console.log('Aborted:', wasAborted); + refreshInitiatorAborted.current = wasAborted; statusComponent.close(); - // this.wbView.trigger('refresh', mode, wasAborted); + triggerRefresh(); }} /> )} + {revertChanges.show && ( + + {commonText.cancel()} + + {wbText.revert()} + + + } + header={wbText.revertChanges()} + onClose={revertChanges.close} + > + {wbText.revertChangesDescription()} + + )} + {saveProgressBar.show && ( + + {loadingBar} + + )} + {operationCompleted.show && ( + + { + /* cellCounts.invalidCells === 0 && */ + mode.current === 'upload' && ( + + ) + } + {commonText.close()} + + } + // header={messages[this.wbView.refreshInitiatedBy].header} + onClose={operationCompleted.close} + > + {/* {messages[this.wbView.refreshInitiatedBy].message} */} + + )} + {operationAborted.show && ( + + {mode.current === 'validate' + ? wbText.validationCanceledDescription() + : mode.current === 'unupload' + ? wbText.rollbackCanceledDescription() + : wbText.uploadCanceledDescription()} + + )}
(0); const navigate = useNavigate(); const hotRef = React.useRef(null); @@ -75,12 +74,8 @@ export function WorkBenchReact(): JSX.Element | null { if (!dataSet || !treeRanksLoaded) return null; const triggerRefresh = () => { - setRefresh((previous) => previous + 1); - } - - React.useEffect(() => { loading(fetchDataSet(dataSet!.id).then(setDataSet)); - }, [refresh]); + } return dataSetId === undefined ? ( From 97040eebd29cdc134bf4b7e877e7f1f9731da6f7 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:29:32 -0400 Subject: [PATCH 019/153] Fix context menu in WbSpreadsheet --- .../components/WorkBench/WbSpreadsheet.tsx | 321 ++++++++++-------- 1 file changed, 188 insertions(+), 133 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 59bb8652691..4008a15cdc6 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -2,13 +2,15 @@ * Component for the Handsontable React wrapper * * TODO: - * - Add context menu settings * - Upgrade to handsontable14 to fix copyPasteEnabled error * - Fix font size and color in the table + * - Add hooks to HotTable */ import { HotTable } from '@handsontable/react'; +import Handsontable from 'handsontable'; import React from 'react'; +import type { Settings } from 'handsontable/plugins/contextMenu'; import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; @@ -20,157 +22,208 @@ import { hasPermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; import { parseWbMappings } from './mapping'; +import { getSelectedRegions } from './hotHelpers'; +import { WbValidationReact } from './WbValidation'; +import { WbCellMetaReact } from './CellMeta'; +import { DisambiguationReact } from './DisambiguationLogic'; +import { wbText } from '../../localization/workbench'; +import { strictGetTable } from '../DataModel/tables'; +import { getIcon, unknownIcon } from '../InitialContext/icons'; +import { commonText } from '../../localization/common'; +import { iconClassName } from '../Atoms/Icons'; export function WbSpreadsheet({ dataset, hotRef, isUploaded, - data + data, + validation, + cells, + disambiguation, }: { readonly dataset: Dataset; readonly hotRef: any; readonly isUploaded: boolean; readonly data: RA>; + readonly validation: WbValidationReact; + readonly cells: WbCellMetaReact; + readonly disambiguation: DisambiguationReact; }): JSX.Element { - const mappings = parseWbMappings(dataset); - const physicalColToMappingCol = (physicalCol: number): number | undefined => mappings?.lines.findIndex( ({ headerName }) => headerName === dataset.columns[physicalCol] ); - // const contextMenuConfig = { - // items: ensure< - // IR< - // | Handsontable.plugins.ContextMenu.MenuItemConfig - // | Handsontable.plugins.ContextMenu.PredefinedMenuItemKey - // > - // >()( - // isUploaded - // ? ({ - // // Display uploaded record - // upload_results: { - // disableSelection: true, - // isCommand: false, - // renderer: (_hot, wrapper) => { - // const { endRow: visualRow, endCol: visualCol } = - // getSelectedRegions(hotRef.current).at(-1) ?? {}; - // const physicalRow = hotRef?.current.toPhysicalRow(visualRow ?? 0); - // const physicalCol = hotRef?.current.toPhysicalColumn(visualCol ?? 0); + // Context menu item definitions (common for fillUp and fillDown) + const fillCellsContextMenuItem = ( + mode: 'down' | 'up' + ): Handsontable.plugins.ContextMenu.MenuItemConfig => { + return { + name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), + disabled: () => + // typeof this.wbView.uploadedView === 'function' || + // typeof this.wbView.coordinateConverterView === 'function' || + !hasPermission('/workbench/dataset', 'update') || + ((hotRef.current.hotInstance as Handsontable) + ?.getSelected() + ?.every((selection) => selection[0] === selection[2]) ?? + false), + callback: (_: any, selections: any) => + selections.forEach((selection: any) => + Array.from( + new Array(selection.end.col + 1 - selection.start.col).keys() + ).forEach((index) => { + const startRow = + mode === 'up' ? selection.start.row + 1 : selection.start.row; + const endRow = selection.end.row; + const col = selection.start.col + index; + const value = + mode === 'up' + ? hotRef.current.hotInstance!.getDataAtCell(endRow, col) + : hotRef.current.hotInstance!.getDataAtCell(startRow, col); + hotRef.current.hotInstance?.setDataAtCell( + Array.from({ length: endRow - startRow }, (_, index) => [ + startRow + index + 1, + col, + value, + ]) + ); + }) + ), + }; + }; + + const contextMenuConfig = { + items: ensure< + IR< + | Handsontable.plugins.ContextMenu.MenuItemConfig + | Handsontable.plugins.ContextMenu.PredefinedMenuItemKey + > + >()( + isUploaded + ? ({ + // Display uploaded record + upload_results: { + disableSelection: true, + isCommand: false, + renderer: (_hot, wrapper) => { + const { endRow: visualRow, endCol: visualCol } = + getSelectedRegions(_hot).at(-1) ?? {}; + const physicalRow = _hot.toPhysicalRow(visualRow ?? 0); + const physicalCol = _hot.toPhysicalColumn(visualCol ?? 0); - // const createdRecords = - // this.validation.uploadResults.newRecords[physicalRow]?.[ - // physicalCol - // ]; + const createdRecords = + validation.uploadResults.newRecords[physicalRow]?.[ + physicalCol + ]; - // if ( - // visualRow === undefined || - // visualCol === undefined || - // createdRecords === undefined || - // !this.cells.getCellMeta(physicalRow, physicalCol, 'isNew') - // ) { - // wrapper.textContent = wbText.noUploadResultsAvailable(); - // wrapper.parentElement?.classList.add('htDisabled'); - // const span = document.createElement('span'); - // span.style.display = 'none'; - // return span; - // } + if ( + visualRow === undefined || + visualCol === undefined || + createdRecords === undefined || + !cells.getCellMeta(physicalRow, physicalCol, 'isNew') + ) { + wrapper.textContent = wbText.noUploadResultsAvailable(); + wrapper.parentElement?.classList.add('htDisabled'); + const span = document.createElement('span'); + span.style.display = 'none'; + return span; + } - // wrapper.setAttribute( - // 'class', - // `${wrapper.getAttribute('class')} flex flex-col !m-0 - // pb-1 wb-uploaded-view-context-menu` - // ); - // wrapper.innerHTML = createdRecords - // .map(([tableName, recordId, label]) => { - // const tableLabel = - // label === '' - // ? strictGetTable(tableName).label - // : label; - // // REFACTOR: use new table icons - // const tableIcon = getIcon(tableName) ?? unknownIcon; + wrapper.setAttribute( + 'class', + `${wrapper.getAttribute('class')} flex flex-col !m-0 + pb-1 wb-uploaded-view-context-menu` + ); + wrapper.innerHTML = createdRecords + .map(([tableName, recordId, label]) => { + const tableLabel = + label === '' ? strictGetTable(tableName).label : label; + // REFACTOR: use new table icons + const tableIcon = getIcon(tableName) ?? unknownIcon; - // return ` - // - // ${tableLabel} - // ${legacyNonJsxIcons.link} - // `; - // }) - // .join(''); + return ` + + ${tableLabel} + ${legacyNonJsxIcons.link} + `; + }) + .join(''); - // const div = document.createElement('div'); - // div.style.display = 'none'; - // return div; - // }, - // }, - // } as const) - // : ({ - // row_above: { - // disabled: () => - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - // !hasPermission('/workbench/dataset', 'update'), - // }, - // row_below: { - // disabled: () => - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - // !hasPermission('/workbench/dataset', 'update'), - // }, - // remove_row: { - // disabled: () => { - // if ( - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - // !hasPermission('/workbench/dataset', 'update') - // ) - // return true; - // // Or if called on the last row - // const selectedRegions = getSelectedRegions(hot); - // return ( - // selectedRegions.length === 1 && - // selectedRegions[0].startRow === this.data.length - 1 && - // selectedRegions[0].startRow === selectedRegions[0].endRow - // ); - // }, - // }, - // disambiguate: { - // name: wbText.disambiguate(), - // disabled: (): boolean => - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - // !this.disambiguation.isAmbiguousCell() || - // !hasPermission('/workbench/dataset', 'update'), - // callback: () => - // this.disambiguation.openDisambiguationDialog(), - // }, - // separator_1: '---------', - // fill_down: this.wbUtils.fillCellsContextMenuItem('down'), - // fill_up: this.wbUtils.fillCellsContextMenuItem('up'), - // separator_2: '---------', - // undo: { - // disabled: () => - // typeof this.uploadedView === 'function' || - // this.hot?.isUndoAvailable() !== true || - // !hasPermission('/workbench/dataset', 'update'), - // }, - // redo: { - // disabled: () => - // typeof this.uploadedView === 'function' || - // this.hot?.isRedoAvailable() !== true || - // !hasPermission('/workbench/dataset', 'update'), - // }, - // } as const) - // ), - // } + const div = document.createElement('div'); + div.style.display = 'none'; + return div; + }, + }, + } as const) + : ({ + row_above: { + disabled: () => + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + !hasPermission('/workbench/dataset', 'update'), + }, + row_below: { + disabled: () => + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + !hasPermission('/workbench/dataset', 'update'), + }, + remove_row: { + disabled: () => { + if ( + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + !hasPermission('/workbench/dataset', 'update') + ) + return true; + // Or if called on the last row + const selectedRegions = getSelectedRegions( + hotRef.current.hotInstance + ); + return ( + selectedRegions.length === 1 && + selectedRegions[0].startRow === data.length - 1 && + selectedRegions[0].startRow === selectedRegions[0].endRow + ); + }, + }, + disambiguate: { + name: wbText.disambiguate(), + disabled: (): boolean => + // typeof this.uploadedView === 'function' || + // typeof this.coordinateConverterView === 'function' || + !disambiguation.isAmbiguousCell() || + !hasPermission('/workbench/dataset', 'update'), + callback: () => disambiguation.openDisambiguationDialog(), + }, + separator_1: '---------', + fill_down: fillCellsContextMenuItem('down'), + fill_up: fillCellsContextMenuItem('up'), + separator_2: '---------', + undo: { + disabled: () => + // typeof this.uploadedView === 'function' || + hotRef.current.hotInstance.isUndoAvailable() !== true || + !hasPermission('/workbench/dataset', 'update'), + }, + redo: { + disabled: () => + // typeof this.uploadedView === 'function' || + hotRef.current.hotInstance.isRedoAvailable() !== true || + !hasPermission('/workbench/dataset', 'update'), + }, + } as const) + ), + }; return ( ); } From ad5b070386e95a68b067df4fba71a561f7d2df12 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:31:08 -0400 Subject: [PATCH 020/153] Refactor cellmeta --- .../lib/components/WorkBench/CellMeta.ts | 380 +++++++++++++++++- 1 file changed, 379 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index a96c3d2f9fa..e5e2ba9c3a3 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -3,7 +3,7 @@ import { wbText } from '../../localization/workbench'; import type { RA, WritableArray } from '../../utils/types'; import { throttle } from '../../utils/utils'; import { getHotPlugin } from './handsontable'; -import type { WbView } from './WbView'; +import type { WbView, Workbench } from './WbView'; const metaKeys = [ 'isNew', @@ -46,6 +46,384 @@ const defaultMetaValues = Object.freeze([ undefined, ] as const); +/* eslint-disable functional/no-this-expression */ +export class WbCellMetaReact { + // Meta data for each cell (indexed by physical columns) + // eslint-disable-next-line functional/prefer-readonly-type + public cellMeta: WritableArray> = []; + + public cellCounts: WbCellCounts | undefined = undefined; + + // Meta data for each cell (indexed by visual columns) + // eslint-disable-next-line functional/prefer-readonly-type + private indexedCellMeta: RA> | undefined = undefined; + + // eslint-disable-next-line functional/prefer-readonly-type + public flushIndexedCellData: boolean = true; + + public constructor( + private readonly workbench: Workbench + ) { + this.updateCellInfoStats = throttle( + this.updateCellInfoStats.bind(this), + this.workbench.throttleRate + ); + } + + getCellMeta( + physicalRow: number, + physicalCol: number, + key: KEY + ): WbMeta[KEY] { + const index = metaKeys.indexOf(key); + return (this.cellMeta[physicalRow]?.[physicalCol]?.[index] ?? + defaultMetaValues[index]) as unknown as WbMeta[KEY]; + } + + getCellMetaFromArray( + metaCell: WbMetaArray, + key: KEY + ): WbMeta[KEY] { + const index = metaKeys.indexOf(key); + return (metaCell?.[index] ?? defaultMetaValues[index]) as WbMeta[KEY]; + } + + /* + * This does not run visual side effects + * For changing meta with side effects, use this.updateCellMeta + */ + setCellMeta( + physicalRow: number, + physicalCol: number, + key: KEY, + value: WbMeta[KEY] + ) { + const currentValue = this.getCellMeta(physicalRow, physicalCol, key); + const issuesChanged = + key === 'issues' && + ((currentValue as RA).length !== (value as RA).length || + JSON.stringify(currentValue) !== JSON.stringify(value)); + const cellValueChanged = key === 'originalValue'; + const metaValueChanged = + issuesChanged || + cellValueChanged || + (['isNew', 'isModified', 'isSearchResult'].includes(key) && + currentValue !== value); + + if (!metaValueChanged) return false; + + const index = metaKeys.indexOf(key); + this.cellMeta[physicalRow] ??= []; + this.cellMeta[physicalRow][physicalCol] ??= Array.from( + defaultMetaValues + ) as unknown as WbMetaArray; + this.cellMeta[physicalRow][physicalCol][index] = value; + + this.flushIndexedCellData = true; + + return true; + } + + /** + * Figuring out if a cell was modified is more complicated then it might seem + */ + private isCellModified(physicalRow: number, physicalCol: number): boolean { + // For now, only readOnly picklists are validated on the front-end + const hasFrontEndValidationErrors = this.getCellMeta( + physicalRow, + physicalCol, + 'issues' + ).some((issue) => + issue.endsWith(backEndText.failedParsingPickList({ value: '' })) + ); + if (hasFrontEndValidationErrors) + /* + * Since isModified state has higher priority then issues, we need to + * remove the isModified state if front end validation errors are to be + * visible + */ + return false; + + const originalCellValue = this.getCellMeta( + physicalRow, + physicalCol, + 'originalValue' + ); + const cellValueChanged = + originalCellValue !== undefined && + (originalCellValue?.toString() ?? '') !== + (this.workbench.data[physicalRow][physicalCol]?.toString() ?? ''); + if (cellValueChanged) return true; + + /* + * If cell was disambiguated, it should show up as changed, even if value + * is unchanged + */ + // @ts-expect-error + return this.workbench.disambiguation?.cellWasDisambiguated( + physicalRow, + physicalCol + ); + } + + /** + * Updates cell's isModified meta state + */ + public recalculateIsModifiedState( + physicalRow: number, + physicalCol: number, + { + // Can optionally provide this to improve performance + visualRow = undefined, + // Can optionally provide this to improve performance + visualCol = undefined, + }: { + readonly visualRow?: number; + readonly visualCol?: number; + } = {} + ): void { + const isModified = this.isCellModified(physicalRow, physicalCol); + this.updateCellMeta(physicalRow, physicalCol, 'isModified', isModified, { + visualRow, + visualCol, + }); + } + + /** + * Note: "cell" is undefined when setting meta for off-screen cell. + * This is the case when rendering off-screen issues (which don't require a cell + * reference) + */ + runMetaUpdateEffects( + cell: HTMLTableCellElement | undefined, + key: KEY, + value: WbMeta[KEY], + visualRow: number, + visualCol: number + ) { + if (this.workbench.hot === undefined) return; + + if (key === 'isNew') + cell?.classList[value === true ? 'add' : 'remove']('wb-no-match-cell'); + else if (key === 'isModified') + cell?.classList[value === true ? 'add' : 'remove']('wb-modified-cell'); + else if (key === 'isSearchResult') + cell?.classList[value === true ? 'add' : 'remove']( + 'wb-search-match-cell' + ); + else if (key === 'issues') { + const issues = value as RA; + if (issues.length === 0) + getHotPlugin(this.workbench.hot, 'comments').removeCommentAtCell( + visualRow, + visualCol + ); + else { + getHotPlugin(this.workbench.hot, 'comments').setCommentAtCell( + visualRow, + visualCol, + issues.join('\n') + ); + getHotPlugin(this.workbench.hot, 'comments').updateCommentMeta( + visualRow, + visualCol, + { + readOnly: true, + } + ); + } + } else + throw new Error( + `Tried to set unknown metaData record ${key}=${JSON.stringify( + value + )} for cell + ${visualRow}x${visualCol}` + ); + } + + updateCellMeta( + physicalRow: number, + physicalCol: number, + key: KEY, + value: WbMeta[KEY], + { + // Can optionally provide this to improve performance + cell: initialCell = undefined, + // Can optionally provide this to improve performance + visualRow: initialVisualRow = undefined, + // Can optionally provide this to improve performance + visualCol: initialVisualCol = undefined, + }: { + readonly cell?: HTMLTableCellElement; + readonly visualRow?: number; + readonly visualCol?: number; + } = {} + ) { + if (this.workbench.hot === undefined) return; + const isValueChanged = this.setCellMeta( + physicalRow, + physicalCol, + key, + value + ); + if (!isValueChanged) return false; + + const visualRow = + initialVisualRow ?? this.workbench.hot.toVisualRow(physicalRow); + const visualCol = + initialVisualCol ?? this.workbench.hot.toVisualColumn(physicalCol); + const cell = initialCell ?? this.workbench.hot.getCell(visualRow, visualCol); + this.runMetaUpdateEffects( + cell ?? undefined, + key, + value, + visualRow, + visualCol + ); + + return true; + } + + /* + * Returns this.cellMeta, but instead of indexing by physical row/col, + * indexes by visual row/col + * + * This is used for navigation among cells. + * + * Also, if navigation direction is set to ColByCol, the resulting array + * is transposed. + * + * this.flushIndexedCellData is set to true whenever visual indexes change + * + */ + public getCellMetaObject(): RA> { + if (this.flushIndexedCellData || this.indexedCellMeta === undefined) { + if (this.workbench.hot === undefined) return []; + const resolveIndex = ( + visualRow: number, + visualCol: number, + first: boolean + ) => + (this.workbench.searchPreferences.navigation.direction === + 'rowFirst') === + first + ? visualRow + : visualCol; + + const indexedCellMeta: WritableArray> = []; + Object.entries(this.cellMeta).forEach(([physicalRow, metaRow]) => + Object.entries(metaRow).forEach(([physicalCol, cellMeta]) => { + const visualRow = this.workbench.hot!.toVisualRow( + (physicalRow as unknown as number) | 0 + ); + const visualCol = this.workbench.hot!.toVisualColumn( + (physicalCol as unknown as number) | 0 + ); + indexedCellMeta[resolveIndex(visualRow, visualCol, true)] ??= []; + indexedCellMeta[resolveIndex(visualRow, visualCol, true)][ + resolveIndex(visualRow, visualCol, false) + ] = cellMeta; + }) + ); + this.indexedCellMeta = indexedCellMeta; + } + this.flushIndexedCellData = false; + return this.indexedCellMeta; + } + + // MetaData + updateCellInfoStats() { + const cellMeta = this.cellMeta.flat(); + + this.cellCounts = { + newCells: cellMeta.reduce( + (count, info) => + count + (this.getCellMetaFromArray(info, 'isNew') ? 1 : 0), + 0 + ), + invalidCells: cellMeta.reduce( + (count, info) => + count + + (this.getCellMetaFromArray(info, 'issues').length > 0 ? 1 : 0), + 0 + ), + searchResults: cellMeta.reduce( + (count, info) => + count + (this.getCellMetaFromArray(info, 'isSearchResult') ? 1 : 0), + 0 + ), + modifiedCells: cellMeta.reduce( + (count, info) => + count + (this.getCellMetaFromArray(info, 'isModified') ? 1 : 0), + 0 + ), + }; + + this.workbench.setCellCounts(this.cellCounts); + + // Update navigation information + // needs to be changed + // Object.values( + // this.wbView.el.getElementsByClassName('wb-navigation-total') + // ).forEach((navigationTotalElement) => { + // const navigationContainer = navigationTotalElement.closest( + // '.wb-navigation-section' + // ); + // if (navigationContainer === null) return; + // const navigationType = navigationContainer.getAttribute( + // 'data-navigation-type' + // ) as keyof WbCellCounts | null; + // if (navigationType === null) return; + // navigationTotalElement.textContent = + // cellCounts[navigationType]?.toString(); + + // if (cellCounts[navigationType] === 0) { + // const currentPositionElement = + // navigationContainer.getElementsByClassName( + // 'wb-navigation-position' + // )?.[0]; + // if (typeof currentPositionElement === 'object') + // currentPositionElement.textContent = '0'; + // } + // }); + + // const uploadButton = + // this.wbView.el.querySelector('.wb-upload'); + // if (uploadButton === null) return; + // const title = wbText.uploadUnavailableWhileHasErrors(); + // if ( + // !uploadButton.disabled || + // uploadButton.getAttribute('title') === title + // ) { + // const hasErrors = cellCounts.invalidCells > 0; + // uploadButton.toggleAttribute('disabled', hasErrors); + // uploadButton.setAttribute('title', hasErrors ? title : ''); + // } + } + +public cellIsType(metaArray: WbMetaArray, type: keyof WbCellCounts): boolean { + switch (type) { + case 'invalidCells': { + return this.getCellMetaFromArray(metaArray, 'issues').length > 0; + } + case 'newCells': { + return this.getCellMetaFromArray(metaArray, 'isNew'); + } + case 'modifiedCells': { + return this.getCellMetaFromArray(metaArray, 'isModified'); + } + case 'searchResults': { + return this.getCellMetaFromArray(metaArray, 'isSearchResult'); + } + default: { + return false; + } + } + } +} +/* eslint-enable functional/no-this-expression */ + + /* eslint-disable functional/no-this-expression */ export class WbCellMeta { // Meta data for each cell (indexed by physical columns) From 8a99a07faa88518973ef8458c3958db966a9a27a Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:31:36 -0400 Subject: [PATCH 021/153] Refactor disambiguation --- .../WorkBench/DisambiguationLogic.tsx | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx index 49ed0d6672a..8405e578d79 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx @@ -13,7 +13,112 @@ import { mappingPathToString } from '../WbPlanView/mappingHelpers'; import { getTableFromMappingPath } from '../WbPlanView/navigator'; import { DisambiguationDialog } from './Disambiguation'; import { getSelectedLast } from './hotHelpers'; -import type { WbView } from './WbView'; +import type { WbView, Workbench } from './WbView'; + +/* eslint-disable functional/no-this-expression */ +export class DisambiguationReact { + public constructor( + private readonly workbench: Workbench + ) {} + + public getDisambiguation(physicalRow: number): IR { + const cols = this.workbench.dataset.columns.length; + const hiddenColumn = this.workbench.data[physicalRow][cols]; + const extra = + typeof hiddenColumn === 'string' && hiddenColumn.length > 0 + ? JSON.parse(hiddenColumn) + : {}; + return extra.disambiguation ?? {}; + } + + public isAmbiguousCell(): boolean { + if (this.workbench.mappings === undefined || this.workbench.hot === undefined) + return false; + + const [visualRow, visualCol] = getSelectedLast(this.workbench.hot); + const physicalRow = this.workbench.hot.toPhysicalRow(visualRow); + const physicalCol = this.workbench.hot.toPhysicalColumn(visualCol); + const disambiguation = this.getDisambiguation(physicalRow); + + return ( + this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow] ?? [] + ).some( + ({ physicalCols, mappingPath }) => + physicalCols.includes(physicalCol) && + typeof disambiguation[mappingPathToString(mappingPath)] !== 'number' + ); + } + + public cellWasDisambiguated( + physicalRow: number, + physicalCol: number + ): boolean { + const da = this.getDisambiguation(physicalRow); + return Boolean( + this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow]?.find( + ({ physicalCols, mappingPath }) => + physicalCols.includes(physicalCol) && + typeof da[mappingPathToString(mappingPath)] === 'number' + ) + ); + } + + private changeDisambiguation( + physicalRow: number, + changeFunction: (oldValue: IR) => IR, + source: 'Disambiguation.Clear' | 'Disambiguation.Set' + ): void { + if (this.workbench.hot === undefined) return; + const cols = this.workbench.dataset.columns.length; + const hidden = this.workbench.data[physicalRow][cols]; + const extra = hidden ? JSON.parse(hidden) : {}; + extra.disambiguation = changeFunction(extra.disambiguation || {}); + const visualRow = this.workbench.hot.toVisualRow(physicalRow); + const visualCol = this.workbench.hot.toVisualColumn(cols); + this.workbench.hot.setDataAtCell( + visualRow, + visualCol, + JSON.stringify(extra), + source + ); + // this.workbench.spreadSheetChanged(); + this.afterChangeDisambiguation(physicalRow); + } + + public afterChangeDisambiguation(physicalRow: number): void { + (this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow] ?? []) + .flatMap(({ physicalCols }) => physicalCols) + .forEach((physicalCol) => + this.workbench.cells?.recalculateIsModifiedState(physicalRow, physicalCol) + ); + this.workbench.cells?.updateCellInfoStats(); + } + + clearDisambiguation(physicalRow: number): void { + const disambiguation = this.getDisambiguation(physicalRow); + if (Object.keys(disambiguation).length === 0) + // Nothing to clear + return; + this.changeDisambiguation(physicalRow, () => ({}), 'Disambiguation.Clear'); + } + + public setDisambiguation( + physicalRow: number, + mappingPath: MappingPath, + id: number + ): void { + this.changeDisambiguation( + physicalRow, + (disambiguations) => ({ + ...disambiguations, + [mappingPathToString(mappingPath)]: id, + }), + 'Disambiguation.Set' + ); + } +} + +/* eslint-enable functional/no-this-expression */ /* eslint-disable functional/no-this-expression */ export class Disambiguation { From a5551b3c7d7a6d8acd4b1d7b2a8db6e97c5ac302 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:32:51 -0400 Subject: [PATCH 022/153] Refactor hot hooks --- .../js_src/lib/components/WorkBench/hooks.ts | 713 +++++++++++++++++- 1 file changed, 675 insertions(+), 38 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 7d4bb2a6089..922b3848444 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -16,6 +16,643 @@ import { schema } from '../DataModel/schema'; import { hasPermission } from '../Permissions/helpers'; import { getHotPlugin } from './handsontable'; import type { WbView } from './WbView'; +import { Workbench } from './WbView'; + +export function getHotHooksReact( + workbench: Workbench, + physicalColToMappingCol: (physicalCol: number) => number | undefined, + spreadSheetChanged: () => void, + checkDeletedFail: (statusCode: number) => boolean, +) { + let sortConfigIsSet: boolean = false; + let hotCommentsContainerRepositionTimeout: + | ReturnType + | undefined = undefined; + + return ensure>()({ + /* + * After cell is rendered, we need to reApply metaData classes + * NOTE: + * .issues are handled automatically by the comments plugin. + * This is why, afterRenderer only has to handle the isModified and isNew + * cases + * + */ + afterRenderer: ( + td: any, + visualRow: number, + visualCol: number, + property: number | string, + _value: string + ) => { + if (workbench.hot === undefined) { + td.classList.add('text-gray-500'); + return; + } + const physicalRow = workbench.hot.toPhysicalRow(visualRow); + const physicalCol = + typeof property === 'number' + ? property + : workbench.hot.toPhysicalColumn(visualCol); + if (physicalCol >= workbench.dataset.columns.length) return; + const metaArray = workbench.cells?.cellMeta?.[physicalRow]?.[physicalCol]; + if (workbench.cells?.getCellMetaFromArray(metaArray!, 'isModified')) + workbench.cells.runMetaUpdateEffects( + td, + 'isModified', + true, + visualRow, + visualCol + ); + if (workbench.cells?.getCellMetaFromArray(metaArray!, 'isNew')) + workbench.cells?.runMetaUpdateEffects( + td, + 'isNew', + true, + visualRow, + visualCol + ); + if (workbench.cells!.getCellMetaFromArray(metaArray!, 'isSearchResult')) + workbench.cells?.runMetaUpdateEffects( + td, + 'isSearchResult', + true, + visualRow, + visualCol + ); + if (workbench.mappings?.mappedHeaders?.[physicalCol] === undefined) + td.classList.add('text-gray-500'); + if (workbench.mappings?.coordinateColumns?.[physicalCol] !== undefined) + td.classList.add('wb-coordinate-cell'); + }, + + // Make HOT use defaultValues for validation if cell is empty + beforeValidate: ( + value: any, + _visualRow: number, + property: number | string + ) => { + if (Boolean(value) || workbench.hot === undefined) return value; + + const visualCol = workbench.hot.propToCol(property); + const physicalCol = workbench.hot.toPhysicalColumn(visualCol); + + return workbench.mappings?.defaultValues[physicalCol] ?? value; + }, + + afterValidate: ( + isValid, + value: string | null = '', + visualRow, + property + ) => { + if (workbench.hot === undefined) return; + const visualCol = workbench.hot.propToCol(property); + + const physicalRow = workbench.hot.toPhysicalRow(visualRow); + const physicalCol = workbench.hot.toPhysicalColumn(visualCol); + const issues = workbench.cells?.getCellMeta( + physicalRow, + physicalCol, + 'issues' + ); + /* + * Don't duplicate failedParsingPickList message if both front-end and + * back-end identified the same issue. + * + * This is the only type of validation that is done on the front-end + */ + const newIssues = f.unique([ + ...(isValid + ? [] + : [ + whitespaceSensitive( + backEndText.failedParsingPickList({ + value: `"${value ?? 'null'}"`, + }) + ), + ]), + ...issues!.filter( + (issue) => + !issue.endsWith( + whitespaceSensitive( + backEndText.failedParsingPickList({ value: '' }) + ) + ) + ), + ]); + if (JSON.stringify(issues) !== JSON.stringify(newIssues)) + workbench.cells!.updateCellMeta( + physicalRow, + physicalCol, + 'issues', + newIssues + ); + }, + + afterUndo: (data) => afterUndoRedo(workbench, 'undo', data), + + afterRedo: (data) => afterUndoRedo(workbench, 'redo', data), + + beforePaste: () => + // !wbView.uploadedView && + // !wbView.isUploaded && + hasPermission('/workbench/dataset', 'update'), + + /* + * If copying values from a 1x3 area and pasting into the last cell, HOT + * would create 2 invisible columns) + * + * This intercepts Paste to prevent creation of these columns + * + * This logic wasn't be put into beforePaste because it receives + * arguments that are inconvenient to work with + * + */ + beforeChange: (unfilteredChanges, source) => { + if (source !== 'CopyPaste.paste') return true; + + const filteredChanges = unfilteredChanges.filter( + ([, property]) => (property as number) < workbench.dataset.columns.length + ); + if ( + filteredChanges.length === unfilteredChanges.length || + workbench.hot === undefined + ) + return true; + workbench.hot.setDataAtCell( + filteredChanges.map(([visualRow, property, _oldValue, newValue]) => [ + visualRow, + workbench.hot!.propToCol(property as number), + newValue, + ]), + 'CopyPaste.paste' + ); + return false; + }, + + afterChange: (unfilteredChanges, source) => { + if ( + ![ + 'edit', + 'CopyPaste.paste', + 'CopyPaste.cut', + 'Autofill.fill', + 'UndoRedo.undo', + 'UndoRedo.redo', + ].includes(source) || + !workbench.hot || + unfilteredChanges === null + ) + return; + console.log("here? afeter") + const changes = unfilteredChanges + .map(([visualRow, property, oldValue, newValue]) => ({ + visualRow, + visualCol: workbench.hot!.propToCol(property), + physicalRow: workbench.hot!.toPhysicalRow(visualRow), + physicalCol: + typeof property === 'number' + ? property + : workbench.hot!.toPhysicalColumn( + workbench.hot!.propToCol(property as number | string) + ), + oldValue, + newValue, + })) + .filter( + ({ oldValue, newValue, visualCol }) => + /* + * Ignore cases where value didn't change + * (happens when double click a cell and then click on another cell) + * + */ + oldValue !== newValue && + // Or where value changed from null to empty + (oldValue !== null || newValue !== '') && + // Or the column does not exist (that can happen on paste) + visualCol < workbench.dataset.columns.length + ); + + if (changes.length === 0) return; + + const changedRows = new Set( + changes + // Ignore changes to unmapped columns + .filter( + ({ physicalCol }) => + physicalColToMappingCol(physicalCol) !== -1 + ) + .sort(sortFunction(({ visualRow }) => visualRow)) + .map(({ physicalRow }) => physicalRow) + ); + + /* + * Don't clear disambiguation when afterChange is triggered by + * wbView.hot.undo() from inside of wbView.afterUndoRedo() + * FEATURE: consider not clearing disambiguation at all + */ + // if (!wbView.undoRedoIsHandled) + // changedRows.forEach((physicalRow) => + // workbench.disambiguation?.clearDisambiguation(physicalRow) + // ); + + changes.forEach( + ({ + visualRow, + visualCol, + physicalRow, + physicalCol, + oldValue = '', + newValue, + }) => { + if ( + workbench.cells?.getCellMeta( + physicalRow, + physicalCol, + 'originalValue' + ) === undefined + ) + workbench.cells?.setCellMeta( + physicalRow, + physicalCol, + 'originalValue', + oldValue + ); + workbench.cells?.recalculateIsModifiedState(physicalRow, physicalCol, { + visualRow, + visualCol, + }); + if ( + workbench.utils?.searchPreferences.search.liveUpdate && + workbench.utils?.searchQuery !== undefined + ) + workbench.cells?.updateCellMeta( + physicalRow, + physicalCol, + 'isSearchResult', + workbench.utils?.searchFunction(newValue), + { visualRow, visualCol } + ); + } + ); + + spreadSheetChanged(); + workbench.cells?.updateCellInfoStats(); + + if (workbench.dataset.uploadplan) + changedRows.forEach((physicalRow) => + workbench.validation?.startValidateRow(physicalRow) + ); + }, + + /* + * This may be called before full initialization of the workbench because + * of the minSpareRows setting in HOT. Thus, be sure to check if + * wbView.hotIsReady is true + * + * Also, I don't think this is ever called with amount > 1. + * Even if multiple new rows where created at once (e.x on paste), HOT calls + * this hook one row at a time + * + * Also, this function needs to be called before afterValidate, thus I used + * beforeCreateRow, instead of afterCreateRow + * + */ + beforeCreateRow: (visualRowStart, amount, source) => { + const addedRows = Array.from( + { length: amount }, + (_, index) => + /* + * If HOT is not yet fully initialized, we can assume that physical row + * order and visual row order is the same + */ + workbench.hot?.toPhysicalRow(visualRowStart + index) ?? + visualRowStart + index + // REFACTOR: use sortFunction here + ).sort(); + + workbench.cells!.flushIndexedCellData = true; + addedRows + .filter((physicalRow) => physicalRow < workbench.cells!.cellMeta.length) + .forEach((physicalRow) => + workbench.cells?.cellMeta.splice(physicalRow, 0, []) + ); + if (workbench.hot && source !== 'auto') + spreadSheetChanged(); + + return true; + }, + + beforeRemoveRow: (visualRowStart, amount, _, source) => { + if (workbench.hot === undefined) return; + // Get indexes of removed rows in reverse order + const removedRows = Array.from({ length: amount }, (_, index) => + workbench.hot!.toPhysicalRow(visualRowStart + index) + ) + .filter((physicalRow) => physicalRow < workbench.cells!.cellMeta.length) + // REFACTOR: use sortFunction here + .sort() + .reverse(); + + removedRows.forEach((physicalRow) => { + workbench.cells!.cellMeta.splice(physicalRow, 1); + workbench.validation!.liveValidationStack.splice(physicalRow, 1); + }); + + workbench.cells!.flushIndexedCellData = true; + + if (workbench.hot && source !== 'auto') { + spreadSheetChanged(); + workbench.cells!.updateCellInfoStats(); + } + + return true; + }, + + /* + * If a tree column is about to be sorted, overwrite the sort config by + * finding all lower level ranks of that tree (within the same -to-many) + * and sorting them in the same direction + */ + beforeColumnSort: (currentSortConfig, newSortConfig) => { + workbench.cells!.flushIndexedCellData = true; + + // if (wbView.coordinateConverterView) return false; + + if ( + workbench.mappings === undefined || + sortConfigIsSet || + workbench.hot === undefined + ) + return true; + + const findTreeColumns = ( + sortConfig: RA, + deltaSearchConfig: RA + ) => + sortConfig + .map(({ column: visualCol, sortOrder }) => ({ + sortOrder, + visualCol, + physicalCol: workbench.hot!.toPhysicalColumn(visualCol), + })) + .map(({ physicalCol, ...rest }) => ({ + ...rest, + rankGroup: workbench.mappings!.treeRanks?.map((rankGroup, groupIndex) => ({ + rankId: rankGroup.find( + (mapping) => mapping.physicalCol === physicalCol + )?.rankId, + groupIndex, + })) + .find(({ rankId }) => rankId !== undefined), + })) + // Filter out columns that aren't tree ranks + .filter(({ rankGroup }) => rankGroup !== undefined) + /* + * Filter out columns that didn't change + * In the end, there should only be 0 or 1 columns + * + */ + .find(({ sortOrder, visualCol }) => { + const deltaColumnState = deltaSearchConfig.find( + ({ column }) => column === visualCol + ); + return ( + deltaColumnState === undefined || + deltaColumnState.sortOrder !== sortOrder + ); + }); + + let changedTreeColumn = findTreeColumns(newSortConfig, currentSortConfig); + let newSortOrderIsUnset = false; + + if (changedTreeColumn === undefined) { + changedTreeColumn = findTreeColumns(currentSortConfig, newSortConfig); + newSortOrderIsUnset = true; + } + + if (changedTreeColumn === undefined) return true; + + /* + * Filter out columns with higher rank than the changed column + * (lower rankId corresponds to a higher tree rank) + * + */ + const columnsToSort = workbench.mappings.treeRanks[ + changedTreeColumn.rankGroup!.groupIndex + ] + .filter(({ rankId }) => rankId >= changedTreeColumn!.rankGroup!.rankId!) + .map(({ physicalCol }) => workbench.hot!.toVisualColumn(physicalCol)); + + // Filter out columns that are about to be sorted + const partialSortConfig = newSortConfig.filter( + ({ column }) => !columnsToSort.includes(column) + ); + + const fullSortConfig = [ + ...partialSortConfig, + ...(newSortOrderIsUnset + ? [] + : columnsToSort.map((visualCol) => ({ + column: visualCol, + sortOrder: changedTreeColumn!.sortOrder, + }))), + ]; + + sortConfigIsSet = true; + getHotPlugin(workbench.hot, 'multiColumnSorting').sort(fullSortConfig); + sortConfigIsSet = false; + + return false; + }, + + // Cache sort config to preserve column sort order across sessions + afterColumnSort: async (_previousSortConfig, sortConfig) => { + if (workbench.hot === undefined) return; + const physicalSortConfig = sortConfig.map((rest) => ({ + ...rest, + physicalCol: workbench.hot!.toPhysicalColumn(rest.column), + })); + setCache( + 'workBenchSortConfig', + `${schema.domainLevelIds.collection}_${workbench.dataset.id}`, + physicalSortConfig + ); + }, + + beforeColumnMove: (_columnIndexes, _finalIndex, dropIndex) => + // Don't allow moving columns when isReadOnly + // !wbView.uploadedView && + // !wbView.coordinateConverterView && + // An ugly fix for jQuery's dialogs conflicting with HOT + (dropIndex !== undefined || !workbench.hot), + + // Save new visualOrder on the back end + afterColumnMove: (_columnIndexes, _finalIndex, dropIndex) => { + // An ugly fix for jQuery's dialogs conflicting with HOT + if ( + dropIndex === undefined || + !workbench.hot + ) + return; + + workbench.cells!.flushIndexedCellData = true; + + const columnOrder = workbench.dataset.columns.map((_, visualCol) => + workbench.hot!.toPhysicalColumn(visualCol) + ); + + if ( + workbench.dataset.visualorder === null || + columnOrder.some((i, index) => i !== workbench.dataset.visualorder![index]) + ) { + overwriteReadOnly(workbench.dataset, 'visualorder', columnOrder); + ping(`/api/workbench/dataset/${workbench.dataset.id}/`, { + method: 'PUT', + body: { visualorder: columnOrder }, + expectedErrors: [Http.NOT_FOUND], + }).then((status) => checkDeletedFail(status)); + } + }, + + // Do not scroll the viewport to the last column after inserting a row + afterPaste: (data, coords) => { + const lastCoords = coords.at(-1); + if ( + typeof lastCoords === 'object' && + data.some((row) => row.length === workbench.dataset.columns.length) && + workbench.hot !== undefined + ) + workbench.hot.scrollViewportTo(lastCoords.endRow, lastCoords.startCol); + }, + + /* + * Reposition the comment box if it is overflowing + * See https://github.com/specify/specify7/issues/932 + * REFACTOR: https://github.com/specify/specify7/issues/1925 + */ + afterOnCellMouseOver: (_event, coordinates, cell) => { + if (workbench.hot === undefined) return; + const physicalRow = workbench.hot.toPhysicalRow(coordinates.row); + const physicalCol = workbench.hot.toPhysicalColumn(coordinates.col); + + // Make sure cell has comments + if ( + workbench.cells!.getCellMeta(physicalRow, physicalCol, 'issues').length === + 0 + ) + return; + + const cellContainerBoundingBox = cell.getBoundingClientRect(); + + // Make sure box is overflowing horizontally + if (globalThis.innerWidth > cellContainerBoundingBox.right + oneRem * 2) + return; + + // wbView.hotCommentsContainer?.style.setProperty( + // '--offset-right', + // `${Math.round(globalThis.innerWidth - cellContainerBoundingBox.x)}px` + // ); + // wbView.hotCommentsContainer?.classList.add( + // 'right-[var(--offset-right)]', + // '!left-[unset]' + // ); + if (hotCommentsContainerRepositionTimeout) { + globalThis.clearTimeout(hotCommentsContainerRepositionTimeout); + hotCommentsContainerRepositionTimeout = undefined; + } + }, + + /* + * Revert comment box's position to original state if needed. + * The 10ms delay helps prevent visual artifacts when the mouse pointer + * moves between cells. + */ + afterOnCellMouseOut: () => { + // if (hotCommentsContainerRepositionTimeout) + // globalThis.clearTimeout(hotCommentsContainerRepositionTimeout); + // if ( + // wbView.hotCommentsContainer?.style.getPropertyValue( + // '--offset-right' + // ) !== '' + // ) + // hotCommentsContainerRepositionTimeout = globalThis.setTimeout( + // () => + // wbView.hotCommentsContainer?.classList.remove( + // 'right-[var(--offset-right)]', + // '!left-[unset]' + // ), + // 10 + // ); + }, + + /* + * Disallow user from selecting several times the same cell + */ + afterSelection: () => { + if (workbench.hot === undefined) return; + const selection = workbench.hot?.getSelected() ?? []; + const newSelection = f + .unique(selection.map((row) => JSON.stringify(row))) + .map((row) => JSON.parse(row)); + if (newSelection.length !== selection.length) { + workbench.hot?.deselectCell(); + workbench.hot?.selectCells(newSelection); + } + }, + }); +} + +/** + * Any change to a row clears disambiguation results + * Clearing disambiguation creates a separate point in the undo/redo stack + * This runs undo twice when undoing a change that caused disambiguation + * clear and similarly redoes the change twice + * + */ +function afterUndoRedo( + workbench: Workbench, + type: 'redo' | 'undo', + data: Action +): void { + if ( + // workbench.undoRedoIsHandled || + data.actionType !== 'change' || + data.changes.length !== 1 || + workbench.hot === undefined + ) + return; + + const [visualRow, visualCol, newData, oldData] = data.changes[0]; + const physicalRow = workbench.hot.toPhysicalRow(visualRow); + const physicalCol = workbench.hot.toPhysicalColumn(visualCol as number); + if (physicalCol !== workbench.dataset.columns.length) return; + + const newValue = JSON.parse(newData || '{}').disambiguation; + const oldValue = JSON.parse(oldData || '{}').disambiguation; + + /* + * Disambiguation results are cleared when any cell in a row changes. + * That change creates a separate point in the undo stack. + * Thus, if HOT tries to undo disambiguation clearing, we need to + * also need to undo the change that caused disambiguation clearing + */ + if ( + type === 'undo' && + Object.keys(newValue ?? {}).length > 0 && + Object.keys(oldValue ?? {}).length === 0 + ) + // HOT doesn't seem to like calling undo from inside of afterUndo + globalThis.setTimeout(() => { + // wbView.undoRedoIsHandled = true; + workbench.hot?.undo(); + // wbView.undoRedoIsHandled = false; + workbench.disambiguation!.afterChangeDisambiguation(physicalRow); + }, 0); + else workbench.disambiguation!.afterChangeDisambiguation(physicalRow); +} + export function getHotHooks(wbView: WbView) { let sortConfigIsSet: boolean = false; @@ -607,44 +1244,44 @@ export function getHotHooks(wbView: WbView) { * clear and similarly redoes the change twice * */ -function afterUndoRedo( - wbView: WbView, - type: 'redo' | 'undo', - data: Action -): void { - if ( - wbView.undoRedoIsHandled || - data.actionType !== 'change' || - data.changes.length !== 1 || - wbView.hot === undefined - ) - return; +// function afterUndoRedo( +// wbView: WbView, +// type: 'redo' | 'undo', +// data: Action +// ): void { +// if ( +// wbView.undoRedoIsHandled || +// data.actionType !== 'change' || +// data.changes.length !== 1 || +// wbView.hot === undefined +// ) +// return; - const [visualRow, visualCol, newData, oldData] = data.changes[0]; - const physicalRow = wbView.hot.toPhysicalRow(visualRow); - const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); - if (physicalCol !== wbView.dataset.columns.length) return; +// const [visualRow, visualCol, newData, oldData] = data.changes[0]; +// const physicalRow = wbView.hot.toPhysicalRow(visualRow); +// const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); +// if (physicalCol !== wbView.dataset.columns.length) return; - const newValue = JSON.parse(newData || '{}').disambiguation; - const oldValue = JSON.parse(oldData || '{}').disambiguation; +// const newValue = JSON.parse(newData || '{}').disambiguation; +// const oldValue = JSON.parse(oldData || '{}').disambiguation; - /* - * Disambiguation results are cleared when any cell in a row changes. - * That change creates a separate point in the undo stack. - * Thus, if HOT tries to undo disambiguation clearing, we need to - * also need to undo the change that caused disambiguation clearing - */ - if ( - type === 'undo' && - Object.keys(newValue ?? {}).length > 0 && - Object.keys(oldValue ?? {}).length === 0 - ) - // HOT doesn't seem to like calling undo from inside of afterUndo - globalThis.setTimeout(() => { - wbView.undoRedoIsHandled = true; - wbView.hot?.undo(); - wbView.undoRedoIsHandled = false; - wbView.disambiguation.afterChangeDisambiguation(physicalRow); - }, 0); - else wbView.disambiguation.afterChangeDisambiguation(physicalRow); -} +// /* +// * Disambiguation results are cleared when any cell in a row changes. +// * That change creates a separate point in the undo stack. +// * Thus, if HOT tries to undo disambiguation clearing, we need to +// * also need to undo the change that caused disambiguation clearing +// */ +// if ( +// type === 'undo' && +// Object.keys(newValue ?? {}).length > 0 && +// Object.keys(oldValue ?? {}).length === 0 +// ) +// // HOT doesn't seem to like calling undo from inside of afterUndo +// globalThis.setTimeout(() => { +// wbView.undoRedoIsHandled = true; +// wbView.hot?.undo(); +// wbView.undoRedoIsHandled = false; +// wbView.disambiguation.afterChangeDisambiguation(physicalRow); +// }, 0); +// else wbView.disambiguation.afterChangeDisambiguation(physicalRow); +// } From 6f2797921a2c5c2734c3010ab4e4fa8361bc1e49 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:34:35 -0400 Subject: [PATCH 023/153] Refactor index.ts --- .../js_src/lib/components/WorkBench/index.tsx | 54 ++++++++----------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index acab93833af..0df4d424095 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -1,58 +1,44 @@ import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import type { LocalizedString } from 'typesafe-i18n'; -import { useUnloadProtect } from '../../hooks/navigation'; import { useAsyncState } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useErrorContext } from '../../hooks/useErrorContext'; import { commonText } from '../../localization/common'; -import { localityText } from '../../localization/locality'; -import { wbPlanText } from '../../localization/wbPlan'; import { wbText } from '../../localization/workbench'; import { ajax } from '../../utils/ajax'; import { f } from '../../utils/functools'; -import type { GetSet, RA } from '../../utils/types'; -import { replaceItem } from '../../utils/utils'; -import { Button } from '../Atoms/Button'; +import type { GetSet } from '../../utils/types'; import { className } from '../Atoms/className'; -import { Input } from '../Atoms/Form'; -import { Link } from '../Atoms/Link'; import { LoadingContext } from '../Core/Contexts'; import { useMenuItem } from '../Header/MenuContext'; import { treeRanksPromise } from '../InitialContext/treeRanks'; import { Dialog } from '../Molecules/Dialog'; -import { Portal } from '../Molecules/Portal'; -import { hasPermission, hasTablePermission } from '../Permissions/helpers'; -import { userPreferences } from '../Preferences/userPreferences'; import { NotFoundView } from '../Router/NotFoundView'; import type { Dataset } from '../WbPlanView/Wrapped'; -import type { WbStatus } from './WbView'; -import { WbView as WbViewClass, WbViewReact } from './WbView'; -import { DataSetName } from './DataSetMeta'; -import { WbSpreadsheet } from './WbSpreadsheet'; -import Handsontable from 'handsontable'; +import { WbViewReact } from './WbView'; + const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); const fetchDataSet = async ( - dataSetId: number | undefined - ): Promise => - typeof dataSetId === 'number' - ? ajax(`/api/workbench/dataset/${dataSetId}/`, { - headers: { Accept: 'application/json' }, - }).then(({ data }) => data) - : undefined; + dataSetId: number | undefined +): Promise => + typeof dataSetId === 'number' + ? ajax(`/api/workbench/dataset/${dataSetId}/`, { + headers: { Accept: 'application/json' }, + }).then(({ data }) => data) + : undefined; // BUG: intercept 403 (if dataset has been transferred to another user) function useDataSet( - dataSetId: number | undefined - ): GetSet { - return useAsyncState( - React.useCallback(async () => fetchDataSet(dataSetId), [dataSetId]), - true - ); - } + dataSetId: number | undefined +): GetSet { + return useAsyncState( + React.useCallback(async () => fetchDataSet(dataSetId), [dataSetId]), + true + ); +} export function WorkBenchReact(): JSX.Element | null { useMenuItem('workBench'); @@ -69,13 +55,14 @@ export function WorkBenchReact(): JSX.Element | null { const navigate = useNavigate(); const hotRef = React.useRef(null); + const spreadsheetContainer = React.useRef(null); // temporary null check, replace with loading screen? if (!dataSet || !treeRanksLoaded) return null; const triggerRefresh = () => { loading(fetchDataSet(dataSet!.id).then(setDataSet)); - } + }; return dataSetId === undefined ? ( @@ -92,12 +79,13 @@ export function WorkBenchReact(): JSX.Element | null { ) : ( <>
-
+
From 3cdb5c5785f33f06dbf144a2764cc1236711520a Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:36:55 -0400 Subject: [PATCH 024/153] Refactor WbActions into smaller components --- .../lib/components/WorkBench/WbActions.tsx | 592 +++++++++++++++++- 1 file changed, 573 insertions(+), 19 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx index a261e5bc90e..adf75f08aa5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx @@ -11,14 +11,568 @@ import { Link } from '../Atoms/Link'; import { loadingBar } from '../Molecules'; import { Dialog } from '../Molecules/Dialog'; import type { Status } from '../WbPlanView/Wrapped'; -import type { WbCellCounts } from './CellMeta'; +import type { WbCellCounts, WbCellMetaReact } from './CellMeta'; import { RollbackConfirmation } from './Components'; import { CreateRecordSetButton } from './RecordSet'; import { WbStatus as WbStatusComponent } from './Status'; -import type { DialogHandlers, WbStatus, WbView } from './WbView'; +import type { DialogHandlers, WbStatus, WbView, Workbench } from './WbView'; import type { Dataset } from '../WbPlanView/Wrapped'; import type { WbMapping } from './mapping'; import type { RA } from '../../utils/types'; +import { WbValidationReact } from './WbValidation'; +import Handsontable from 'handsontable'; +import { WbUtils } from './WbUtils'; +import { className } from '../Atoms/className'; +import { hasPermission } from '../Permissions/helpers'; +import { userPreferences } from '../Preferences/userPreferences'; +import { useBooleanState } from '../../hooks/useBooleanState'; + +export function useWbActions({ + datasetId, + triggerRefresh, + checkDeletedFail, + openStatus, + spreadSheetUpToDate, + workbench, +}: { + readonly datasetId: number; + readonly triggerRefresh: () => void; + readonly checkDeletedFail: (statusCode: number) => void; + readonly openStatus: () => void; + readonly spreadSheetUpToDate: () => void; + readonly workbench: Workbench; +}) { + const mode = React.useRef(undefined); + const refreshInitiatorAborted = React.useRef(false); + + const startUpload = (newMode: WbStatus): void => { + // this.validation.stopLiveValidation(); + // this.validation.updateValidationButton(); + ping(`/api/workbench/${newMode}/${datasetId}/`, { + method: 'POST', + expectedErrors: [Http.CONFLICT], + }) + .then((statusCode): void => { + checkDeletedFail(statusCode); + checkConflictFail(statusCode); + }) + .then(() => triggerStatusComponent(newMode)); + }; + + const changeMode = (newMode: WbStatus): void => { + mode.current = newMode; + }; + + const triggerStatusComponent = (newMode: WbStatus): void => { + changeMode(newMode); + openStatus(); + }; + + const checkConflictFail = (statusCode: number): boolean => { + if (statusCode === Http.CONFLICT) + /* + * Upload/Validation/Un-Upload has been initialized by another session + * Need to reload the page to display the new state + */ + triggerRefresh(); + return statusCode === Http.CONFLICT; + }; + + const save = () => { + // Clear validation + overwriteReadOnly(workbench.dataset, 'rowresults', null); + // this.validation.stopLiveValidation(); + // this.validation.updateValidationButton(); + + // Show saving progress bar + // this.saveProgressBar.open(); + + // Send data + ping(`/api/workbench/rows/${datasetId}/`, { + method: 'PUT', + body: workbench.data, + expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], + }) + .then((status) => checkDeletedFail(status)) + .then(() => { + spreadSheetUpToDate(); + workbench.cells!.cellMeta = []; + // utils.searchCells({ key: 'SettingsChange' }); + workbench.hot?.render(); + }); + // .finally(this.saveProgressBar.close); + }; + + return { + mode, + refreshInitiatorAborted, + startUpload, + triggerStatusComponent, + save, + }; +} + +export function WbActionsComponent({ + dataset, + hasUnSavedChanges, + isUploaded, + triggerRefresh, + mappings, + checkDeletedFail, + spreadSheetUpToDate, + workbench, + toggleResults +}: { + readonly dataset: Dataset; + readonly hasUnSavedChanges: boolean; + readonly isUploaded: boolean; + readonly triggerRefresh: () => void; + readonly mappings: WbMapping; + readonly checkDeletedFail: (statusCode: number) => void; + readonly spreadSheetUpToDate: () => void; + readonly workbench: Workbench; + readonly toggleResults: () => void; +}): JSX.Element { + const [canLiveValidate] = userPreferences.use( + 'workBench', + 'general', + 'liveValidation' + ); + const [noUploadPlan, openNoUploadPlan, closeNoUploadPlan] = useBooleanState(); + const [showStatus, openStatus, closeStatus] = useBooleanState(); + const [operationAborted, openAbortedMessage, closeAbortedMessage] = + useBooleanState(); + const [operationCompleted, openOperationCompleted, closeOperationCompleted] = + useBooleanState(); + const { mode, refreshInitiatorAborted, ...actions } = useWbActions({ + datasetId: dataset.id, + triggerRefresh, + checkDeletedFail, + openStatus, + spreadSheetUpToDate, + workbench, + }); + const cells = workbench.cells!; + const messages = { + validate: + cells.cellCounts?.invalidCells === 0 + ? { + header: wbText.validationNoErrors(), + message: ( + <> + {wbText.validationNoErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + } + : { + header: wbText.validationErrors(), + message: ( + <> + {wbText.validationErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + }, + upload: + cells.cellCounts?.invalidCells === 0 + ? { + header: wbText.uploadSuccessful(), + message: wbText.uploadSuccessfulDescription(), + } + : { + header: wbText.uploadErrors(), + message: ( + <> + {wbText.uploadErrorsDescription()} +
+
+ {wbText.uploadErrorsSecondDescription()} + + ), + }, + unupload: { + header: wbText.dataSetRollback(), + message: wbText.dataSetRollbackDescription(), + }, + }; + React.useEffect(() => { + if ( + !isUploaded && + (mappings?.lines ?? []).length === 0 && + hasPermission('/workbench/dataset', 'upload') + ) { + openNoUploadPlan(); + } + }, []); + return ( + <> + {noUploadPlan && ( + + {commonText.close()} + + {commonText.create()} + + + } + header={wbPlanText.noUploadPlan()} + onClose={closeNoUploadPlan} + > + {wbPlanText.noUploadPlanDescription()} + + )} + {!isUploaded && hasPermission('/workbench/dataset', 'validate') && ( + + )} + + {isUploaded && hasPermission('/workbench/dataset', 'unupload') && ( + + )} + {!isUploaded && hasPermission('/workbench/dataset', 'upload') && ( + + )} + {!isUploaded && hasPermission('/workbench/dataset', 'update') && ( + <> + + + + )} + {mode.current && showStatus && ( + { + refreshInitiatorAborted.current = wasAborted; + closeStatus(); + if (wasAborted) openAbortedMessage(); + triggerRefresh(); + }} + /> + )} + {operationCompleted && ( + + {cells.cellCounts?.invalidCells === 0 && + mode.current === 'upload' && ( + { + mode.current = undefined; + refreshInitiatorAborted.current = false; + closeOperationCompleted(); + }} + /> + )} + {commonText.close()} + + } + header={messages[mode.current as WbStatus].header} + onClose={closeOperationCompleted} + > + {messages[mode.current as WbStatus].message} + + )} + {operationAborted && ( + + {mode.current === 'validate' + ? wbText.validationCanceledDescription() + : mode.current === 'unupload' + ? wbText.rollbackCanceledDescription() + : wbText.uploadCanceledDescription()} + + )} + + ); +} + +export function WbValidate({ + canLiveValidate, + hasUnSavedChanges, + startUpload, + validation +}: { + readonly canLiveValidate: boolean; + readonly hasUnSavedChanges: boolean; + readonly startUpload: (mode: WbStatus) => void; + readonly validation: WbValidationReact +}): JSX.Element { + const handleValidate = () => startUpload('validate'); + const [validateIsReady, setValidateIsReady] = React.useState(false); + const [liveValidationCount, setCount] = React.useState(validation.liveValidationStack.length); + React.useEffect(() => { + validation.setCount = setCount; + setValidateIsReady(true); + }, []) + return ( + <> + validation.toggleDataCheck()} + aria-pressed={validation.validationMode === "live"} + > + {validation.validationMode === 'live' + ? liveValidationCount > 0 + ? commonText.countLine({ + resource: wbText.dataCheckOn(), + count: liveValidationCount, + }) + : wbText.dataCheckOn() + : wbText.dataCheck()} + + + {wbText.validate()} + + + ); +} + +export function WbResults({ + hasUnSavedChanges, + toggleResults +}: { + readonly hasUnSavedChanges: boolean; + readonly toggleResults: () => void; +}): JSX.Element { + + return ( + <> + + {commonText.results()} + + + ); +} + +export function WbRollback({ + datasetId, + triggerStatusComponent, +}: { + readonly datasetId: number; + readonly triggerStatusComponent: (mode: WbStatus) => void; +}): JSX.Element { + const [rollback, handleOpen, handleClose] = useBooleanState(); + return ( + <> + + {wbText.rollback()} + + {rollback && ( + triggerStatusComponent('unupload')} + /> + )} + + ); +} + +export function WbUpload({ + hasUnSavedChanges, + mappings, + openNoUploadPlan, + startUpload, +}: { + readonly hasUnSavedChanges: boolean; + readonly mappings: WbMapping; + readonly openNoUploadPlan: () => void; + readonly startUpload: (mode: WbStatus) => void; +}): JSX.Element { + const [showUpload, openUpload, closeUpload] = useBooleanState(); + const handleUpload = (): void => { + if ((mappings?.lines ?? []).length > 0) { + openUpload(); + } else { + openNoUploadPlan(); + } + }; + return ( + <> + + {wbText.upload()} + + {showUpload && ( + + {commonText.cancel()} + { + startUpload('upload'); + closeUpload(); + }} + > + {wbText.upload()} + + + } + header={wbText.startUpload()} + onClose={closeUpload} + > + {wbText.startUploadDescription()} + + )} + + ); +} + +export function WbRevert({ + hasUnSavedChanges, + triggerRefresh, + spreadSheetUpToDate, +}: { + readonly hasUnSavedChanges: boolean; + readonly triggerRefresh: () => void; + readonly spreadSheetUpToDate: () => void; +}): JSX.Element { + const [showRevert, openRevert, closeRevert] = useBooleanState(); + const handleRevert = () => { + triggerRefresh(); + closeRevert(); + spreadSheetUpToDate(); + }; + return ( + <> + + {wbText.revert()} + + {showRevert && ( + + {commonText.cancel()} + + {wbText.revert()} + + + } + header={wbText.revertChanges()} + onClose={closeRevert} + > + {wbText.revertChangesDescription()} + + )} + + ); +} + +export function WbSave({ + hasUnSavedChanges, + save, +}: { + readonly hasUnSavedChanges: boolean; + readonly save: () => void; +}): JSX.Element { + const [showProgressBar, openProgressBar, closeProgressBar] = + useBooleanState(); + const handleSave = () => { + openProgressBar(); + save(); + closeProgressBar(); + }; + return ( + <> + + {commonText.save()} + + {showProgressBar && ( + + {loadingBar} + + )} + + ); +} /* eslint-disable functional/no-this-expression */ export class WbActionsReact { @@ -38,7 +592,12 @@ export class WbActionsReact { private refreshInitiatedBy: React.MutableRefObject, private refreshInitiatorAborted: React.MutableRefObject, private readonly operationCompleted: DialogHandlers, - private readonly operationAborted: DialogHandlers + private readonly operationAborted: DialogHandlers, + private readonly opMsg: any, + private readonly validation: WbValidationReact, + private readonly cells: WbCellMetaReact, + private readonly hot: Handsontable, + private readonly utils: WbUtils // change ) {} // BUG: disable the button if there is nothing to upload @@ -53,8 +612,8 @@ export class WbActionsReact { } startUpload(mode: WbStatus): void { - // this.wbView.validation.stopLiveValidation(); - // this.wbView.validation.updateValidationButton(); + // this.validation.stopLiveValidation(); + // this.validation.updateValidationButton(); ping(`/api/workbench/${mode}/${this.dataset.id}/`, { method: 'POST', expectedErrors: [Http.CONFLICT], @@ -74,8 +633,8 @@ export class WbActionsReact { async save() { // Clear validation overwriteReadOnly(this.dataset, 'rowresults', null); - // this.wbView.validation.stopLiveValidation(); - // this.wbView.validation.updateValidationButton(); + // this.validation.stopLiveValidation(); + // this.validation.updateValidationButton(); // Show saving progress bar this.saveProgressBar.open(); @@ -89,9 +648,9 @@ export class WbActionsReact { .then((status) => this.checkDeletedFail(status)) .then(() => { this.spreadSheetUpToDate(); - // this.wbView.cells.cellMeta = []; - // this.wbView.wbUtils.searchCells({ key: 'SettingsChange' }); - // this.wbView.hot?.render(); + this.cells.cellMeta = []; + this.utils.searchCells({ key: 'SettingsChange' }); + this.hot?.render(); }) .finally(this.saveProgressBar.close); } @@ -165,13 +724,8 @@ export class WbActionsReact { // refactor to set this as a state or ref const messageToShow = messages[this.refreshInitiatedBy.current]; + this.opMsg(messageToShow); this.operationCompleted.open(); - - // set this in onClose - this.refreshInitiatedBy.current = undefined; - this.refreshInitiatorAborted.current = false; - - return messageToShow; } operationAbortedMessage(): void { @@ -183,9 +737,9 @@ export class WbActionsReact { this.operationAborted.open(); - // set this in onClose - this.refreshInitiatedBy.current = undefined; - this.refreshInitiatorAborted.current = false; + // // set this in onClose + // this.refreshInitiatedBy.current = undefined; + // this.refreshInitiatorAborted.current = false; } spreadSheetChanged(): void { From bf13c57c9049cf29344558a4110d01ff7b912e8c Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:40:15 -0400 Subject: [PATCH 025/153] Remove console statement --- specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 922b3848444..19d5d0f774a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -205,7 +205,6 @@ export function getHotHooksReact( unfilteredChanges === null ) return; - console.log("here? afeter") const changes = unfilteredChanges .map(([visualRow, property, oldValue, newValue]) => ({ visualRow, From 830273a97577a835c15bb69f9834dc96d3c3d4b0 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:04:08 -0400 Subject: [PATCH 026/153] Comment out setCellCount as it breaks error cells --- .../frontend/js_src/lib/components/WorkBench/CellMeta.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index e5e2ba9c3a3..9249497bbd4 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -359,8 +359,10 @@ export class WbCellMetaReact { ), }; - this.workbench.setCellCounts(this.cellCounts); + // below line breaks error cell highlighting + // this.workbench.setCellCounts(this.cellCounts); + // OLD CODE: NEEDS TO BE REMOVED AT THE END // Update navigation information // needs to be changed // Object.values( From 4bf6176ccc17f55b84ad6cc10910472eb5d18f1b Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:05:38 -0400 Subject: [PATCH 027/153] Add temporary workaround for error cells disappearing --- .../js_src/lib/components/WorkBench/hooks.ts | 94 ++++++++++--------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 19d5d0f774a..fbaac9c0085 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -80,6 +80,12 @@ export function getHotHooksReact( visualRow, visualCol ); + // if (workbench.cells!.getCellMetaFromArray(metaArray!, 'issues')) { + // const issues = workbench.cells!.getCellMetaFromArray(metaArray!, 'issues') + // td?.classList[issues.length > 0 ? 'add' : 'remove']( + // 'htCommentCell' + // ); + // } if (workbench.mappings?.mappedHeaders?.[physicalCol] === undefined) td.classList.add('text-gray-500'); if (workbench.mappings?.coordinateColumns?.[physicalCol] !== undefined) @@ -150,9 +156,9 @@ export function getHotHooksReact( ); }, - afterUndo: (data) => afterUndoRedo(workbench, 'undo', data), + afterUndo: (data) => afterUndoRedoReact(workbench, 'undo', data), - afterRedo: (data) => afterUndoRedo(workbench, 'redo', data), + afterRedo: (data) => afterUndoRedoReact(workbench, 'redo', data), beforePaste: () => // !wbView.uploadedView && @@ -610,7 +616,7 @@ export function getHotHooksReact( * clear and similarly redoes the change twice * */ -function afterUndoRedo( +function afterUndoRedoReact( workbench: Workbench, type: 'redo' | 'undo', data: Action @@ -1243,44 +1249,44 @@ export function getHotHooks(wbView: WbView) { * clear and similarly redoes the change twice * */ -// function afterUndoRedo( -// wbView: WbView, -// type: 'redo' | 'undo', -// data: Action -// ): void { -// if ( -// wbView.undoRedoIsHandled || -// data.actionType !== 'change' || -// data.changes.length !== 1 || -// wbView.hot === undefined -// ) -// return; - -// const [visualRow, visualCol, newData, oldData] = data.changes[0]; -// const physicalRow = wbView.hot.toPhysicalRow(visualRow); -// const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); -// if (physicalCol !== wbView.dataset.columns.length) return; - -// const newValue = JSON.parse(newData || '{}').disambiguation; -// const oldValue = JSON.parse(oldData || '{}').disambiguation; - -// /* -// * Disambiguation results are cleared when any cell in a row changes. -// * That change creates a separate point in the undo stack. -// * Thus, if HOT tries to undo disambiguation clearing, we need to -// * also need to undo the change that caused disambiguation clearing -// */ -// if ( -// type === 'undo' && -// Object.keys(newValue ?? {}).length > 0 && -// Object.keys(oldValue ?? {}).length === 0 -// ) -// // HOT doesn't seem to like calling undo from inside of afterUndo -// globalThis.setTimeout(() => { -// wbView.undoRedoIsHandled = true; -// wbView.hot?.undo(); -// wbView.undoRedoIsHandled = false; -// wbView.disambiguation.afterChangeDisambiguation(physicalRow); -// }, 0); -// else wbView.disambiguation.afterChangeDisambiguation(physicalRow); -// } +function afterUndoRedo( + wbView: WbView, + type: 'redo' | 'undo', + data: Action +): void { + if ( + wbView.undoRedoIsHandled || + data.actionType !== 'change' || + data.changes.length !== 1 || + wbView.hot === undefined + ) + return; + + const [visualRow, visualCol, newData, oldData] = data.changes[0]; + const physicalRow = wbView.hot.toPhysicalRow(visualRow); + const physicalCol = wbView.hot.toPhysicalColumn(visualCol as number); + if (physicalCol !== wbView.dataset.columns.length) return; + + const newValue = JSON.parse(newData || '{}').disambiguation; + const oldValue = JSON.parse(oldData || '{}').disambiguation; + + /* + * Disambiguation results are cleared when any cell in a row changes. + * That change creates a separate point in the undo stack. + * Thus, if HOT tries to undo disambiguation clearing, we need to + * also need to undo the change that caused disambiguation clearing + */ + if ( + type === 'undo' && + Object.keys(newValue ?? {}).length > 0 && + Object.keys(oldValue ?? {}).length === 0 + ) + // HOT doesn't seem to like calling undo from inside of afterUndo + globalThis.setTimeout(() => { + wbView.undoRedoIsHandled = true; + wbView.hot?.undo(); + wbView.undoRedoIsHandled = false; + wbView.disambiguation.afterChangeDisambiguation(physicalRow); + }, 0); + else wbView.disambiguation.afterChangeDisambiguation(physicalRow); +} From a2dd13817c6d506a5b6b3853ac55c73cbbe3821f Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:06:26 -0400 Subject: [PATCH 028/153] Add disambiguation dialogs to spreadsheet --- .../components/WorkBench/WbSpreadsheet.tsx | 305 +++++++++++++----- 1 file changed, 227 insertions(+), 78 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 4008a15cdc6..c80b49e33ab 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -31,6 +31,16 @@ import { strictGetTable } from '../DataModel/tables'; import { getIcon, unknownIcon } from '../InitialContext/icons'; import { commonText } from '../../localization/common'; import { iconClassName } from '../Atoms/Icons'; +import { getSelectedLast } from './hotHelpers'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { getTableFromMappingPath } from '../WbPlanView/navigator'; +import { hasTablePermission } from '../Permissions/helpers'; +import type { Collection } from '../DataModel/specifyTable'; +import { AnySchema } from '../DataModel/helperTypes'; +import { Dialog } from '../Molecules/Dialog'; +import { DisambiguationDialog } from './Disambiguation'; +import { mappingPathToString } from '../WbPlanView/mappingHelpers'; +import { MappingPath } from '../WbPlanView/Mapper'; export function WbSpreadsheet({ dataset, @@ -40,6 +50,7 @@ export function WbSpreadsheet({ validation, cells, disambiguation, + hooks, }: { readonly dataset: Dataset; readonly hotRef: any; @@ -48,6 +59,7 @@ export function WbSpreadsheet({ readonly validation: WbValidationReact; readonly cells: WbCellMetaReact; readonly disambiguation: DisambiguationReact; + readonly hooks: any; }): JSX.Element { const mappings = parseWbMappings(dataset); const physicalColToMappingCol = (physicalCol: number): number | undefined => @@ -55,6 +67,81 @@ export function WbSpreadsheet({ ({ headerName }) => headerName === dataset.columns[physicalCol] ); + const [disambiguationMatches, setMatches] = React.useState<{ + readonly physicalCols: RA; + readonly mappingPath: MappingPath; + readonly ids: RA; + readonly key: string; + }>(); + const [disambiguationPhysicalRow, setPhysicalRow] = React.useState(); + const [disambiguationResource, setResource] = + React.useState>(); + const [noDisambiguationDialog, openNoDisamb, closeNoDisamb] = + useBooleanState(); + const [disambiguationDialog, openDisambiguation, closeDisambiguation] = + useBooleanState(); + + const openDisambiguationDialog = () => { + if (mappings === undefined || !hotRef.current) return; + + const [visualRow, visualCol] = getSelectedLast(hotRef.current.hotInstance); + const physicalRow = hotRef.current.hotInstance.toPhysicalRow(visualRow); + const physicalCol = hotRef.current.hotInstance.toPhysicalColumn(visualCol); + + const matches = validation?.uploadResults.ambiguousMatches[ + physicalRow + ].find(({ physicalCols }) => physicalCols.includes(physicalCol)); + if (matches === undefined) return; + const tableName = getTableFromMappingPath( + mappings.baseTable.name, + matches.mappingPath + ); + const table = strictGetTable(tableName); + const resources = new table.LazyCollection({ + filters: { id__in: matches.ids.join(',') }, + }) as Collection; + + (hasTablePermission(table.name, 'read') + ? resources.fetch({ limit: 0 }) + : Promise.resolve(resources) + ).then(({ models }) => { + if (models.length === 0) { + openNoDisamb(); + return; + } + + // Re-enable this once live validation is available again: + /* + * Disable "Apply All" if validation is still in progress. + * This is because we don't know all matches until validation is done + */ + /* + *Let applyAllAvailable = true; + *const applyAllButton = content.find('#applyAllButton'); + * + *const updateIt = () => { + * const newState = this.liveValidationStack.length === 0; + * if (newState !== applyAllAvailable) { + * applyAllAvailable = newState; + * applyAllButton.disabled = !newState; + * applyAllButton[newState ? 'removeAttribute' : 'setAttribute']( + * 'title', + * wbText.applyAllUnavailable() + * ); + * } + *}; + * + *const interval = globalThis.setInterval(updateIt, 100); + * // onClose: globalThis.clearInterval(interval); + */ + + setMatches(matches); + setResource(resources); + setPhysicalRow(physicalRow); + openDisambiguation(); + }); + }; + // Context menu item definitions (common for fillUp and fillDown) const fillCellsContextMenuItem = ( mode: 'down' | 'up' @@ -203,7 +290,7 @@ export function WbSpreadsheet({ // typeof this.coordinateConverterView === 'function' || !disambiguation.isAmbiguousCell() || !hasPermission('/workbench/dataset', 'update'), - callback: () => disambiguation.openDisambiguationDialog(), + callback: () => openDisambiguationDialog(), }, separator_1: '---------', fill_down: fillCellsContextMenuItem('down'), @@ -226,23 +313,24 @@ export function WbSpreadsheet({ }; return ( - { - const tableIcon = mappings?.mappedHeaders?.[physicalCol]; - const isMapped = tableIcon !== undefined; - const mappingCol = physicalColToMappingCol(physicalCol); - const tableName = - (typeof mappingCol === 'number' - ? mappings?.tableNames[mappingCol] - : undefined) ?? - tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; - const tableLabel = isMapped - ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' - : ''; - // REFACTOR: use new table icons - return `
+ <> + { + const tableIcon = mappings?.mappedHeaders?.[physicalCol]; + const isMapped = tableIcon !== undefined; + const mappingCol = physicalColToMappingCol(physicalCol); + const tableName = + (typeof mappingCol === 'number' + ? mappings?.tableNames[mappingCol] + : undefined) ?? + tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; + const tableLabel = isMapped + ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' + : ''; + // REFACTOR: use new table icons + return `
${ isMapped ? `
`; - }} - columns={Array.from( - // Last column is invisible and contains disambiguation metadata - { length: dataset.columns.length + 1 }, - (_, physicalCol) => ({ - // Get data from nth column for nth column - data: physicalCol, - }) + }} + columns={Array.from( + // Last column is invisible and contains disambiguation metadata + { length: dataset.columns.length + 1 }, + (_, physicalCol) => ({ + // Get data from nth column for nth column + data: physicalCol, + }) + )} + commentedCellClassName="htCommentCell" + comments={{ + displayDelay: 100, + }} + data={data as (string | null)[][]} + enterBeginsEditing={userPreferences.get( + 'workBench', + 'editor', + 'enterBeginsEditing' + )} + enterMoves={ + userPreferences.get('workBench', 'editor', 'enterMoveDirection') === + 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 } + } + hiddenColumns={{ + // Hide the disambiguation column + columns: [dataset.columns.length], + indicators: false, + // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // Typing doesn't match for handsontable 12.1.0, fixed in 14 + // copyPasteEnabled: false, + }} + hiddenRows={{ + rows: [], + indicators: false, + // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // Typing doesn't match for handsontable 12.1.0, fixed in 14 + // copyPasteEnabled: false, + }} + invalidCellClassName="-" + language={LANGUAGE} + licenseKey="non-commercial-and-evaluation" + manualColumnMove={true} + manualColumnResize={true} + minSpareRows={userPreferences.get( + 'workBench', + 'editor', + 'minSpareRows' + )} + multiColumnSorting={true} + outsideClickDeselects={false} + placeholderCellClassName="htPlaceholder" + ref={hotRef} + rowHeaders={true} + sortIndicator={true} + stretchH="all" + tabMoves={ + userPreferences.get('workBench', 'editor', 'tabMoveDirection') === + 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 } + } + readOnly={isUploaded || !hasPermission('/workbench/dataset', 'update')} + contextMenu={contextMenuConfig as Settings} + {...hooks} + /> + {noDisambiguationDialog && ( + + {wbText.noDisambiguationResultsDescription()} + )} - commentedCellClassName="htCommentCell" - comments={{ - displayDelay: 100, - }} - data={data as (string | null)[][]} - enterBeginsEditing={userPreferences.get( - 'workBench', - 'editor', - 'enterBeginsEditing' + {disambiguationDialog && ( + { + disambiguation.setDisambiguation( + disambiguationPhysicalRow!, + disambiguationMatches!.mappingPath, + selected.id + ); + validation?.startValidateRow(disambiguationPhysicalRow!); + hotRef.current.hotInstance?.render(); + }} + onSelectedAll={(selected): void => + // Loop backwards so the live validation will go from top to bottom + hotRef.current.hotInstance?.batch(() => { + for ( + let visualRow = data.length - 1; + visualRow >= 0; + visualRow-- + ) { + const physicalRow = + hotRef.current.hotInstance!.toPhysicalRow(visualRow); + if ( + !validation?.uploadResults.ambiguousMatches[ + physicalRow + ]?.find( + ({ key, mappingPath }) => + key === disambiguationMatches!.key && + typeof disambiguation.getDisambiguation(physicalRow)[ + mappingPathToString(mappingPath) + ] !== 'number' + ) + ) + continue; + disambiguation.setDisambiguation( + physicalRow, + disambiguationMatches!.mappingPath, + selected.id + ); + validation?.startValidateRow(physicalRow); + } + }) + } + /> )} - enterMoves={ - userPreferences.get('workBench', 'editor', 'enterMoveDirection') === - 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 } - } - hiddenColumns={{ - // Hide the disambiguation column - columns: [dataset.columns.length], - indicators: false, - // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error - // Typing doesn't match for handsontable 12.1.0, fixed in 14 - // copyPasteEnabled: false, - }} - hiddenRows={{ - rows: [], - indicators: false, - // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error - // Typing doesn't match for handsontable 12.1.0, fixed in 14 - // copyPasteEnabled: false, - }} - invalidCellClassName="-" - language={LANGUAGE} - licenseKey="non-commercial-and-evaluation" - manualColumnMove={true} - manualColumnResize={true} - minSpareRows={userPreferences.get('workBench', 'editor', 'minSpareRows')} - multiColumnSorting={true} - outsideClickDeselects={false} - placeholderCellClassName="htPlaceholder" - ref={hotRef} - rowHeaders={true} - // @ts-expect-error Incorrect type again. Possibly fixed in handsontable14 - sortIndicator={true} - stretchH="all" - tabMoves={ - userPreferences.get('workBench', 'editor', 'tabMoveDirection') === 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 } - } - readOnly={isUploaded || !hasPermission('/workbench/dataset', 'update')} - contextMenu={contextMenuConfig as Settings} - /> + ); } From 727052742c880511bf1829405f1fe0b152fdb3dc Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:07:18 -0400 Subject: [PATCH 029/153] Add enable/disable behavior for Export/Change Owner --- .../js_src/lib/components/WorkBench/WbToolkit.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx index 75a83633198..9120c39ae1d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx @@ -30,7 +30,7 @@ export function WbToolkit({ mappings, data, handleDatasetDelete, - actions, + hasUnSavedChanges, }: { readonly dataset: Dataset; readonly hotRef: any; @@ -38,7 +38,7 @@ export function WbToolkit({ readonly mappings: WbMapping; readonly data: RA>; readonly handleDatasetDelete: () => void; - readonly actions: WbActionsReact; + readonly hasUnSavedChanges: boolean; }): JSX.Element { const hot = hotRef.current.hotInstance; @@ -90,8 +90,8 @@ export function WbToolkit({ aria-pressed={toolkitOptions.changeOwner.show} className="wb-change-data-set-owner" onClick={toolkitOptions.changeOwner.open} - disabled={actions.hasUnSavedChanges} - title={actions.hasUnSavedChanges ? wbText.unavailableWhileEditing() : ""} + disabled={hasUnSavedChanges} + title={hasUnSavedChanges ? wbText.unavailableWhileEditing() : ""} > {wbText.changeOwner()} @@ -108,8 +108,8 @@ export function WbToolkit({ {commonText.export()} From 16c1078c7767a5d3bdb4ee406c1cdc54f8c819fe Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:07:43 -0400 Subject: [PATCH 030/153] Refactor WbUtils to React --- .../lib/components/WorkBench/WbUtils.tsx | 642 +++++++++++++++++- 1 file changed, 640 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 312065c6cbc..cbd3af1a023 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -25,12 +25,649 @@ import { getInitialSearchPreferences, WbAdvancedSearch, } from './AdvancedSearch'; -import type { WbCellCounts } from './CellMeta'; +import type { WbCellCounts, WbCellMetaReact } from './CellMeta'; import { CoordinateConverter } from './CoordinateConverter'; import { getSelectedLocalities, WbGeoLocate } from './GeoLocate'; import { getHotPlugin } from './handsontable'; import { getSelectedLast, getVisualHeaders } from './hotHelpers'; -import type { WbView } from './WbView'; +import type { WbView, Workbench } from './WbView'; +import { Input } from '../Atoms/Form'; +import { LocalizedString } from 'typesafe-i18n'; +import { Button } from '../Atoms/Button'; +import { className } from '../Atoms/className'; +import { commonText } from '../../localization/common'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { Dataset } from '../WbPlanView/Wrapped'; +import { WbMapping } from './mapping'; + +function Navigation({ + name, + label, + totalCount, + utils, + spreadsheetContainer +}: { + readonly name: keyof WbCellCounts; + readonly label: LocalizedString; + readonly totalCount: number; + readonly utils: WbUtilsReact; + readonly spreadsheetContainer: any; +}): JSX.Element { + const [currentPosition, setCurrentPosition] = React.useState(0); + const [buttonIsPressed, press, unpress, togglePress] = useBooleanState(); + const handleTypeToggle = () => { + togglePress(); + utils.toggleCellTypes(name, "toggle", spreadsheetContainer.current); + } + const handlePrevious = () => { + setCurrentPosition((prev) => Math.max(0, prev - 1)); + utils.navigateCells(name, 'previous'); + } + const handleNext = () => { + setCurrentPosition((prev) => Math.min(totalCount, prev + 1)); + utils.navigateCells(name, 'next'); + } + return ( + + + {'<'} + + + {label} ( + + {currentPosition} + + /{totalCount}) + + + {'>'} + + + ); +} + +export function WbUtilsComponent({ + isUploaded, + searchPreferences, + cellCounts, + utils, + spreadsheetContainer, +}: { + readonly isUploaded: boolean; + readonly searchPreferences: WbSearchPreferences; + readonly cellCounts: WbCellCounts; + readonly utils: WbUtilsReact; + readonly spreadsheetContainer: any; +}): JSX.Element { + const searchRef = React.useRef(null); + const replaceRef = React.useRef(null); + + const handleSearch = (event: KeyboardEvent) => { + // check if debounce is needed here + console.log("here?", event.key) + utils.searchCells(event, searchRef.current as HTMLInputElement, spreadsheetContainer.current); + } + + const handleReplace = (event: KeyboardEvent) => { + utils.replaceCells(event, replaceRef.current as HTMLInputElement); + } + + return ( + <> +
+ +
+ +
+ {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( +
+ +
+ ) : undefined} + + { + // searchPreferences = newSearchPreferences; + // if ( + // searchPreferences.navigation.direction !== + // initialNavigationDirection + // ) { + // this.wbView.cells.flushIndexedCellData = true; + // initialNavigationDirection = + // searchPreferences.navigation.direction; + // } + // if (searchPreferences.search.liveUpdate) + // this.searchCells({ + // key: 'SettingsChange', + // }).catch(softFail); + }} + /> + +
+ + {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( + + ) : undefined} + + {!isUploaded && ( + + )} +
+ + ); +} + +/* eslint-disable functional/no-this-expression */ +export class WbUtilsReact { + // eslint-disable-next-line functional/prefer-readonly-type + public searchQuery: RegExp | string | undefined = undefined; + + // eslint-disable-next-line functional/prefer-readonly-type + private rawSearchQuery: string | undefined = undefined; + + // eslint-disable-next-line functional/prefer-readonly-type + public searchPreferences: WbSearchPreferences = getInitialSearchPreferences(); + + constructor(private readonly workbench: Workbench) { + const debounced = _.debounce( + this.searchCells, + Math.ceil(clamp(10, this.workbench.data.length / 20, 200)), + false + ).bind(this); + // Workaround for _.debounce not working with async functions + // this.searchCells = async (event): Promise => debounced(event, null); + } + + navigateCells( + type: keyof WbCellCounts | null, + direction: 'previous' | 'next', + // If true and current cell is of correct type, don't navigate away + matchCurrentCell = false, + /* + * Overwrite what is considered to be a current cell + * Setting to [0,0] and matchCurrentCell=true allows navigation to the first + * cell of type (used on hitting "Enter" in the Search Box) + */ + currentCell: readonly [number, number] | undefined = undefined + ): + | { + readonly visualRow: number; + readonly visualCol: number; + } + | undefined { + /* + * Can get data-* via button.dataset, but this way is better as can find + * usages this way easily (the button.dataset converts keys to camelCase) + */ + + if (type === null) return undefined; + // const currentPositionElement = buttonParent.getElementsByClassName( + // 'wb-navigation-position' + // )[0]; + // const totalCountElement = buttonParent.getElementsByClassName( + // 'wb-navigation-total' + // )[0]; + const totalCount = 0; + + const cellMetaObject = this.workbench.cells!.getCellMetaObject(); + + /* + * The cellMetaObject is transposed if navigation direction is "Column + * first". + * In that case, the meaning of visualRow and visualCol is swapped. + * resolveIndex exists to resolve the canonical visualRow/visualCol + */ + const resolveIndex = ( + visualRow: number, + visualCol: number, + first: boolean + ): number => + (this.searchPreferences.navigation.direction === 'rowFirst') === first + ? visualRow + : visualCol; + + const [currentRow, currentCol] = + currentCell ?? getSelectedLast(this.workbench.hot); + + const [currentTransposedRow, currentTransposedCol] = [ + resolveIndex(currentRow, currentCol, true), + resolveIndex(currentRow, currentCol, false), + ]; + + const compareRows = + direction === 'next' + ? (visualRow: number) => visualRow >= currentTransposedRow + : (visualRow: number) => visualRow <= currentTransposedRow; + + const compareCols = + direction === 'next' + ? matchCurrentCell + ? (visualCol: number) => visualCol >= currentTransposedCol + : (visualCol: number) => visualCol > currentTransposedCol + : matchCurrentCell + ? (visualCol: number) => visualCol <= currentTransposedCol + : (visualCol: number) => visualCol < currentTransposedCol; + + let matchedCell: + | { + readonly visualRow: number; + readonly visualCol: number; + } + | undefined; + let cellIsTypeCount = 0; + + const orderIt = + direction === 'next' + ? f.id + : (array: RA): RA => Array.from(array).reverse(); + + orderIt(Object.entries(cellMetaObject)).find(([visualRowString, metaRow]) => + typeof metaRow === 'object' + ? orderIt(Object.entries(metaRow)).find( + ([visualColString, metaArray]) => { + /* + * This is 10 times faster then Number.parseInt because of a slow + * Babel polyfill + */ + const visualRow = (visualRowString as unknown as number) | 0; + const visualCol = (visualColString as unknown as number) | 0; + + const cellTypeMatches = this.workbench.cells?.cellIsType( + metaArray, + type + ); + cellIsTypeCount += cellTypeMatches ? 1 : 0; + + const isWithinBounds = + compareRows(visualRow) && + (visualRow !== currentTransposedRow || compareCols(visualCol)); + + const matches = cellTypeMatches && isWithinBounds; + if (matches) + matchedCell = { + visualRow: resolveIndex(visualRow, visualCol, true), + visualCol: resolveIndex(visualRow, visualCol, false), + }; + return matches; + } + ) + : undefined + ); + + let cellRelativePosition; + if (matchedCell === undefined) cellRelativePosition = 0; + else if (direction === 'next') cellRelativePosition = cellIsTypeCount; + else cellRelativePosition = totalCount - cellIsTypeCount + 1; + + const boundaryCell = direction === 'next' ? totalCount : 1; + + // const initialCellRelativePosition = Number.parseInt( + // currentPositionElement.textContent ?? '0' + // ); + const initialCellRelativePosition = 0; + // if ( + // cellRelativePosition !== 0 || + // initialCellRelativePosition !== boundaryCell || + // totalCount === 0 + // ) + // currentPositionElement.textContent = cellRelativePosition.toString(); + + if (matchedCell === undefined) return undefined; + + this.workbench.hot.selectCell(matchedCell.visualRow, matchedCell.visualCol); + + // Turn on the respective cell type if it was hidden + // this.toggleCellTypes(event, 'remove'); + + return matchedCell; + } + + searchCells( + event: KeyboardEvent | { readonly key: 'SettingsChange' }, + searchQueryElement: HTMLInputElement, + spreadsheetContainer: any + ){ + if (!this.workbench.hot) return; + console.log("here? MATE") + /* + * Don't rerun search on live search if search query did not change + * (e.x, if Ctrl/Cmd+A is clicked in the search box) + */ + if ( + searchQueryElement.value === this.rawSearchQuery && + !['SettingsChange', 'Enter'].includes(event.key) + ) + return; + + // Don't handle onKeyDown event if live search is disabled + if (event.key !== 'Enter' && !this.searchPreferences.search.liveUpdate) + return; + + if (this.parseSearchQuery(searchQueryElement) === undefined) { + // navigationTotalElement.textContent = '0'; + this.toggleCellTypes('searchResults', 'add', spreadsheetContainer); + return; + } + this.toggleCellTypes('searchResults', 'remove', spreadsheetContainer); + + // const navigationButton = + // navigationContainer.getElementsByClassName('wb-cell-navigation'); + + let resultsCount = 0; + const data = this.workbench.dataset.rows; + const firstVisibleRow = + getHotPlugin(this.workbench.hot, 'autoRowSize').getFirstVisibleRow() - 3; + const lastVisibleRow = + getHotPlugin(this.workbench.hot, 'autoRowSize').getLastVisibleRow() + 3; + const firstVisibleColumn = + getHotPlugin(this.workbench.hot, 'autoColumnSize').getFirstVisibleColumn() - + 3; + const lastVisibleColumn = + getHotPlugin(this.workbench.hot, 'autoColumnSize').getLastVisibleColumn() + + 3; + + for (let visualRow = 0; visualRow < data.length; visualRow++) { + const physicalRow = this.workbench.hot.toPhysicalRow(visualRow); + for ( + let visualCol = 0; + visualCol < this.workbench.dataset.columns.length; + visualCol++ + ) { + const physicalCol = this.workbench.hot.toPhysicalColumn(visualCol); + const isSearchResult = this.searchFunction( + (data[physicalRow][physicalCol] || + this.workbench.mappings?.defaultValues[physicalCol]) ?? + '' + ); + + let cell = undefined; + let render = false; + + /* + * Calling this.wbView.hot.getCell only if cell is within the render + * bounds. + * While hot.getCell is supposed to check for this too, doing it this + * way makes search about 25% faster + */ + if ( + firstVisibleRow <= visualRow && + lastVisibleRow >= visualRow && + firstVisibleColumn <= visualCol && + lastVisibleColumn >= visualCol + ) { + cell = this.workbench.hot.getCell(visualRow, visualCol) ?? undefined; + console.log("CELL", cell) + render = Boolean(cell); + } + + this.workbench.cells![render ? 'updateCellMeta' : 'setCellMeta']( + physicalRow, + physicalCol, + 'isSearchResult', + isSearchResult, + { + cell, + visualRow, + visualCol, + } + ); + if (isSearchResult) resultsCount += 1; + } + } + + // navigationTotalElement.textContent = resultsCount.toString(); + console.log("COUNT", resultsCount.toString()); + + // Navigate to the first search result when hitting Enter + if (event.key === 'Enter') + this.navigateCells( + "searchResults", + "next", + event.key === 'Enter', + event.key === 'Enter' ? [0, 0] : undefined + ); + + console.log("here? MATE FINISHED", this.workbench.cells!.cellMeta) + } + + parseSearchQuery(searchQueryElement: HTMLInputElement) { + if (searchQueryElement === null) return; + + this.rawSearchQuery = searchQueryElement.value; + + this.searchQuery = this.searchPreferences.search.useRegex + ? this.rawSearchQuery + : this.rawSearchQuery.trim(); + + if (this.searchQuery === '') { + this.searchQuery = undefined; + return; + } + + if (this.searchPreferences.search.useRegex) + try { + if (this.searchPreferences.search.fullMatch) { + if (!this.searchQuery.startsWith('^')) + this.searchQuery = `^${this.searchQuery}`; + if (!this.searchQuery.endsWith('$')) + this.searchQuery = `${this.searchQuery}$`; + } + // Regex may be coming from the user, thus disable strict mode + + this.searchQuery = new RegExp( + this.searchQuery, + this.searchPreferences.search.caseSensitive ? '' : 'i' + ); + } catch (error) { + searchQueryElement.setCustomValidity((error as SyntaxError).message); + searchQueryElement.reportValidity(); + this.searchQuery = undefined; + return; + } + else if (!this.searchPreferences.search.caseSensitive) + this.searchQuery = this.searchQuery.toLowerCase(); + + searchQueryElement.setCustomValidity(''); + + return this.searchQuery; + } + + toggleCellTypes( + navigationType: keyof WbCellCounts, + action: 'add' | 'remove' | 'toggle' = 'toggle', + spreadsheetContainer?: any, + ): void { + const groupName = camelToKebab(navigationType); + const cssClassName = `wb-hide-${groupName}`; + if (spreadsheetContainer) spreadsheetContainer.classList[action](cssClassName); + } + + searchFunction(initialCellValue = '') { + let cellValue = initialCellValue; + + if (this.searchQuery === undefined) return false; + + if (!this.searchPreferences.search.caseSensitive) + cellValue = cellValue.toLowerCase(); + + if (this.searchPreferences.search.useRegex) + return cellValue.search(this.searchQuery) !== -1; + + return this.searchPreferences.search.fullMatch + ? cellValue === this.searchQuery + : cellValue.includes(this.searchQuery as string); + } + + replaceCells(event: KeyboardEvent, replacementValueElement: HTMLInputElement) { + if ( + event.key !== 'Enter' || + (this.searchPreferences.search.useRegex && + this.searchQuery === undefined) || + this.workbench.hot === undefined + ) + return; + + // const buttonContainer = button.parentElement; + // const replacementValueElement = buttonContainer?.getElementsByClassName( + // 'wb-replace-value' + // )[0] as HTMLInputElement | undefined; + if (replacementValueElement === undefined) return; + const replacementValue = this.searchPreferences.search.useRegex + ? replacementValueElement.value + : replacementValueElement.value.trim(); + + const getNewCellValue = this.searchPreferences.search.fullMatch + ? (): string => replacementValue + : (cellValue: string): string => + this.searchPreferences.search.useRegex + ? cellValue.replaceAll( + // Regex may be coming from the user, thus disable strict mode + // eslint-disable-next-line require-unicode-regexp + new RegExp(this.searchQuery!, 'g'), + replacementValue + ) + : cellValue.split(this.searchQuery ?? '').join(replacementValue); + + if (this.searchPreferences.replace.replaceMode === 'replaceAll') { + // eslint-disable-next-line functional/prefer-readonly-type + const modifications: WritableArray<[number, number, string]> = []; + Object.entries(this.workbench.cells!.cellMeta).forEach( + ([physicalRow, metaRow]) => + Object.entries(metaRow).forEach(([physicalCol, metaArray]) => { + if ( + !this.workbench.cells!.getCellMetaFromArray( + metaArray, + 'isSearchResult' + ) + ) + return; + const visualRow = this.workbench.hot!.toVisualRow( + (physicalRow as unknown as number) | 0 + ); + const visualCol = this.workbench.hot!.toVisualColumn( + (physicalCol as unknown as number) | 0 + ); + const cellValue = + this.workbench.hot!.getDataAtCell(visualRow, visualCol) || ''; + // Don't replace cells with default values + if (cellValue === '') return; + modifications.push([ + visualRow, + visualCol, + getNewCellValue(cellValue), + ]); + }) + ); + this.workbench.hot.setDataAtCell(modifications); + } else { + const nextCellOfType = () => + this.navigateCells( + "searchResults", + "next", + false + ); + const [currentRow, currentCol] = getSelectedLast(this.workbench.hot); + const physicalRow = this.workbench.hot.toPhysicalRow(currentRow); + const physicalCol = this.workbench.hot.toPhysicalColumn(currentCol); + let nextCell = [currentRow, currentCol] as const; + if ( + !this.workbench.cells!.cellIsType( + this.workbench.cells!.cellMeta[physicalRow]?.[physicalCol], + 'searchResults' + ) + ) { + const next = nextCellOfType(); + if (typeof next === 'object') { + const { visualRow, visualCol } = next; + nextCell = [visualRow, visualCol]; + } + } + + if (!Array.isArray(nextCell)) return; + + this.workbench.hot.setDataAtCell( + ...nextCell, + getNewCellValue(this.workbench.hot.getDataAtCell(...nextCell)) + ); + + nextCellOfType(); + } + } +} +/* eslint-enable functional/no-this-expression */ // REFACTOR: rewrite to React /* eslint-disable functional/no-this-expression */ @@ -267,6 +904,7 @@ export class WbUtils extends Backbone.View { event: keyof WbCellCounts | { readonly target: Element }, action: 'add' | 'remove' | 'toggle' = 'toggle' ): boolean { + console.log(event, 'toggle event'); let navigationType: keyof WbCellCounts | undefined; let buttonContainer: HTMLElement | undefined; if (typeof event === 'string') { From 31b85447023eccef9eee00e806038e41918dd2db Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:08:36 -0400 Subject: [PATCH 031/153] Refactor WbValidation --- .../lib/components/WorkBench/WbValidation.tsx | 385 +++++++++++++++++- 1 file changed, 383 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index 418c43074ae..41bcb46e6e1 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -12,10 +12,391 @@ import { formatToManyIndex, formatTreeRank, } from '../WbPlanView/mappingHelpers'; -import type { WbMeta } from './CellMeta'; +import type { WbCellMetaReact, WbMeta } from './CellMeta'; import type { UploadResult } from './resultsParser'; import { resolveValidationMessage } from './resultsParser'; -import type { WbView } from './WbView'; +import type { WbView, Workbench } from './WbView'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import Handsontable from 'handsontable'; +import { WbMapping } from './mapping'; +import { WbUtils } from './WbUtils'; +import { WbActionsReact } from './WbActions'; +import { Dispatch, SetStateAction } from 'react'; + +/* eslint-disable functional/no-this-expression */ +export class WbValidationReact { + // eslint-disable-next-line functional/prefer-readonly-type + public liveValidationStack: WritableArray = []; + + // eslint-disable-next-line functional/prefer-readonly-type + private liveValidationActive: boolean = false; + + // eslint-disable-next-line functional/prefer-readonly-type + public validationMode: 'live' | 'off' | 'static'; + + // eslint-disable-next-line functional/prefer-readonly-type + public uploadResults: { + readonly ambiguousMatches: WritableArray< + WritableArray<{ + readonly physicalCols: RA; + readonly mappingPath: MappingPath; + readonly ids: RA; + readonly key: string; + }> + >; + readonly recordCounts: Partial, number>>; + readonly newRecords: Partial< + WritableArray< + WritableArray< + WritableArray< + Readonly< + readonly [ + tableName: Lowercase, + id: number, + alternativeLabel: string | '' + ] + > + > + > + > + >; + } = { + ambiguousMatches: [], + recordCounts: {}, + newRecords: [], + }; + + public constructor( + private readonly workbench: Workbench + ) { + this.stopLiveValidation(); + this.validationMode = + this.workbench.dataset.rowresults === null ? 'off' : 'static'; + } + + toggleDataCheck(): void { + if (!this.workbench?.hot) return; + this.validationMode = + this.validationMode === 'live' || + (this.workbench.mappings?.lines ?? []).length === 0 + ? 'off' + : 'live'; + + this.uploadResults = { + ambiguousMatches: [], + recordCounts: {}, + newRecords: [], + }; + this.workbench.cells!.cellMeta = []; + + switch (this.validationMode) { + case 'live': { + this.liveValidationStack = Array.from( + { length: this.workbench.hot.countRows() }, + (_, visualRow) => this.workbench.hot!.toPhysicalRow(visualRow) + ).reverse(); + this.triggerLiveValidation(); + // this.workbench.utils?.toggleCellTypes('newCells', 'remove'); + // this.workbench.utils?.toggleCellTypes('invalidCells', 'remove'); + break; + } + case 'off': { + this.liveValidationStack = []; + this.liveValidationActive = false; + break; + } + } + + this.workbench.hot.render(); + this.updateValidationButton(); + } + + public startValidateRow(physicalRow: number): void { + if (this.validationMode !== 'live') return; + this.liveValidationStack = this.liveValidationStack + .filter((row) => row !== physicalRow) + .concat(physicalRow); + this.triggerLiveValidation(); + } + + triggerLiveValidation() { + const pumpValidation = (): void => { + this.updateValidationButton(); + if (this.liveValidationStack.length === 0) { + this.liveValidationActive = false; + return; + } + this.liveValidationActive = true; + const physicalRow = this.liveValidationStack.pop(); + if (physicalRow === undefined || this.workbench.hot === undefined) return; + const rowData = this.workbench.hot.getSourceDataAtRow(physicalRow); + ajax<{ + readonly result: UploadResult; + } | null>(`/api/workbench/validate_row/${this.workbench.dataset.id}/`, { + method: 'POST', + body: rowData, + headers: { Accept: 'application/json' }, + }) + .then(({ data: result }) => { + const uploads = result?.result; + if (typeof uploads === 'object' && uploads !== null) + this.gotRowValidationResult(physicalRow, uploads); + }) + .then(pumpValidation); + }; + + if (!this.liveValidationActive) { + pumpValidation(); + } + } + + updateValidationButton(): void { + // if (!this.setCount) return; + // this.setCount(this.liveValidationStack.length); + } + + gotRowValidationResult(physicalRow: number, result: UploadResult): void { + if ( + this.validationMode !== 'live' || + this.workbench.hot?.isDestroyed !== false + ) + return; + this.uploadResults.ambiguousMatches[physicalRow] = []; + this.workbench.hot.batch(() => + this.applyRowValidationResults(physicalRow, result) + ); + this.workbench.cells?.updateCellInfoStats(); + } + + getHeadersFromMappingPath( + mappingPathFilter: RA, + tryBest = true + ): RA { + if (this.workbench.mappings === undefined) return []; + if (!tryBest) + // Find all columns with the shared parent mapping path + return this.workbench.mappings.lines + .filter(({ mappingPath }) => + pathStartsWith(mappingPath, mappingPathFilter) + ) + .map(({ headerName }) => headerName); + return ( + mappedFind(mappingPathFilter, (_, index) => { + const columns = this.workbench.mappings!.lines.filter(({ mappingPath }) => + pathStartsWith( + mappingPath, + mappingPathFilter.slice(0, index === 0 ? undefined : -1 * index) + ) + ) + .map(({ headerName }) => headerName); + return columns.length > 0 ? columns : undefined; + }) ?? [] + ); + } + + resolveValidationColumns( + initialColumns: RA, + inferColumnsCallback: (() => RA) | undefined = undefined + ) { + // See https://github.com/specify/specify7/issues/810 + let columns: RA = initialColumns.filter(Boolean); + if (typeof inferColumnsCallback === 'function') { + if (columns.length === 0) columns = inferColumnsCallback(); + if (columns.length === 0) columns = this.workbench.dataset.columns; + } + // Convert to physicalCol and filter out unknown columns + return columns + .map((column) => this.workbench.dataset.columns.indexOf(column)) + .filter((physicalCol) => physicalCol !== -1); + } + + applyRowValidationResults(physicalRow: number, result: UploadResult) { + const rowMeta: WritableArray>> = + this.workbench.dataset.columns.map(() => ({ + isNew: false, + issues: [], + })); + + const setMeta = ( + key: KEY, + value: KEY extends 'issues' ? string : WbMeta[KEY], + columns: RA, + inferColumnsCallback: (() => RA) | undefined + ): void => + this.resolveValidationColumns(columns, inferColumnsCallback).forEach( + (physicalCol) => { + if (key === 'issues') + (rowMeta[physicalCol][key] as WritableArray).push( + capitalize(value as string) + ); + else rowMeta[physicalCol][key as 'isNew'] = value as boolean; + } + ); + + this.parseRowValidationResults(result, setMeta, physicalRow); + + rowMeta.forEach((cellMeta, physicalCol) => { + // To make errors discovered by live validation visible, isModified must be unset + if (cellMeta.issues?.length !== 0 && this.validationMode === 'live') + cellMeta.isModified = false; + Object.entries(cellMeta).map(([key, value]) => + this.workbench.cells?.updateCellMeta(physicalRow, physicalCol, key, value) + ); + }); + } + + parseRowValidationResults( + result: UploadResult, + setMetaCallback: ( + key: KEY, + value: KEY extends 'issues' ? string : WbMeta[KEY], + columns: RA, + inferColumnsCallback: (() => RA) | undefined + ) => void, + physicalRow: number, + initialMappingPath: MappingPath | undefined = [] + ) { + const uploadResult = result.UploadResult; + const uploadStatus = Object.keys(uploadResult.record_result)[0]; + const statusData = uploadResult.record_result[uploadStatus]; + const data = uploadResult.record_result; + + const isTree = 'info' in statusData && statusData.info?.treeInfo !== null; + const mappingPath = isTree + ? [...initialMappingPath, formatTreeRank(statusData.info.treeInfo.rank)] + : initialMappingPath; + + const resolveColumns = this.getHeadersFromMappingPath.bind( + this, + mappingPath + ); + + // Ignore these statuses + if (['NullRecord', 'PropagatedFailure', 'Matched'].includes(uploadStatus)) { + } else if (uploadStatus === 'ParseFailures') + data.ParseFailures.failures.forEach((line) => { + const [issueMessage, payload, column] = + line.length === 2 ? [line[0], {}, line[1]] : line; + setMetaCallback( + 'issues', + whitespaceSensitive( + resolveValidationMessage(issueMessage, payload ?? {}) + ), + [column], + resolveColumns + ); + }); + else if (uploadStatus === 'NoMatch') + setMetaCallback( + 'issues', + wbText.noMatchErrorMessage(), + data.NoMatch.info.columns, + resolveColumns + ); + else if (uploadStatus === 'FailedBusinessRule') + setMetaCallback( + 'issues', + whitespaceSensitive( + resolveValidationMessage( + data.FailedBusinessRule.message, + data.FailedBusinessRule.payload ?? {} + ) + ), + data.FailedBusinessRule.info.columns, + resolveColumns + ); + else if (uploadStatus === 'MatchedMultiple') { + this.uploadResults.ambiguousMatches[physicalRow] ??= []; + this.uploadResults.ambiguousMatches[physicalRow].push({ + physicalCols: this.resolveValidationColumns( + data.MatchedMultiple.info.columns, + resolveColumns + ), + mappingPath, + ids: data.MatchedMultiple.ids, + key: data.MatchedMultiple.key, + }); + setMetaCallback( + 'issues', + whitespaceSensitive(wbText.matchedMultipleErrorMessage()), + data.MatchedMultiple.info.columns, + resolveColumns + ); + } else if (uploadStatus === 'Uploaded') { + setMetaCallback('isNew', true, data.Uploaded.info.columns, undefined); + const tableName = toLowerCase(data.Uploaded.info.tableName); + this.uploadResults.recordCounts[tableName] ??= 0; + this.uploadResults.recordCounts[tableName]! += 1; + this.uploadResults.newRecords[physicalRow] ??= []; + this.resolveValidationColumns(data.Uploaded.info.columns, undefined).map( + (physicalCol) => { + this.uploadResults.newRecords[physicalRow]![physicalCol] ??= []; + this.uploadResults.newRecords[physicalRow]![physicalCol].push([ + tableName, + data.Uploaded.id, + data.Uploaded.info?.treeInfo + ? `${data.Uploaded.info.treeInfo.name} (${data.Uploaded.info.treeInfo.rank})` + : '', + ]); + } + ); + } else + raise( + new Error( + `Trying to parse unknown uploadStatus type "${uploadStatus}" at + row ${this.workbench.hot?.toVisualRow(physicalRow) ?? ''}` + ) + ); + + Object.entries(uploadResult.toOne).forEach(([fieldName, uploadResult]) => + this.parseRowValidationResults( + uploadResult, + setMetaCallback, + physicalRow, + fieldName === 'parent' && isTree + ? mappingPath.slice(0, -1) + : [...mappingPath, fieldName] + ) + ); + + Object.entries(uploadResult.toMany).forEach(([fieldName, uploadResults]) => + uploadResults.forEach((uploadResult, toManyIndex) => + this.parseRowValidationResults( + uploadResult, + setMetaCallback, + physicalRow, + [...mappingPath, fieldName, formatToManyIndex(toManyIndex + 1)] + ) + ) + ); + } + + getValidationResults(): void { + if (!this.workbench.mappings) + return; + + if (this.workbench.dataset.rowresults === null) { + this.validationMode = 'off'; + this.updateValidationButton(); + return; + } + + this.workbench.dataset.rowresults.forEach((result, physicalRow) => { + this.applyRowValidationResults(physicalRow, result); + }); + + this.workbench.cells?.updateCellInfoStats(); + } + + stopLiveValidation(): void { + this.liveValidationStack = []; + this.liveValidationActive = false; + this.validationMode = 'off'; + } +} + +/* eslint-enable functional/no-this-expression */ + /* eslint-disable functional/no-this-expression */ export class WbValidation { From 2f4bc62caa273daebd4b8a7bf5a9dfa83514a566 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 29 Mar 2024 17:10:17 -0400 Subject: [PATCH 032/153] Add upload view, validation results and action components to WbView --- .../lib/components/WorkBench/WbView.tsx | 558 +++++++----------- 1 file changed, 206 insertions(+), 352 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index e0c94ca1f36..12064d94b58 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -37,22 +37,22 @@ import { hasPermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { WbCellMeta } from './CellMeta'; +import { WbCellCounts, WbCellMeta, WbCellMetaReact } from './CellMeta'; import { DataSetNameView } from './DataSetMeta'; import { DevShowPlan } from './DevShowPlan'; -import { Disambiguation } from './DisambiguationLogic'; +import { Disambiguation, DisambiguationReact } from './DisambiguationLogic'; import { configureHandsontable, getHotPlugin } from './handsontable'; import { downloadDataSet } from './helpers'; -import { getHotHooks } from './hooks'; +import { getHotHooks, getHotHooksReact } from './hooks'; import { getSelectedRegions } from './hotHelpers'; import type { WbMapping } from './mapping'; import { parseWbMappings } from './mapping'; import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; import { wbViewTemplate } from './Template'; -import { WbActions, WbActionsReact } from './WbActions'; -import { WbUtils } from './WbUtils'; -import { WbValidation } from './WbValidation'; +import { WbActions, WbActionsComponent } from './WbActions'; +import { WbUtils, WbUtilsComponent, WbUtilsReact } from './WbUtils'; +import { WbValidation, WbValidationReact } from './WbValidation'; import type { LocalizedString } from 'typesafe-i18n'; import { className } from '../Atoms/className'; import { Input } from '../Atoms/Form'; @@ -60,17 +60,34 @@ import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; import { WbToolkit } from './WbToolkit'; -import { getCache } from '../../utils/cache'; -import { WbAdvancedSearch } from './AdvancedSearch'; +import { + WbAdvancedSearch, + getInitialSearchPreferences, +} from './AdvancedSearch'; import type { WbSearchPreferences } from './AdvancedSearch'; -import { RollbackConfirmation } from './Components'; -import { WbStatus as WbStatusComponent } from './Status'; -import type { Status } from '../WbPlanView/Wrapped'; import { loadingBar } from '../Molecules'; -import { CreateRecordSetButton } from './RecordSet'; export type WbStatus = 'unupload' | 'upload' | 'validate'; +export type Workbench = { + dataset: Dataset; + cells: WbCellMetaReact | undefined; + disambiguation: DisambiguationReact | undefined; + validation: WbValidationReact | undefined; + data: RA>; + searchPreferences: WbSearchPreferences; + hot: Handsontable; + throttleRate: number; + mappings: WbMapping; + utils: WbUtilsReact | undefined; + setCellCounts: (cellCounts: WbCellCounts) => void; +}; + +type ActionHandlers = { + spreadSheetChanged: () => void; + spreadSheetUpToDate: () => void; +}; + function Navigation({ name, label, @@ -139,7 +156,6 @@ function useWbViewHandlers() { statusComponent: useBooleanState(), revertChanges: useBooleanState(), saveProgressBar: useBooleanState(), - operationCompleted: useBooleanState(), operationAborted: useBooleanState(), devPlan: useBooleanState(), changeOwner: useBooleanState(), @@ -163,11 +179,13 @@ export function WbViewReact({ hotRef, handleDatasetDelete, triggerRefresh, + spreadsheetContainer, }: { readonly dataset: Dataset; readonly hotRef: any; readonly handleDatasetDelete: () => void; readonly triggerRefresh: () => void; + readonly spreadsheetContainer: any; }): JSX.Element { const data = React.useMemo>>( () => @@ -180,11 +198,6 @@ export function WbViewReact({ const isUploaded = dataset.uploadresult !== null && dataset.uploadresult.success; const canUpdate = hasPermission('/workbench/dataset', 'update'); - const [canLiveValidate] = userPreferences.use( - 'workBench', - 'general', - 'liveValidation' - ); const { toolkit, upload, @@ -193,7 +206,6 @@ export function WbViewReact({ statusComponent, revertChanges, saveProgressBar, - operationCompleted, operationAborted, ...toolkitOptions } = useWbViewHandlers(); @@ -201,57 +213,151 @@ export function WbViewReact({ (): WbMapping | undefined => parseWbMappings(dataset), [dataset] ); - const hot = hotRef.current ? hotRef.current.hotInstance : undefined; - const defaultSearchPreferences: WbSearchPreferences = { - navigation: { - direction: 'columnFirst', - }, - search: { - fullMatch: true, - caseSensitive: true, - useRegex: false, - liveUpdate: true, - }, - replace: { - replaceMode: 'replaceAll', - }, - }; - let searchPreferences = - getCache('workbench', 'searchProperties') ?? defaultSearchPreferences; - const initialNavigationDirection = searchPreferences.navigation.direction; + + const searchPreferences = getInitialSearchPreferences(); + const throttleRate = Math.ceil(clamp(10, data.length / 10, 2000)); + const [hasUnSavedChanges, spreadSheetChanged, spreadSheetUpToDate] = + useBooleanState(); + + const [hotIsReady, setHotToReady] = useBooleanState(); + + React.useEffect(() => { + const getPickLists = async () => + mappings === undefined + ? {} + : await fetchWbPickLists( + dataset.columns, + mappings.tableNames, + mappings.lines + ); + if (hotRef.current) { + setHotToReady(); + const configureHotAndPickLists = async () => { + const pickLists = await getPickLists(); // Await getPickLists() here + configureHandsontable( + hotRef.current.hotInstance, + mappings, + dataset, + pickLists + ); + }; + configureHotAndPickLists(); // Call the async function + } + }, [hotRef.current]); + + const [cellCounts, setCellCounts] = React.useState({ + newCells: 0, + invalidCells: 0, + searchResults: 0, + modifiedCells: 0, + }); + + const workbench = React.useMemo(() => { + const workbench: Workbench = { + data, + dataset, + hot: hotRef.current?.hotInstance, + mappings: mappings as WbMapping, + searchPreferences, + throttleRate, + cells: undefined, + disambiguation: undefined, + validation: undefined, + utils: undefined, + setCellCounts + }; + workbench.cells = new WbCellMetaReact(workbench); + workbench.disambiguation = new DisambiguationReact(workbench); + workbench.validation = new WbValidationReact(workbench); + workbench.utils = new WbUtilsReact(workbench); + return workbench; + }, [dataset, hotIsReady]); + + const physicalColToMappingCol = (physicalCol: number): number | undefined => + mappings?.lines.findIndex( + ({ headerName }) => headerName === dataset.columns[physicalCol] + ); + const checkDeletedFail = (statusCode: number): boolean => { if (statusCode === Http.NOT_FOUND) handleDatasetDelete(); return statusCode === Http.NOT_FOUND; }; - const mode = React.useRef(undefined); - const refreshInitiatorAborted = React.useRef(false); - - const actionsRef = React.useRef( - new WbActionsReact( - data, - dataset, - mappings as WbMapping, - noUploadPlan, - upload, - statusComponent, - checkDeletedFail, - triggerRefresh, - saveProgressBar, - mode, - refreshInitiatorAborted, - operationCompleted, - operationAborted - ) + const hooks = getHotHooksReact( + workbench, + physicalColToMappingCol, + spreadSheetChanged, + checkDeletedFail ); + const [showResults, openResults, closeResults, toggleResults] = + useBooleanState(); + + // Makes the hot changes required for upload view results + React.useEffect(() => { + if (!showResults) return; + + workbench.hot.updateSettings({ readOnly: true }); + const initialHiddenRows = getHotPlugin( + workbench.hot, + 'hiddenRows' + ).getHiddenRows(); + const initialHiddenCols = getHotPlugin( + workbench.hot, + 'hiddenColumns' + ).getHiddenColumns(); + const rowsToInclude = new Set(); + const colsToInclude = new Set(); + Object.entries(workbench.cells!.cellMeta).forEach( + ([physicalRow, rowMeta]) => + rowMeta.forEach((metaArray, physicalCol) => { + if (!workbench.cells!.getCellMetaFromArray(metaArray, 'isNew')) + return; + rowsToInclude.add((physicalRow as unknown as number) | 0); + colsToInclude.add(physicalCol); + }) + ); + const rowsToHide = workbench.data + .map((_, physicalRow) => physicalRow) + .filter( + (physicalRow) => + !rowsToInclude.has(physicalRow) && + !initialHiddenRows.includes(physicalRow) + ) + .map(workbench.hot.toVisualRow); + const colsToHide = workbench.dataset.columns + .map((_, physicalCol) => physicalCol) + .filter( + (physicalCol) => + !colsToInclude.has(physicalCol) && + !initialHiddenCols.includes(physicalCol) + ) + .map(workbench.hot.toVisualColumn); + + getHotPlugin(workbench.hot, 'hiddenRows').hideRows(rowsToHide); + getHotPlugin(workbench.hot, 'hiddenColumns').hideColumns(colsToHide); + + workbench.utils!.toggleCellTypes( + 'newCells', + 'remove', + spreadsheetContainer.current + ); + workbench.hot.render(); + }, [showResults]); + + // Calls validation results on load if dataset has been validated before React.useEffect(() => { - if ( - !isUploaded && - (mappings?.lines ?? []).length === 0 && - hasPermission('/workbench/dataset', 'update') - ) { - noUploadPlan.open(); + if (dataset.rowresults) { + workbench.validation?.getValidationResults(); + if (workbench.validation?.validationMode === 'static' && !isUploaded) + workbench.utils?.toggleCellTypes( + 'invalidCells', + 'remove', + spreadsheetContainer.current + ); + + workbench.cells!.flushIndexedCellData = true; } - }, []); + }, [dataset.rowresults]); + return ( <>
- hot + hotRef.current?.hotInstance ? dataset.rows.length - : hot.countRows() - hot.countEmptyRows(true) + : hotRef.current.hotInstance.countRows() - + hotRef.current.hotInstance.countEmptyRows(true) } />
@@ -282,96 +389,17 @@ export function WbViewReact({ {wbPlanText.dataMapper()} ) : undefined} - {!isUploaded && hasPermission('/workbench/dataset', 'validate') && ( - <> - - {wbText.dataCheck()} - - - {wbText.validate()} - - - )} - - {commonText.results()} - - {isUploaded ? ( - hasPermission('/workbench/dataset', 'unupload') && ( - - {wbText.rollback()} - - ) - ) : ( - <> - {hasPermission('/workbench/dataset', 'upload') && ( - { - mode.current = 'upload'; - actionsRef.current.upload(mode.current); - }} - disabled={actionsRef.current.hasUnSavedChanges} - title={ - actionsRef.current.hasUnSavedChanges - ? wbText.unavailableWhileEditing() - : '' - } - > - {wbText.upload()} - - )} - {hasPermission('/workbench/dataset', 'update') && ( - <> - - {wbText.revert()} - - - {commonText.save()} - - - )} - - )} +
{toolkit.show && ( - )} - {unupload.show && ( - actionsRef.current.openStatus('unupload')} - /> - )} - {mode.current === 'upload' && upload.show && ( - - {commonText.cancel()} - { - actionsRef.current.startUpload(mode.current as WbStatus); - upload.close(); - }} - > - {wbText.upload()} - - - } - header={wbText.startUpload()} - onClose={upload.close} - > - {wbText.startUploadDescription()} - - )} - {noUploadPlan.show && ( - - {commonText.close()} - - {commonText.create()} - - - } - header={wbPlanText.noUploadPlan()} - onClose={noUploadPlan.close} - > - {wbPlanText.noUploadPlanDescription()} - - )} - {mode.current && statusComponent.show && ( - { - refreshInitiatorAborted.current = wasAborted; - statusComponent.close(); - triggerRefresh(); - }} + hasUnSavedChanges={hasUnSavedChanges} /> )} - {revertChanges.show && ( - - {commonText.cancel()} - - {wbText.revert()} - - - } - header={wbText.revertChanges()} - onClose={revertChanges.close} - > - {wbText.revertChangesDescription()} - - )} - {saveProgressBar.show && ( - - {loadingBar} - - )} - {operationCompleted.show && ( - - { - /* cellCounts.invalidCells === 0 && */ - mode.current === 'upload' && ( - - ) - } - {commonText.close()} - - } - // header={messages[this.wbView.refreshInitiatedBy].header} - onClose={operationCompleted.close} - > - {/* {messages[this.wbView.refreshInitiatedBy].message} */} - - )} - {operationAborted.show && ( - - {mode.current === 'validate' - ? wbText.validationCanceledDescription() - : mode.current === 'unupload' - ? wbText.rollbackCanceledDescription() - : wbText.uploadCanceledDescription()} - - )}
-
-
- -
- -
- {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( -
- -
- ) : undefined} - - { - // searchPreferences = newSearchPreferences; - // if ( - // searchPreferences.navigation.direction !== - // initialNavigationDirection - // ) { - // this.wbView.cells.flushIndexedCellData = true; - // initialNavigationDirection = - // searchPreferences.navigation.direction; - // } - // if (searchPreferences.search.liveUpdate) - // this.searchCells({ - // key: 'SettingsChange', - // }).catch(softFail); - }} + {showResults && ( + )}
+ ); } @@ -812,6 +664,8 @@ export class WbView extends Backbone.View { pickLists ); + console.log(this.dataset, 'LOOK DATASET'); + if (this.dataset.rowresults) this.validation.getValidationResults(); // CHeck if any column is reordered From 7fa4d8b284570345aa415249bc67aca9b72580d5 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:57:54 -0400 Subject: [PATCH 033/153] Refactor WbToolkit into smaller components - Removes the mega-state for dialog handlers - Removes some unused Backbone and old React components - Old WbView code will be cleaned up at the end --- .../lib/components/WorkBench/DataSetMeta.tsx | 144 +----------- .../WorkBench/Toolkit/ChangeOwner.tsx | 128 +++++++++++ .../{ => Toolkit}/CoordinateConverter.tsx | 69 ++++-- .../WorkBench/{ => Toolkit}/DevShowPlan.tsx | 66 +++++- .../WorkBench/{ => Toolkit}/GeoLocate.tsx | 61 +++++- .../WorkBench/Toolkit/WbLeafletMap.tsx | 75 +++++++ .../WorkBench/Toolkit/WbToolkit.tsx | 108 +++++++++ .../lib/components/WorkBench/WbToolkit.tsx | 206 ------------------ .../lib/components/WorkBench/WbUtils.tsx | 3 +- .../lib/components/WorkBench/WbView.tsx | 127 +---------- 10 files changed, 485 insertions(+), 502 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx rename specifyweb/frontend/js_src/lib/components/WorkBench/{ => Toolkit}/CoordinateConverter.tsx (77%) rename specifyweb/frontend/js_src/lib/components/WorkBench/{ => Toolkit}/DevShowPlan.tsx (50%) rename specifyweb/frontend/js_src/lib/components/WorkBench/{ => Toolkit}/GeoLocate.tsx (79%) create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx delete mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx index f56026d9aa7..87527bc8703 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx @@ -1,31 +1,23 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; -import { useAsyncState } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useId } from '../../hooks/useId'; import { commonText } from '../../localization/common'; import { StringToJsx } from '../../localization/utils'; import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; -import { formData } from '../../utils/ajax/helpers'; import { ping } from '../../utils/ajax/ping'; -import type { RA } from '../../utils/types'; -import { defined, localized, overwriteReadOnly } from '../../utils/types'; +import { localized, overwriteReadOnly } from '../../utils/types'; import { Button } from '../Atoms/Button'; -import { Form, Input, Label, Select } from '../Atoms/Form'; +import { Form, Input, Label } from '../Atoms/Form'; import { icons } from '../Atoms/Icons'; import { formatNumber } from '../Atoms/Internationalization'; import { Submit } from '../Atoms/Submit'; import type { EagerDataSet } from '../AttachmentsBulkImport/Import'; import { LoadingContext } from '../Core/Contexts'; -import { Backbone } from '../DataModel/backbone'; -import { fetchCollection } from '../DataModel/collection'; import { getField } from '../DataModel/helpers'; -import type { SerializedResource } from '../DataModel/helperTypes'; import { tables } from '../DataModel/tables'; -import type { SpecifyUser } from '../DataModel/types'; -import { userInformation } from '../InitialContext/userInformation'; import { useTitle } from '../Molecules/AppTitle'; import { AutoGrowTextArea } from '../Molecules/AutoGrowTextArea'; import { DateElement } from '../Molecules/DateElement'; @@ -396,134 +388,4 @@ export function DataSetName({ )} ); -} - -const fetchListOfUsers = async (): Promise< - RA> -> => - fetchCollection('SpecifyUser', { limit: 500, domainFilter: false }).then( - ({ records: users }) => users.filter(({ id }) => id !== userInformation.id) - ); - -export function ChangeOwner({ - dataset, - onClose: handleClose, -}: { - readonly dataset: Dataset; - readonly onClose: () => void; -}): JSX.Element | null { - const [users] = useAsyncState>>( - fetchListOfUsers, - true - ); - - const id = useId('change-data-set-owner'); - const [newOwner, setNewOwner] = React.useState(undefined); - const [isChanged, setIsChanged] = React.useState(false); - const loading = React.useContext(LoadingContext); - - return users === undefined ? null : isChanged ? ( - unsafeNavigate('/specify/', { replace: true })} - > -

{wbText.dataSetOwnerChanged()}

-
- ) : ( - - {commonText.cancel()} - - {wbText.changeOwner()} - - - } - header={wbText.changeDataSetOwner()} - onClose={handleClose} - > -
- loading( - ping(`/api/workbench/transfer/${dataset.id}/`, { - method: 'POST', - body: formData({ - specifyuserid: newOwner!, - }), - }).then(() => setIsChanged(true)) - ) - } - > - -

{wbText.changeDataSetOwnerDescription()}

- -
-
-
- ); -} - -// A wrapper for DS Meta for embedding in the WB -export class DataSetNameView extends Backbone.View { - // eslint-disable-next-line functional/prefer-readonly-type - private dataSetMeta: (() => void) | undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - private changeOwnerView: (() => void) | undefined; - - public constructor( - private readonly options: { - readonly dataset: Dataset; - readonly el: HTMLElement; - readonly display: ( - jsx: JSX.Element, - element?: HTMLElement, - destructor?: () => void - ) => () => void; - readonly getRowCount: () => number; - } - ) { - super(options); - } - - public render(): this { - const nameContainer = - this.options.el.getElementsByClassName('wb-name-container')?.[0]; - this.dataSetMeta = this.options.display( - , - defined(nameContainer, 'Unable to find Wb Name container') as HTMLElement - ); - return this; - } - - public changeOwner(): void { - const handleClose = (): void => void this.changeOwnerView?.(); - this.changeOwnerView = this.options.display( - - ); - } - - public remove(): this { - this.dataSetMeta?.(); - this.changeOwnerView?.(); - Backbone.View.prototype.remove.call(this); - return this; - } -} +} \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx new file mode 100644 index 00000000000..c1539b10535 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx @@ -0,0 +1,128 @@ +import React from "react"; + +import { useAsyncState } from '../../../hooks/useAsyncState'; +import { formData } from '../../../utils/ajax/helpers'; +import type { RA } from '../../../utils/types'; +import { Form, Label, Select } from '../../Atoms/Form'; +import { fetchCollection } from '../../DataModel/collection'; +import type { SerializedResource } from '../../DataModel/helperTypes'; +import { userInformation } from '../../InitialContext/userInformation'; +import type { SpecifyUser } from '../../DataModel/types'; +import { useId } from '../../../hooks/useId'; +import type { Dataset } from "../../WbPlanView/Wrapped"; +import { useBooleanState } from "../../../hooks/useBooleanState"; +import { ping } from '../../../utils/ajax/ping'; +import { Submit } from '../../Atoms/Submit'; +import { LoadingContext } from '../../Core/Contexts'; +import { Dialog } from '../../Molecules/Dialog'; +import { unsafeNavigate } from '../../Router/Router'; +import { Button } from '../../Atoms/Button'; +import { commonText } from '../../../localization/common'; +import { wbText } from "../../../localization/workbench"; + +export function WbChangeOwner({ + hasUnSavedChanges, + dataset, +}: { + readonly hasUnSavedChanges: boolean; + readonly dataset: Dataset; +}): JSX.Element { + const [showChangeOwner, openChangeOwner, closeChangeOwner] = + useBooleanState(); + return ( + <> + + {wbText.changeOwner()} + + {showChangeOwner && ( + + )} + + ); +} + +const fetchListOfUsers = async (): Promise< + RA> +> => + fetchCollection('SpecifyUser', { limit: 500, domainFilter: false }).then( + ({ records: users }) => users.filter(({ id }) => id !== userInformation.id) + ); + +function ChangeOwner({ + dataset, + onClose: handleClose, +}: { + readonly dataset: Dataset; + readonly onClose: () => void; +}): JSX.Element | null { + const [users] = useAsyncState>>( + fetchListOfUsers, + true + ); + + const id = useId('change-data-set-owner'); + const [newOwner, setNewOwner] = React.useState(undefined); + const [isChanged, setIsChanged] = React.useState(false); + const loading = React.useContext(LoadingContext); + + return users === undefined ? null : isChanged ? ( + unsafeNavigate('/specify/', { replace: true })} + > +

{wbText.dataSetOwnerChanged()}

+
+ ) : ( + + {commonText.cancel()} + + {wbText.changeOwner()} + + + } + header={wbText.changeDataSetOwner()} + onClose={handleClose} + > +
+ loading( + ping(`/api/workbench/transfer/${dataset.id}/`, { + method: 'POST', + body: formData({ + specifyuserid: newOwner!, + }), + }).then(() => setIsChanged(true)) + ) + } + > + +

{wbText.changeDataSetOwnerDescription()}

+ +
+
+
+ ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/CoordinateConverter.tsx similarity index 77% rename from specifyweb/frontend/js_src/lib/components/WorkBench/CoordinateConverter.tsx rename to specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/CoordinateConverter.tsx index fb4f6f31b92..4e1b921c91f 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/CoordinateConverter.tsx @@ -1,18 +1,61 @@ import type Handsontable from 'handsontable'; import React from 'react'; -import { useCachedState } from '../../hooks/useCachedState'; -import { commonText } from '../../localization/common'; -import { localityText } from '../../localization/locality'; -import { wbText } from '../../localization/workbench'; -import type { ConversionFunction } from '../../utils/latLong'; -import { Lat, Long } from '../../utils/latLong'; -import type { RA, RR } from '../../utils/types'; -import { Ul } from '../Atoms'; -import { Button } from '../Atoms/Button'; -import { Input, Label } from '../Atoms/Form'; -import { Dialog } from '../Molecules/Dialog'; -import { getSelectedCells, getSelectedLast, setHotData } from './hotHelpers'; +import { useCachedState } from '../../../hooks/useCachedState'; +import { commonText } from '../../../localization/common'; +import { localityText } from '../../../localization/locality'; +import { wbText } from '../../../localization/workbench'; +import type { ConversionFunction } from '../../../utils/latLong'; +import { Lat, Long } from '../../../utils/latLong'; +import type { RA, RR } from '../../../utils/types'; +import { Ul } from '../../Atoms'; +import { Button } from '../../Atoms/Button'; +import { Input, Label } from '../../Atoms/Form'; +import { Dialog } from '../../Molecules/Dialog'; +import { getSelectedCells, getSelectedLast, setHotData } from '../hotHelpers'; +import { useBooleanState } from '../../../hooks/useBooleanState'; +import type { Dataset } from '../../WbPlanView/Wrapped'; +import type { WbMapping } from '../mapping'; + +export function WbConvertCoordinates({ + hasLocality, + dataset, + data, + mappings, + hot, +}: { + readonly hasLocality: boolean; + readonly dataset: Dataset; + readonly data: RA>; + readonly mappings: WbMapping; + readonly hot: Handsontable; +}): JSX.Element { + const [showConvertCoords, openConvertCoords, closeConvertCoords] = + useBooleanState(); + return ( + <> + + {wbText.convertCoordinates()} + + {showConvertCoords && ( + + )} + + ); +} const options: RA<{ readonly label: string; @@ -51,7 +94,7 @@ const options: RA<{ }, ]; -export function CoordinateConverter({ +function CoordinateConverter({ hot, data, columns, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx similarity index 50% rename from specifyweb/frontend/js_src/lib/components/WorkBench/DevShowPlan.tsx rename to specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx index 2d4e208d77e..0e2c31c2818 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx @@ -1,20 +1,62 @@ import React from 'react'; -import { commonText } from '../../localization/common'; -import { wbPlanText } from '../../localization/wbPlan'; -import { Http } from '../../utils/ajax/definitions'; -import { ping } from '../../utils/ajax/ping'; -import { Button } from '../Atoms/Button'; -import { LoadingContext } from '../Core/Contexts'; -import { AutoGrowTextArea } from '../Molecules/AutoGrowTextArea'; -import { Dialog } from '../Molecules/Dialog'; -import { downloadFile } from '../Molecules/FilePicker'; -import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; +import { commonText } from '../../../localization/common'; +import { wbPlanText } from '../../../localization/wbPlan'; +import { Http } from '../../../utils/ajax/definitions'; +import { ping } from '../../../utils/ajax/ping'; +import { Button } from '../../Atoms/Button'; +import { LoadingContext } from '../../Core/Contexts'; +import { AutoGrowTextArea } from '../../Molecules/AutoGrowTextArea'; +import { Dialog } from '../../Molecules/Dialog'; +import { downloadFile } from '../../Molecules/FilePicker'; +import type { UploadPlan } from '../../WbPlanView/uploadPlanParser'; +import { overwriteReadOnly } from '../../../utils/types'; +import type { Dataset } from '../../WbPlanView/Wrapped'; +import { useBooleanState } from '../../../hooks/useBooleanState'; +import { wbText } from '../../../localization/workbench'; + + +export function WbDevPlan({ + dataset, + handleDatasetDelete, + triggerRefresh, +}: { + readonly dataset: Dataset; + readonly handleDatasetDelete: () => void; + readonly triggerRefresh: () => void; +}): JSX.Element { + const [showDevPlan, openDevPlan, closeDevPlan] = useBooleanState(); + return ( + <> + + {wbText.uploadPlan()} + + {showDevPlan && ( + { + overwriteReadOnly(dataset, 'uploadplan', plan); + triggerRefresh(); + }} + onClose={closeDevPlan} + onDeleted={handleDatasetDelete} + /> + )} + + ); +} /** - * Show upload plan as JSON. Available in Development only + * Show upload plan as JSON */ -export function DevShowPlan({ +function DevShowPlan({ dataSetId, dataSetName: name, uploadPlan: rawPlan, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx similarity index 79% rename from specifyweb/frontend/js_src/lib/components/WorkBench/GeoLocate.tsx rename to specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx index d6e6a90a69f..99ede0de483 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx @@ -1,21 +1,62 @@ import type Handsontable from 'handsontable'; import React from 'react'; -import { commonText } from '../../localization/common'; -import { f } from '../../utils/functools'; -import type { IR, RA } from '../../utils/types'; -import { filterArray } from '../../utils/types'; -import { sortFunction } from '../../utils/utils'; -import { Button } from '../Atoms/Button'; +import { commonText } from '../../../localization/common'; +import { f } from '../../../utils/functools'; +import type { IR, RA } from '../../../utils/types'; +import { filterArray } from '../../../utils/types'; +import { sortFunction } from '../../../utils/utils'; +import { Button } from '../../Atoms/Button'; import { getLocalityCoordinate, getSelectedLocalityColumns, -} from '../Leaflet/wbLocalityDataExtractor'; -import type { GeoLocatePayload } from '../Molecules/GeoLocate'; -import { GenericGeoLocate } from '../Molecules/GeoLocate'; -import { getSelectedRegions, getVisualHeaders, setHotData } from './hotHelpers'; +} from '../../Leaflet/wbLocalityDataExtractor'; +import type { GeoLocatePayload } from '../../Molecules/GeoLocate'; +import { GenericGeoLocate } from '../../Molecules/GeoLocate'; +import { getSelectedRegions, getVisualHeaders, setHotData } from '../hotHelpers'; +import { useBooleanState } from '../../../hooks/useBooleanState'; +import type { Dataset } from '../../WbPlanView/Wrapped'; +import type { WbMapping } from '../mapping'; +import { wbText } from '../../../localization/workbench'; +import { localityText } from '../../../localization/locality'; export function WbGeoLocate({ + hasLocality, + hot, + dataset, + mappings, +}: { + readonly hasLocality: boolean; + readonly hot: Handsontable; + readonly dataset: Dataset; + readonly mappings: WbMapping; +}): JSX.Element { + const [showGeoLocate, openGeoLocate, closeGeoLocate] = useBooleanState(); + return ( + <> + + {localityText.geoLocate()} + + {showGeoLocate && mappings && ( + + )} + + ); +} + +function GeoLocate({ hot, columns, localityColumns, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx new file mode 100644 index 00000000000..6cb548b87b0 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import Handsontable from "handsontable"; + +import { getSelectedLocalities } from './GeoLocate'; +import { getLocalitiesDataFromSpreadsheet } from '../../Leaflet/wbLocalityDataExtractor'; +import { getSelectedLast, getVisualHeaders } from '../hotHelpers'; +import { LeafletMap } from '../../Leaflet/Map'; +import { useBooleanState } from '../../../hooks/useBooleanState'; +import type { Dataset } from "../../WbPlanView/Wrapped"; +import type { WbMapping } from "../mapping"; +import { Button } from "../../Atoms/Button"; +import { localityText } from "../../../localization/locality"; +import { wbText } from "../../../localization/workbench"; + +export function WbLeafletMap({ + hasLocality, + hot, + dataset, + mappings, + }: { + readonly hasLocality: boolean; + readonly hot: Handsontable; + readonly dataset: Dataset; + readonly mappings: WbMapping; + }): JSX.Element { + const [showLeafletMap, openLeafletMap, closeLeafletMap] = useBooleanState(); + const localityPoints = React.useMemo(() => { + const selection = getSelectedLocalities( + hot, + dataset.columns, + mappings.localityColumns, + false + ); + + if (!selection) return undefined; + + return getLocalitiesDataFromSpreadsheet( + mappings.localityColumns, + selection.visualRows.map((visualRow) => hot!.getDataAtRow(visualRow)), + getVisualHeaders(hot, dataset.columns), + selection.visualRows + ); + }, [mappings.localityColumns]); + + return ( + <> + + {localityText.geoMap()} + + {showLeafletMap && ( + { + if (localityPoints === undefined) return; + const rowNumber = localityPoints[localityPoint].rowNumber.value; + if (typeof rowNumber !== 'number') + throw new Error('rowNumber must be a number'); + const [_currentRow, currentCol] = getSelectedLast(hot!); + hot?.scrollViewportTo(rowNumber, currentCol); + // Select entire row + hot?.selectRows(rowNumber); + }} + /> + )} + + ); + } \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx new file mode 100644 index 00000000000..786b5f9dc30 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx @@ -0,0 +1,108 @@ +import React from 'react'; + +import { hasPermission, hasTablePermission } from '../../Permissions/helpers'; +import { WbChangeOwner } from './ChangeOwner'; +import { WbConvertCoordinates } from './CoordinateConverter'; +import { commonText } from '../../../localization/common'; +import { wbText } from '../../../localization/workbench'; +import type { RA } from '../../../utils/types'; +import { Button } from '../../Atoms/Button'; +import { raise } from '../../Errors/Crash'; +import { userPreferences } from '../../Preferences/userPreferences'; +import type { Dataset } from '../../WbPlanView/Wrapped'; +import { downloadDataSet } from '../helpers'; +import type { WbMapping } from '../mapping'; +import { WbDevPlan } from './DevShowPlan'; +import { WbGeoLocate } from './GeoLocate'; +import { WbLeafletMap } from './WbLeafletMap'; + +export function WbToolkit({ + dataset, + hotRef, + mappings, + data, + handleDatasetDelete, + hasUnSavedChanges, + triggerRefresh +}: { + readonly dataset: Dataset; + readonly hotRef: any; + readonly mappings: WbMapping; + readonly data: RA>; + readonly handleDatasetDelete: () => void; + readonly hasUnSavedChanges: boolean; + readonly triggerRefresh: () => void; +}): JSX.Element { + const hot = hotRef.current.hotInstance; + + const handleExport = React.useCallback((): void => { + const delimiter = userPreferences.get( + 'workBench', + 'editor', + 'exportFileDelimiter' + ); + + downloadDataSet( + dataset.name, + dataset.rows, + dataset.columns, + delimiter + ).catch(raise); + }, [dataset]); + + const hasLocality = mappings ? mappings.localityColumns.length > 0 : false; + return ( +
+ {hasPermission('/workbench/dataset', 'transfer') && + hasTablePermission('SpecifyUser', 'read') ? ( + <> + + + + ) : undefined} + + {commonText.export()} + + + {hasPermission('/workbench/dataset', 'update') && ( + <> + + + + )} + +
+ ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx deleted file mode 100644 index 9120c39ae1d..00000000000 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbToolkit.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import React from 'react'; - -import { hasPermission, hasTablePermission } from '../Permissions/helpers'; -import { ChangeOwner } from './DataSetMeta'; -import { localityText } from '../../localization/locality'; -import { getSelectedLocalities, WbGeoLocate } from './GeoLocate'; -import { getLocalitiesDataFromSpreadsheet } from '../Leaflet/wbLocalityDataExtractor'; -import { getSelectedLast, getVisualHeaders } from './hotHelpers'; -import { LeafletMap } from '../Leaflet/Map'; -import { CoordinateConverter } from './CoordinateConverter'; -import { commonText } from '../../localization/common'; -import { wbText } from '../../localization/workbench'; -import type { RA } from '../../utils/types'; -import { Button } from '../Atoms/Button'; -import { raise } from '../Errors/Crash'; -import { userPreferences } from '../Preferences/userPreferences'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import { downloadDataSet } from './helpers'; -import type { WbMapping } from './mapping'; -import type { HandlersObject } from './WbView'; -import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; -import { DevShowPlan } from './DevShowPlan'; -import { overwriteReadOnly } from '../../utils/types'; -import { WbActionsReact } from './WbActions'; - -export function WbToolkit({ - dataset, - hotRef, - toolkitOptions, - mappings, - data, - handleDatasetDelete, - hasUnSavedChanges, -}: { - readonly dataset: Dataset; - readonly hotRef: any; - readonly toolkitOptions: HandlersObject; - readonly mappings: WbMapping; - readonly data: RA>; - readonly handleDatasetDelete: () => void; - readonly hasUnSavedChanges: boolean; -}): JSX.Element { - const hot = hotRef.current.hotInstance; - - const handleExport = React.useCallback((): void => { - const delimiter = userPreferences.get( - 'workBench', - 'editor', - 'exportFileDelimiter' - ); - - downloadDataSet( - dataset.name, - dataset.rows, - dataset.columns, - delimiter - ).catch(raise); - }, [dataset]); - - const localityPoints = React.useMemo(() => { - const selection = getSelectedLocalities( - hot, - dataset.columns, - mappings.localityColumns, - false - ); - - if (!selection) return undefined; - - return getLocalitiesDataFromSpreadsheet( - mappings.localityColumns, - selection.visualRows.map((visualRow) => hot!.getDataAtRow(visualRow)), - getVisualHeaders(hot, dataset.columns), - selection.visualRows - ); - }, [mappings.localityColumns]); - - const hasLocality = mappings ? mappings.localityColumns.length > 0 : false; - return ( -
- {hasPermission('/workbench/dataset', 'transfer') && - hasTablePermission('SpecifyUser', 'read') ? ( - <> - - {wbText.changeOwner()} - - - {wbText.uploadPlan()} - - - ) : undefined} - - {commonText.export()} - - - {hasPermission('/workbench/dataset', 'update') && ( - <> - - {wbText.convertCoordinates()} - - - {localityText.geoLocate()} - - - )} - - {localityText.geoMap()} - - {toolkitOptions.changeOwner.show && ( - - )} - {toolkitOptions.devPlan.show && ( - { - overwriteReadOnly(dataset, 'uploadplan', plan); - // TODO: figure out this trigger - // trigger('refresh'); - }} - onClose={toolkitOptions.devPlan.close} - onDeleted={handleDatasetDelete} - /> - )} - {toolkitOptions.coordinatesConversion.show && ( - - )} - {toolkitOptions.geoLocate.show && mappings && ( - - )} - {toolkitOptions.leafletMap.show && ( - { - if (localityPoints === undefined) return; - const rowNumber = localityPoints[localityPoint].rowNumber.value; - if (typeof rowNumber !== 'number') - throw new Error('rowNumber must be a number'); - const [_currentRow, currentCol] = getSelectedLast(hot!); - hot?.scrollViewportTo(rowNumber, currentCol); - // Select entire row - hot?.selectRows(rowNumber); - }} - /> - )} -
- ); -} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index cbd3af1a023..8f5ca61f541 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -26,8 +26,7 @@ import { WbAdvancedSearch, } from './AdvancedSearch'; import type { WbCellCounts, WbCellMetaReact } from './CellMeta'; -import { CoordinateConverter } from './CoordinateConverter'; -import { getSelectedLocalities, WbGeoLocate } from './GeoLocate'; +import { getSelectedLocalities, WbGeoLocate } from './Toolkit/GeoLocate'; import { getHotPlugin } from './handsontable'; import { getSelectedLast, getVisualHeaders } from './hotHelpers'; import type { WbView, Workbench } from './WbView'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 12064d94b58..d1e762b186c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -38,8 +38,6 @@ import { userPreferences } from '../Preferences/userPreferences'; import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbCellCounts, WbCellMeta, WbCellMetaReact } from './CellMeta'; -import { DataSetNameView } from './DataSetMeta'; -import { DevShowPlan } from './DevShowPlan'; import { Disambiguation, DisambiguationReact } from './DisambiguationLogic'; import { configureHandsontable, getHotPlugin } from './handsontable'; import { downloadDataSet } from './helpers'; @@ -53,19 +51,12 @@ import { wbViewTemplate } from './Template'; import { WbActions, WbActionsComponent } from './WbActions'; import { WbUtils, WbUtilsComponent, WbUtilsReact } from './WbUtils'; import { WbValidation, WbValidationReact } from './WbValidation'; -import type { LocalizedString } from 'typesafe-i18n'; -import { className } from '../Atoms/className'; -import { Input } from '../Atoms/Form'; import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { WbToolkit } from './WbToolkit'; -import { - WbAdvancedSearch, - getInitialSearchPreferences, -} from './AdvancedSearch'; +import { WbToolkit } from './Toolkit/WbToolkit'; +import { getInitialSearchPreferences } from './AdvancedSearch'; import type { WbSearchPreferences } from './AdvancedSearch'; -import { loadingBar } from '../Molecules'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -83,97 +74,6 @@ export type Workbench = { setCellCounts: (cellCounts: WbCellCounts) => void; }; -type ActionHandlers = { - spreadSheetChanged: () => void; - spreadSheetUpToDate: () => void; -}; - -function Navigation({ - name, - label, -}: { - readonly name: string; - readonly label: LocalizedString; -}): JSX.Element { - return ( - - - {'<'} - - - {label} (0/ - 0) - - - {'>'} - - - ); -} - -export type DialogHandlers = { - show: boolean; - open: () => void; - close: () => void; - toggle: () => void; -}; - -export type HandlersObject = { - [key: string]: DialogHandlers; -}; - -// Returns an object consisting of handler objects for dialog actions in the Workbench -function useWbViewHandlers() { - const handlers = { - toolkit: useBooleanState(), - upload: useBooleanState(), - noUploadPlan: useBooleanState(), - unupload: useBooleanState(), - statusComponent: useBooleanState(), - revertChanges: useBooleanState(), - saveProgressBar: useBooleanState(), - operationAborted: useBooleanState(), - devPlan: useBooleanState(), - changeOwner: useBooleanState(), - geoLocate: useBooleanState(), - leafletMap: useBooleanState(), - coordinatesConversion: useBooleanState(), - }; - - // convert useBooleanState()'s resulting array to an object - const handlersObject: HandlersObject = {}; - for (const [name, state] of Object.entries(handlers)) { - const [show, open, close, toggle] = state; - handlersObject[name] = { show, open, close, toggle }; - } - - return handlersObject; -} - export function WbViewReact({ dataset, hotRef, @@ -198,17 +98,6 @@ export function WbViewReact({ const isUploaded = dataset.uploadresult !== null && dataset.uploadresult.success; const canUpdate = hasPermission('/workbench/dataset', 'update'); - const { - toolkit, - upload, - noUploadPlan, - unupload, - statusComponent, - revertChanges, - saveProgressBar, - operationAborted, - ...toolkitOptions - } = useWbViewHandlers(); const mappings = React.useMemo( (): WbMapping | undefined => parseWbMappings(dataset), [dataset] @@ -219,6 +108,8 @@ export function WbViewReact({ const [hasUnSavedChanges, spreadSheetChanged, spreadSheetUpToDate] = useBooleanState(); + const [showToolkit, _openToolkit, _closeToolkit, toggleToolkit] = + useBooleanState(); const [hotIsReady, setHotToReady] = useBooleanState(); React.useEffect(() => { @@ -264,7 +155,7 @@ export function WbViewReact({ disambiguation: undefined, validation: undefined, utils: undefined, - setCellCounts + setCellCounts, }; workbench.cells = new WbCellMetaReact(workbench); workbench.disambiguation = new DisambiguationReact(workbench); @@ -377,9 +268,9 @@ export function WbViewReact({
{commonText.tools()} @@ -401,15 +292,15 @@ export function WbViewReact({ toggleResults={toggleResults} />
- {toolkit.show && ( + {showToolkit && ( )}
From 8073da620934b662cfe2678c54da865804fe3533 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 2 Apr 2024 13:55:52 -0400 Subject: [PATCH 034/153] Refactor WbActions into smaller components - Adds smaller components to Workbench/Actions folder --- .../WorkBench/Actions/WbActions.tsx | 315 +++++ .../WorkBench/Actions/WbResults.tsx | 27 + .../components/WorkBench/Actions/WbRevert.tsx | 54 + .../WorkBench/Actions/WbRollback.tsx | 39 + .../components/WorkBench/Actions/WbSave.tsx | 78 ++ .../components/WorkBench/Actions/WbUpload.tsx | 66 + .../WorkBench/Actions/WbValidate.tsx | 50 + .../lib/components/WorkBench/WbActions.tsx | 1091 ----------------- .../lib/components/WorkBench/WbView.tsx | 59 +- 9 files changed, 657 insertions(+), 1122 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx delete mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx new file mode 100644 index 00000000000..0d1aa712963 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx @@ -0,0 +1,315 @@ +import React from 'react'; + +import { commonText } from '../../../localization/common'; +import { wbPlanText } from '../../../localization/wbPlan'; +import { wbText } from '../../../localization/workbench'; +import { Http } from '../../../utils/ajax/definitions'; +import { ping } from '../../../utils/ajax/ping'; +import { Button } from '../../Atoms/Button'; +import { Link } from '../../Atoms/Link'; +import { Dialog } from '../../Molecules/Dialog'; +import type { Status } from '../../WbPlanView/Wrapped'; +import { CreateRecordSetButton } from '../RecordSet'; +import { WbStatus as WbStatusComponent } from '../Status'; +import type { WbStatus, Workbench } from '../WbView'; +import type { Dataset } from '../../WbPlanView/Wrapped'; +import type { WbMapping } from '../mapping'; +import { hasPermission } from '../../Permissions/helpers'; +import { userPreferences } from '../../Preferences/userPreferences'; +import { useBooleanState } from '../../../hooks/useBooleanState'; +import { WbResults } from './WbResults'; +import { WbValidate } from './WbValidate'; +import { WbRollback } from './WbRollback'; +import { WbUpload } from './WbUpload'; +import { WbRevert } from './WbRevert'; +import { WbSave } from './WbSave'; + +export function useWbActions({ + datasetId, + triggerRefresh, + checkDeletedFail, + openStatus, + workbench, +}: { + readonly datasetId: number; + readonly triggerRefresh: () => void; + readonly checkDeletedFail: (statusCode: number) => void; + readonly openStatus: () => void; + readonly workbench: Workbench; +}) { + const mode = React.useRef(undefined); + const refreshInitiatorAborted = React.useRef(false); + + const startUpload = (newMode: WbStatus): void => { + workbench.validation.stopLiveValidation(); + // TODO: figure out what to do in updateValidationButton(); + workbench.validation.updateValidationButton(); + ping(`/api/workbench/${newMode}/${datasetId}/`, { + method: 'POST', + expectedErrors: [Http.CONFLICT], + }) + .then((statusCode): void => { + checkDeletedFail(statusCode); + checkConflictFail(statusCode); + }) + .then(() => triggerStatusComponent(newMode)); + }; + + const triggerStatusComponent = (newMode: WbStatus): void => { + mode.current = newMode; + openStatus(); + }; + + const checkConflictFail = (statusCode: number): boolean => { + if (statusCode === Http.CONFLICT) + /* + * Upload/Validation/Un-Upload has been initialized by another session + * Need to reload the page to display the new state + */ + triggerRefresh(); + return statusCode === Http.CONFLICT; + }; + + return { + mode, + refreshInitiatorAborted, + startUpload, + triggerStatusComponent, + }; +} + +export function WbActionsComponent({ + dataset, + hasUnSavedChanges, + isUploaded, + triggerRefresh, + mappings, + checkDeletedFail, + spreadSheetUpToDate, + workbench, + onToggleResults, +}: { + readonly dataset: Dataset; + readonly hasUnSavedChanges: boolean; + readonly isUploaded: boolean; + readonly triggerRefresh: () => void; + readonly mappings: WbMapping; + readonly checkDeletedFail: (statusCode: number) => void; + readonly spreadSheetUpToDate: () => void; + readonly workbench: Workbench; + readonly onToggleResults: () => void; +}): JSX.Element { + const [canLiveValidate] = userPreferences.use( + 'workBench', + 'general', + 'liveValidation' + ); + const [noUploadPlan, openNoUploadPlan, closeNoUploadPlan] = useBooleanState(); + const [showStatus, openStatus, closeStatus] = useBooleanState(); + const [operationAborted, openAbortedMessage, closeAbortedMessage] = + useBooleanState(); + const [operationCompleted, openOperationCompleted, closeOperationCompleted] = + useBooleanState(); + const { mode, refreshInitiatorAborted, ...actions } = useWbActions({ + datasetId: dataset.id, + triggerRefresh, + checkDeletedFail, + openStatus, + workbench, + }); + + const cells = workbench.cells; + const messages = { + validate: + cells.cellCounts?.invalidCells === 0 + ? { + header: wbText.validationNoErrors(), + message: ( + <> + {wbText.validationNoErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + } + : { + header: wbText.validationErrors(), + message: ( + <> + {wbText.validationErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + }, + upload: + cells.cellCounts?.invalidCells === 0 + ? { + header: wbText.uploadSuccessful(), + message: wbText.uploadSuccessfulDescription(), + } + : { + header: wbText.uploadErrors(), + message: ( + <> + {wbText.uploadErrorsDescription()} +
+
+ {wbText.uploadErrorsSecondDescription()} + + ), + }, + unupload: { + header: wbText.dataSetRollback(), + message: wbText.dataSetRollbackDescription(), + }, + }; + + React.useEffect(() => { + if ( + !isUploaded && + (mappings?.lines ?? []).length === 0 && + hasPermission('/workbench/dataset', 'upload') + ) { + openNoUploadPlan(); + } + }, []); + + return ( + <> + {noUploadPlan && ( + + {commonText.close()} + + {commonText.create()} + + + } + header={wbPlanText.noUploadPlan()} + onClose={closeNoUploadPlan} + > + {wbPlanText.noUploadPlanDescription()} + + )} + {!isUploaded && hasPermission('/workbench/dataset', 'validate') && ( + + )} + + {isUploaded && hasPermission('/workbench/dataset', 'unupload') && ( + + )} + {!isUploaded && hasPermission('/workbench/dataset', 'upload') && ( + + )} + {!isUploaded && hasPermission('/workbench/dataset', 'update') && ( + <> + + + + )} + {mode.current && showStatus && ( + { + refreshInitiatorAborted.current = wasAborted; + closeStatus(); + if (wasAborted) openAbortedMessage(); + else openOperationCompleted(); + triggerRefresh(); + }} + /> + )} + {operationCompleted && ( + + {cells.cellCounts?.invalidCells === 0 && + mode.current === 'upload' && ( + { + mode.current = undefined; + refreshInitiatorAborted.current = false; + closeOperationCompleted(); + }} + /> + )} + {commonText.close()} + + } + header={messages[mode.current!].header} + onClose={closeOperationCompleted} + > + {messages[mode.current!].message} + + )} + {operationAborted && ( + + {mode.current === 'validate' + ? wbText.validationCanceledDescription() + : mode.current === 'unupload' + ? wbText.rollbackCanceledDescription() + : wbText.uploadCanceledDescription()} + + )} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx new file mode 100644 index 00000000000..bbabcc05acc --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx @@ -0,0 +1,27 @@ +import React from 'react'; + +import { Button } from '../../Atoms/Button'; +import { wbText } from '../../../localization/workbench'; +import { commonText } from '../../../localization/common'; + +export function WbResults({ + hasUnSavedChanges, + onToggleResults: handleToggleResults, +}: { + readonly hasUnSavedChanges: boolean; + readonly onToggleResults: () => void; +}): JSX.Element { + return ( + <> + + {commonText.results()} + + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx new file mode 100644 index 00000000000..94ad349dc89 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx @@ -0,0 +1,54 @@ +import React from "react"; + +import { useBooleanState } from "../../../hooks/useBooleanState"; +import { Button } from "../../Atoms/Button"; +import { wbText } from "../../../localization/workbench"; +import { commonText } from "../../../localization/common"; +import { Dialog } from "../../Molecules/Dialog"; + +export function WbRevert({ + hasUnSavedChanges, + triggerRefresh, + spreadSheetUpToDate, +}: { + readonly hasUnSavedChanges: boolean; + readonly triggerRefresh: () => void; + readonly spreadSheetUpToDate: () => void; +}): JSX.Element { + const [showRevert, openRevert, closeRevert] = useBooleanState(); + + const handleRevert = () => { + triggerRefresh(); + closeRevert(); + spreadSheetUpToDate(); + }; + + return ( + <> + + {wbText.revert()} + + {showRevert && ( + + {commonText.cancel()} + + {wbText.revert()} + + + } + header={wbText.revertChanges()} + onClose={closeRevert} + > + {wbText.revertChangesDescription()} + + )} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx new file mode 100644 index 00000000000..acec7d07c2e --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx @@ -0,0 +1,39 @@ +import React from 'react'; + +import type { WbStatus } from '../WbView'; +import { useBooleanState } from '../../../hooks/useBooleanState'; +import { Button } from '../../Atoms/Button'; +import { wbText } from '../../../localization/workbench'; +import { RollbackConfirmation } from '../Components'; + +export function WbRollback({ + datasetId, + triggerStatusComponent, +}: { + readonly datasetId: number; + readonly triggerStatusComponent: (mode: WbStatus) => void; +}): JSX.Element { + const [rollback, handleOpen, handleClose] = useBooleanState(); + + const handleRollback = () => triggerStatusComponent('unupload'); + + return ( + <> + + {wbText.rollback()} + + {rollback && ( + + )} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx new file mode 100644 index 00000000000..1daf42fb336 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx @@ -0,0 +1,78 @@ +import React from 'react'; + +import { useBooleanState } from '../../../hooks/useBooleanState'; +import { Button } from '../../Atoms/Button'; +import { className } from '../../Atoms/className'; +import { Dialog } from '../../Molecules/Dialog'; +import { commonText } from '../../../localization/common'; +import { wbText } from '../../../localization/workbench'; +import { loadingBar } from '../../Molecules'; +import { Workbench } from '../WbView'; +import { overwriteReadOnly } from '../../../utils/types'; +import { ping } from '../../../utils/ajax/ping'; +import { Http } from '../../../utils/ajax/definitions'; + +export function WbSave({ + workbench, + hasUnSavedChanges, + checkDeletedFail, + spreadSheetUpToDate, +}: { + readonly workbench: Workbench; + readonly hasUnSavedChanges: boolean; + readonly checkDeletedFail: (statusCode: number) => void; + readonly spreadSheetUpToDate: () => void; +}): JSX.Element { + const [showProgressBar, openProgressBar, closeProgressBar] = + useBooleanState(); + + const handleSave = () => { + // Clear validation + overwriteReadOnly(workbench.dataset, 'rowresults', null); + workbench.validation.stopLiveValidation(); + // TODO: figure out what to do in updateValidationButton + workbench.validation.updateValidationButton(); + + // Show saving progress bar + openProgressBar(); + + // Send data + ping(`/api/workbench/rows/${workbench.dataset.id}/`, { + method: 'PUT', + body: workbench.data, + expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], + }) + .then((status) => checkDeletedFail(status)) + .then(() => { + spreadSheetUpToDate(); + workbench.cells.cellMeta = []; + // TODO: Figure out how to rework searchCells for SettingsChange as input + // workbench.utils?.searchCells({ key: 'SettingsChange' }); + workbench.hot.render(); + closeProgressBar(); + }); + }; + + return ( + <> + + {commonText.save()} + + {showProgressBar && ( + + {loadingBar} + + )} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx new file mode 100644 index 00000000000..078b4d34858 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx @@ -0,0 +1,66 @@ +import React from 'react'; + +import type { WbMapping } from '../mapping'; +import type { WbStatus } from '../WbView'; +import { useBooleanState } from '../../../hooks/useBooleanState'; +import { Button } from '../../Atoms/Button'; +import { Dialog } from '../../Molecules/Dialog'; +import { wbText } from '../../../localization/workbench'; +import { commonText } from '../../../localization/common'; + +export function WbUpload({ + hasUnSavedChanges, + mappings, + openNoUploadPlan, + startUpload, +}: { + readonly hasUnSavedChanges: boolean; + readonly mappings: WbMapping; + readonly openNoUploadPlan: () => void; + readonly startUpload: (mode: WbStatus) => void; +}): JSX.Element { + const [showUpload, openUpload, closeUpload] = useBooleanState(); + + const handleUpload = (): void => { + if ((mappings?.lines ?? []).length > 0) { + openUpload(); + } else { + openNoUploadPlan(); + } + }; + + const handleConfirmUpload = (): void => { + startUpload('upload'); + closeUpload(); + }; + + return ( + <> + + {wbText.upload()} + + {showUpload && ( + + {commonText.cancel()} + + {wbText.upload()} + + + } + header={wbText.startUpload()} + onClose={closeUpload} + > + {wbText.startUploadDescription()} + + )} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx new file mode 100644 index 00000000000..79bcb825ac9 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx @@ -0,0 +1,50 @@ +import React from 'react'; + +import type { WbStatus } from '../WbView'; +import { WbValidationReact } from '../WbValidation'; +import { Button } from '../../Atoms/Button'; +import { commonText } from '../../../localization/common'; +import { wbText } from '../../../localization/workbench'; + +export function WbValidate({ + canLiveValidate, + hasUnSavedChanges, + startUpload, + validation, +}: { + readonly canLiveValidate: boolean; + readonly hasUnSavedChanges: boolean; + readonly startUpload: (mode: WbStatus) => void; + readonly validation: WbValidationReact; +}): JSX.Element { + const handleValidate = () => startUpload('validate'); + const handleToggleDataCheck = () => validation.toggleDataCheck(); + + return ( + <> + + {validation.validationMode === 'live' + ? validation.liveValidationStack.length > 0 + ? commonText.countLine({ + resource: wbText.dataCheckOn(), + count: validation.liveValidationStack.length, + }) + : wbText.dataCheckOn() + : wbText.dataCheck()} + + + {wbText.validate()} + + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx deleted file mode 100644 index adf75f08aa5..00000000000 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbActions.tsx +++ /dev/null @@ -1,1091 +0,0 @@ -import React from 'react'; - -import { commonText } from '../../localization/common'; -import { wbPlanText } from '../../localization/wbPlan'; -import { wbText } from '../../localization/workbench'; -import { Http } from '../../utils/ajax/definitions'; -import { ping } from '../../utils/ajax/ping'; -import { overwriteReadOnly } from '../../utils/types'; -import { Button } from '../Atoms/Button'; -import { Link } from '../Atoms/Link'; -import { loadingBar } from '../Molecules'; -import { Dialog } from '../Molecules/Dialog'; -import type { Status } from '../WbPlanView/Wrapped'; -import type { WbCellCounts, WbCellMetaReact } from './CellMeta'; -import { RollbackConfirmation } from './Components'; -import { CreateRecordSetButton } from './RecordSet'; -import { WbStatus as WbStatusComponent } from './Status'; -import type { DialogHandlers, WbStatus, WbView, Workbench } from './WbView'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import type { WbMapping } from './mapping'; -import type { RA } from '../../utils/types'; -import { WbValidationReact } from './WbValidation'; -import Handsontable from 'handsontable'; -import { WbUtils } from './WbUtils'; -import { className } from '../Atoms/className'; -import { hasPermission } from '../Permissions/helpers'; -import { userPreferences } from '../Preferences/userPreferences'; -import { useBooleanState } from '../../hooks/useBooleanState'; - -export function useWbActions({ - datasetId, - triggerRefresh, - checkDeletedFail, - openStatus, - spreadSheetUpToDate, - workbench, -}: { - readonly datasetId: number; - readonly triggerRefresh: () => void; - readonly checkDeletedFail: (statusCode: number) => void; - readonly openStatus: () => void; - readonly spreadSheetUpToDate: () => void; - readonly workbench: Workbench; -}) { - const mode = React.useRef(undefined); - const refreshInitiatorAborted = React.useRef(false); - - const startUpload = (newMode: WbStatus): void => { - // this.validation.stopLiveValidation(); - // this.validation.updateValidationButton(); - ping(`/api/workbench/${newMode}/${datasetId}/`, { - method: 'POST', - expectedErrors: [Http.CONFLICT], - }) - .then((statusCode): void => { - checkDeletedFail(statusCode); - checkConflictFail(statusCode); - }) - .then(() => triggerStatusComponent(newMode)); - }; - - const changeMode = (newMode: WbStatus): void => { - mode.current = newMode; - }; - - const triggerStatusComponent = (newMode: WbStatus): void => { - changeMode(newMode); - openStatus(); - }; - - const checkConflictFail = (statusCode: number): boolean => { - if (statusCode === Http.CONFLICT) - /* - * Upload/Validation/Un-Upload has been initialized by another session - * Need to reload the page to display the new state - */ - triggerRefresh(); - return statusCode === Http.CONFLICT; - }; - - const save = () => { - // Clear validation - overwriteReadOnly(workbench.dataset, 'rowresults', null); - // this.validation.stopLiveValidation(); - // this.validation.updateValidationButton(); - - // Show saving progress bar - // this.saveProgressBar.open(); - - // Send data - ping(`/api/workbench/rows/${datasetId}/`, { - method: 'PUT', - body: workbench.data, - expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], - }) - .then((status) => checkDeletedFail(status)) - .then(() => { - spreadSheetUpToDate(); - workbench.cells!.cellMeta = []; - // utils.searchCells({ key: 'SettingsChange' }); - workbench.hot?.render(); - }); - // .finally(this.saveProgressBar.close); - }; - - return { - mode, - refreshInitiatorAborted, - startUpload, - triggerStatusComponent, - save, - }; -} - -export function WbActionsComponent({ - dataset, - hasUnSavedChanges, - isUploaded, - triggerRefresh, - mappings, - checkDeletedFail, - spreadSheetUpToDate, - workbench, - toggleResults -}: { - readonly dataset: Dataset; - readonly hasUnSavedChanges: boolean; - readonly isUploaded: boolean; - readonly triggerRefresh: () => void; - readonly mappings: WbMapping; - readonly checkDeletedFail: (statusCode: number) => void; - readonly spreadSheetUpToDate: () => void; - readonly workbench: Workbench; - readonly toggleResults: () => void; -}): JSX.Element { - const [canLiveValidate] = userPreferences.use( - 'workBench', - 'general', - 'liveValidation' - ); - const [noUploadPlan, openNoUploadPlan, closeNoUploadPlan] = useBooleanState(); - const [showStatus, openStatus, closeStatus] = useBooleanState(); - const [operationAborted, openAbortedMessage, closeAbortedMessage] = - useBooleanState(); - const [operationCompleted, openOperationCompleted, closeOperationCompleted] = - useBooleanState(); - const { mode, refreshInitiatorAborted, ...actions } = useWbActions({ - datasetId: dataset.id, - triggerRefresh, - checkDeletedFail, - openStatus, - spreadSheetUpToDate, - workbench, - }); - const cells = workbench.cells!; - const messages = { - validate: - cells.cellCounts?.invalidCells === 0 - ? { - header: wbText.validationNoErrors(), - message: ( - <> - {wbText.validationNoErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - } - : { - header: wbText.validationErrors(), - message: ( - <> - {wbText.validationErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - }, - upload: - cells.cellCounts?.invalidCells === 0 - ? { - header: wbText.uploadSuccessful(), - message: wbText.uploadSuccessfulDescription(), - } - : { - header: wbText.uploadErrors(), - message: ( - <> - {wbText.uploadErrorsDescription()} -
-
- {wbText.uploadErrorsSecondDescription()} - - ), - }, - unupload: { - header: wbText.dataSetRollback(), - message: wbText.dataSetRollbackDescription(), - }, - }; - React.useEffect(() => { - if ( - !isUploaded && - (mappings?.lines ?? []).length === 0 && - hasPermission('/workbench/dataset', 'upload') - ) { - openNoUploadPlan(); - } - }, []); - return ( - <> - {noUploadPlan && ( - - {commonText.close()} - - {commonText.create()} - - - } - header={wbPlanText.noUploadPlan()} - onClose={closeNoUploadPlan} - > - {wbPlanText.noUploadPlanDescription()} - - )} - {!isUploaded && hasPermission('/workbench/dataset', 'validate') && ( - - )} - - {isUploaded && hasPermission('/workbench/dataset', 'unupload') && ( - - )} - {!isUploaded && hasPermission('/workbench/dataset', 'upload') && ( - - )} - {!isUploaded && hasPermission('/workbench/dataset', 'update') && ( - <> - - - - )} - {mode.current && showStatus && ( - { - refreshInitiatorAborted.current = wasAborted; - closeStatus(); - if (wasAborted) openAbortedMessage(); - triggerRefresh(); - }} - /> - )} - {operationCompleted && ( - - {cells.cellCounts?.invalidCells === 0 && - mode.current === 'upload' && ( - { - mode.current = undefined; - refreshInitiatorAborted.current = false; - closeOperationCompleted(); - }} - /> - )} - {commonText.close()} - - } - header={messages[mode.current as WbStatus].header} - onClose={closeOperationCompleted} - > - {messages[mode.current as WbStatus].message} - - )} - {operationAborted && ( - - {mode.current === 'validate' - ? wbText.validationCanceledDescription() - : mode.current === 'unupload' - ? wbText.rollbackCanceledDescription() - : wbText.uploadCanceledDescription()} - - )} - - ); -} - -export function WbValidate({ - canLiveValidate, - hasUnSavedChanges, - startUpload, - validation -}: { - readonly canLiveValidate: boolean; - readonly hasUnSavedChanges: boolean; - readonly startUpload: (mode: WbStatus) => void; - readonly validation: WbValidationReact -}): JSX.Element { - const handleValidate = () => startUpload('validate'); - const [validateIsReady, setValidateIsReady] = React.useState(false); - const [liveValidationCount, setCount] = React.useState(validation.liveValidationStack.length); - React.useEffect(() => { - validation.setCount = setCount; - setValidateIsReady(true); - }, []) - return ( - <> - validation.toggleDataCheck()} - aria-pressed={validation.validationMode === "live"} - > - {validation.validationMode === 'live' - ? liveValidationCount > 0 - ? commonText.countLine({ - resource: wbText.dataCheckOn(), - count: liveValidationCount, - }) - : wbText.dataCheckOn() - : wbText.dataCheck()} - - - {wbText.validate()} - - - ); -} - -export function WbResults({ - hasUnSavedChanges, - toggleResults -}: { - readonly hasUnSavedChanges: boolean; - readonly toggleResults: () => void; -}): JSX.Element { - - return ( - <> - - {commonText.results()} - - - ); -} - -export function WbRollback({ - datasetId, - triggerStatusComponent, -}: { - readonly datasetId: number; - readonly triggerStatusComponent: (mode: WbStatus) => void; -}): JSX.Element { - const [rollback, handleOpen, handleClose] = useBooleanState(); - return ( - <> - - {wbText.rollback()} - - {rollback && ( - triggerStatusComponent('unupload')} - /> - )} - - ); -} - -export function WbUpload({ - hasUnSavedChanges, - mappings, - openNoUploadPlan, - startUpload, -}: { - readonly hasUnSavedChanges: boolean; - readonly mappings: WbMapping; - readonly openNoUploadPlan: () => void; - readonly startUpload: (mode: WbStatus) => void; -}): JSX.Element { - const [showUpload, openUpload, closeUpload] = useBooleanState(); - const handleUpload = (): void => { - if ((mappings?.lines ?? []).length > 0) { - openUpload(); - } else { - openNoUploadPlan(); - } - }; - return ( - <> - - {wbText.upload()} - - {showUpload && ( - - {commonText.cancel()} - { - startUpload('upload'); - closeUpload(); - }} - > - {wbText.upload()} - - - } - header={wbText.startUpload()} - onClose={closeUpload} - > - {wbText.startUploadDescription()} - - )} - - ); -} - -export function WbRevert({ - hasUnSavedChanges, - triggerRefresh, - spreadSheetUpToDate, -}: { - readonly hasUnSavedChanges: boolean; - readonly triggerRefresh: () => void; - readonly spreadSheetUpToDate: () => void; -}): JSX.Element { - const [showRevert, openRevert, closeRevert] = useBooleanState(); - const handleRevert = () => { - triggerRefresh(); - closeRevert(); - spreadSheetUpToDate(); - }; - return ( - <> - - {wbText.revert()} - - {showRevert && ( - - {commonText.cancel()} - - {wbText.revert()} - - - } - header={wbText.revertChanges()} - onClose={closeRevert} - > - {wbText.revertChangesDescription()} - - )} - - ); -} - -export function WbSave({ - hasUnSavedChanges, - save, -}: { - readonly hasUnSavedChanges: boolean; - readonly save: () => void; -}): JSX.Element { - const [showProgressBar, openProgressBar, closeProgressBar] = - useBooleanState(); - const handleSave = () => { - openProgressBar(); - save(); - closeProgressBar(); - }; - return ( - <> - - {commonText.save()} - - {showProgressBar && ( - - {loadingBar} - - )} - - ); -} - -/* eslint-disable functional/no-this-expression */ -export class WbActionsReact { - // eslint-disable-next-line functional/prefer-readonly-type - public hasUnSavedChanges: boolean = false; - - public constructor( - private readonly data: RA>, - private readonly dataset: Dataset, - private readonly mappings: WbMapping, - private readonly noUploadPlan: DialogHandlers, - private readonly uploadDialog: DialogHandlers, - private readonly statusComponent: DialogHandlers, - private readonly checkDeletedFail: (statusCode: number) => boolean, - private readonly triggerRefresh: () => void, - private readonly saveProgressBar: DialogHandlers, - private refreshInitiatedBy: React.MutableRefObject, - private refreshInitiatorAborted: React.MutableRefObject, - private readonly operationCompleted: DialogHandlers, - private readonly operationAborted: DialogHandlers, - private readonly opMsg: any, - private readonly validation: WbValidationReact, - private readonly cells: WbCellMetaReact, - private readonly hot: Handsontable, - private readonly utils: WbUtils // change - ) {} - - // BUG: disable the button if there is nothing to upload - upload(mode: WbStatus): void { - if ((this.mappings?.lines ?? []).length > 0) { - if (mode === 'upload') { - this.uploadDialog.open(); - } else this.startUpload(mode); - } else { - this.noUploadPlan.open(); - } - } - - startUpload(mode: WbStatus): void { - // this.validation.stopLiveValidation(); - // this.validation.updateValidationButton(); - ping(`/api/workbench/${mode}/${this.dataset.id}/`, { - method: 'POST', - expectedErrors: [Http.CONFLICT], - }) - .then((statusCode): void => { - this.checkDeletedFail(statusCode); - this.checkConflictFail(statusCode); - }) - .then(() => this.openStatus(mode)); - } - - openStatus(mode: WbStatus): void { - this.refreshInitiatedBy.current = mode; - this.statusComponent.open(); - } - - async save() { - // Clear validation - overwriteReadOnly(this.dataset, 'rowresults', null); - // this.validation.stopLiveValidation(); - // this.validation.updateValidationButton(); - - // Show saving progress bar - this.saveProgressBar.open(); - - // Send data - return ping(`/api/workbench/rows/${this.dataset.id}/`, { - method: 'PUT', - body: this.data, - expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], - }) - .then((status) => this.checkDeletedFail(status)) - .then(() => { - this.spreadSheetUpToDate(); - this.cells.cellMeta = []; - this.utils.searchCells({ key: 'SettingsChange' }); - this.hot?.render(); - }) - .finally(this.saveProgressBar.close); - } - - // Check if AJAX failed because Data Set was modified by other session - checkConflictFail(statusCode: number): boolean { - if (statusCode === Http.CONFLICT) - /* - * Upload/Validation/Un-Upload has been initialized by another session - * Need to reload the page to display the new state - */ - this.triggerRefresh(); - return statusCode === Http.CONFLICT; - } - - spreadSheetUpToDate(): void { - if (!this.hasUnSavedChanges) return; - this.hasUnSavedChanges = false; - } - - public operationCompletedMessage(cellCounts: WbCellCounts) { - if (this.refreshInitiatedBy.current === undefined) return; - - const messages = { - validate: - cellCounts.invalidCells === 0 - ? { - header: wbText.validationNoErrors(), - message: ( - <> - {wbText.validationNoErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - } - : { - header: wbText.validationErrors(), - message: ( - <> - {wbText.validationErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - }, - upload: - cellCounts.invalidCells === 0 - ? { - header: wbText.uploadSuccessful(), - message: wbText.uploadSuccessfulDescription(), - } - : { - header: wbText.uploadErrors(), - message: ( - <> - {wbText.uploadErrorsDescription()} -
-
- {wbText.uploadErrorsSecondDescription()} - - ), - }, - unupload: { - header: wbText.dataSetRollback(), - message: wbText.dataSetRollbackDescription(), - }, - }; - - // refactor to set this as a state or ref - const messageToShow = messages[this.refreshInitiatedBy.current]; - this.opMsg(messageToShow); - this.operationCompleted.open(); - } - - operationAbortedMessage(): void { - if ( - this.refreshInitiatedBy.current === undefined || - this.refreshInitiatorAborted.current - ) - return; - - this.operationAborted.open(); - - // // set this in onClose - // this.refreshInitiatedBy.current = undefined; - // this.refreshInitiatorAborted.current = false; - } - - spreadSheetChanged(): void { - if (this.hasUnSavedChanges) return; - this.hasUnSavedChanges = true; - } -} - -/* eslint-enable functional/no-this-expression */ - -/* eslint-disable functional/no-this-expression */ -export class WbActions { - // eslint-disable-next-line functional/prefer-readonly-type - private hasUnSavedChanges: boolean = false; - - // eslint-disable-next-line functional/prefer-readonly-type - public status: (() => void) | undefined = undefined; - - public constructor(private readonly wbView: WbView) {} - - /* - * Actions - * aka Rollback - */ - unupload(): void { - const dialog = this.wbView.options.display( - dialog()} - onRollback={() => this.openStatus('unupload')} - /> - ); - } - - // BUG: disable the button if there is nothing to upload - upload(event: MouseEvent): void { - const mode = - (event.currentTarget as HTMLElement | null)?.classList.contains( - 'wb-upload' - ) === true - ? 'upload' - : 'validate'; - if ((this.wbView.mappings?.lines ?? []).length > 0) { - if (mode === 'upload') { - const dialog = this.wbView.options.display( - - {commonText.cancel()} - { - this.startUpload(mode); - dialog(); - }} - > - {wbText.upload()} - - - } - header={wbText.startUpload()} - onClose={(): void => dialog()} - > - {wbText.startUploadDescription()} - - ); - } else this.startUpload(mode); - } else { - const dialog = this.wbView.options.display( - - {commonText.close()} - - {commonText.create()} - - - } - header={wbPlanText.noUploadPlan()} - onClose={() => dialog()} - > - {wbPlanText.noUploadPlanDescription()} - - ); - } - } - - startUpload(mode: WbStatus): void { - this.wbView.validation.stopLiveValidation(); - this.wbView.validation.updateValidationButton(); - ping(`/api/workbench/${mode}/${this.wbView.dataset.id}/`, { - method: 'POST', - expectedErrors: [Http.CONFLICT], - }) - .then((statusCode): void => { - this.wbView.checkDeletedFail(statusCode); - this.checkConflictFail(statusCode); - }) - .then(() => this.openStatus(mode)); - } - - openStatus(mode: WbStatus): void { - this.status = this.wbView.options.display( - { - this.status?.(); - this.status = undefined; - this.wbView.trigger('refresh', mode, wasAborted); - }} - /> - ); - } - - revertChanges(): void { - const dialog = this.wbView.options.display( - - {commonText.cancel()} - this.wbView.trigger('refresh')}> - {wbText.revert()} - - - } - header={wbText.revertChanges()} - onClose={() => dialog()} - > - {wbText.revertChangesDescription()} - - ); - } - - async save() { - // Clear validation - overwriteReadOnly(this.wbView.dataset, 'rowresults', null); - this.wbView.validation.stopLiveValidation(); - this.wbView.validation.updateValidationButton(); - - // Show saving progress bar - const dialog = this.wbView.options.display( - dialog()} - > - {loadingBar} - - ); - - // Send data - return ping(`/api/workbench/rows/${this.wbView.dataset.id}/`, { - method: 'PUT', - body: this.wbView.data, - expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], - }) - .then((status) => this.wbView.checkDeletedFail(status)) - .then(() => { - this.spreadSheetUpToDate(); - this.wbView.cells.cellMeta = []; - this.wbView.wbUtils.searchCells({ key: 'SettingsChange' }); - this.wbView.hot?.render(); - }) - .finally(() => dialog()); - } - - // Check if AJAX failed because Data Set was modified by other session - checkConflictFail(statusCode: number): boolean { - if (statusCode === Http.CONFLICT) - /* - * Upload/Validation/Un-Upload has been initialized by another session - * Need to reload the page to display the new state - */ - this.wbView.trigger('reload'); - return statusCode === Http.CONFLICT; - } - - spreadSheetUpToDate(): void { - if (!this.hasUnSavedChanges) return; - this.hasUnSavedChanges = false; - Array.from( - this.wbView.el.querySelectorAll( - '.wb-upload, .wb-validate, .wb-export-data-set, .wb-change-data-set-owner' - ), - (element) => { - (element as HTMLButtonElement).disabled = false; - element.setAttribute('title', ''); - } - ); - this.wbView.el.querySelector('.wb-save')?.toggleAttribute('disabled', true); - this.wbView.el - .querySelector('.wb-revert') - ?.toggleAttribute('disabled', true); - this.wbView.options.onSetUnloadProtect(false); - } - - public operationCompletedMessage(cellCounts: WbCellCounts): void { - if (this.wbView.refreshInitiatedBy === undefined) return; - - const messages = { - validate: - cellCounts.invalidCells === 0 - ? { - header: wbText.validationNoErrors(), - message: ( - <> - {wbText.validationNoErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - } - : { - header: wbText.validationErrors(), - message: ( - <> - {wbText.validationErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - }, - upload: - cellCounts.invalidCells === 0 - ? { - header: wbText.uploadSuccessful(), - message: wbText.uploadSuccessfulDescription(), - } - : { - header: wbText.uploadErrors(), - message: ( - <> - {wbText.uploadErrorsDescription()} -
-
- {wbText.uploadErrorsSecondDescription()} - - ), - }, - unupload: { - header: wbText.dataSetRollback(), - message: wbText.dataSetRollbackDescription(), - }, - }; - - const dialog = this.wbView.options.display( - - {cellCounts.invalidCells === 0 && - this.wbView.refreshInitiatedBy === 'upload' ? ( - dialog()} - /> - ) : undefined} - {commonText.close()} - - } - header={messages[this.wbView.refreshInitiatedBy].header} - onClose={() => dialog()} - > - {messages[this.wbView.refreshInitiatedBy].message} - - ); - - this.wbView.refreshInitiatedBy = undefined; - this.wbView.refreshInitiatorAborted = false; - } - - operationAbortedMessage(): void { - if ( - this.wbView.refreshInitiatedBy === undefined || - this.wbView.refreshInitiatorAborted - ) - return; - - const dialog = this.wbView.options.display( - dialog()} - > - {this.wbView.refreshInitiatedBy === 'validate' - ? wbText.validationCanceledDescription() - : this.wbView.refreshInitiatedBy === 'unupload' - ? wbText.rollbackCanceledDescription() - : wbText.uploadCanceledDescription()} - - ); - this.wbView.refreshInitiatedBy = undefined; - this.wbView.refreshInitiatorAborted = false; - } - - spreadSheetChanged(): void { - if (this.hasUnSavedChanges) return; - this.hasUnSavedChanges = true; - - Array.from( - this.wbView.el.querySelectorAll( - '.wb-upload, .wb-validate, .wb-export-data-set, .wb-change-data-set-owner' - ), - (element) => { - (element as HTMLButtonElement).disabled = true; - element.setAttribute('title', wbText.unavailableWhileEditing()); - } - ); - this.wbView.el - .querySelector('.wb-save') - ?.toggleAttribute('disabled', false); - this.wbView.el - .querySelector('.wb-revert') - ?.toggleAttribute('disabled', false); - const uploadView = this.wbView.el.querySelector('.wb-show-upload-view'); - uploadView?.toggleAttribute('disabled', true); - uploadView?.setAttribute('title', wbText.wbUploadedUnavailable()); - this.wbView.options.onSetUnloadProtect(true); - } -} - -/* eslint-enable functional/no-this-expression */ diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index d1e762b186c..c2b5bcefd75 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -48,7 +48,7 @@ import { parseWbMappings } from './mapping'; import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; import { wbViewTemplate } from './Template'; -import { WbActions, WbActionsComponent } from './WbActions'; +import { WbActionsComponent } from './Actions/WbActions'; import { WbUtils, WbUtilsComponent, WbUtilsReact } from './WbUtils'; import { WbValidation, WbValidationReact } from './WbValidation'; import { DataSetName } from './DataSetMeta'; @@ -62,15 +62,15 @@ export type WbStatus = 'unupload' | 'upload' | 'validate'; export type Workbench = { dataset: Dataset; - cells: WbCellMetaReact | undefined; - disambiguation: DisambiguationReact | undefined; - validation: WbValidationReact | undefined; + cells: WbCellMetaReact; + disambiguation: DisambiguationReact; + validation: WbValidationReact; data: RA>; searchPreferences: WbSearchPreferences; hot: Handsontable; throttleRate: number; mappings: WbMapping; - utils: WbUtilsReact | undefined; + utils: WbUtilsReact; setCellCounts: (cellCounts: WbCellCounts) => void; }; @@ -151,10 +151,10 @@ export function WbViewReact({ mappings: mappings as WbMapping, searchPreferences, throttleRate, - cells: undefined, - disambiguation: undefined, - validation: undefined, - utils: undefined, + cells: undefined!, + disambiguation: undefined!, + validation: undefined!, + utils: undefined!, setCellCounts, }; workbench.cells = new WbCellMetaReact(workbench); @@ -179,8 +179,7 @@ export function WbViewReact({ spreadSheetChanged, checkDeletedFail ); - const [showResults, openResults, closeResults, toggleResults] = - useBooleanState(); + const [showResults, _, closeResults, toggleResults] = useBooleanState(); // Makes the hot changes required for upload view results React.useEffect(() => { @@ -197,14 +196,12 @@ export function WbViewReact({ ).getHiddenColumns(); const rowsToInclude = new Set(); const colsToInclude = new Set(); - Object.entries(workbench.cells!.cellMeta).forEach( - ([physicalRow, rowMeta]) => - rowMeta.forEach((metaArray, physicalCol) => { - if (!workbench.cells!.getCellMetaFromArray(metaArray, 'isNew')) - return; - rowsToInclude.add((physicalRow as unknown as number) | 0); - colsToInclude.add(physicalCol); - }) + Object.entries(workbench.cells.cellMeta).forEach(([physicalRow, rowMeta]) => + rowMeta.forEach((metaArray, physicalCol) => { + if (!workbench.cells.getCellMetaFromArray(metaArray, 'isNew')) return; + rowsToInclude.add((physicalRow as unknown as number) | 0); + colsToInclude.add(physicalCol); + }) ); const rowsToHide = workbench.data .map((_, physicalRow) => physicalRow) @@ -226,7 +223,7 @@ export function WbViewReact({ getHotPlugin(workbench.hot, 'hiddenRows').hideRows(rowsToHide); getHotPlugin(workbench.hot, 'hiddenColumns').hideColumns(colsToHide); - workbench.utils!.toggleCellTypes( + workbench.utils.toggleCellTypes( 'newCells', 'remove', spreadsheetContainer.current @@ -236,18 +233,18 @@ export function WbViewReact({ // Calls validation results on load if dataset has been validated before React.useEffect(() => { - if (dataset.rowresults) { - workbench.validation?.getValidationResults(); + if (dataset.rowresults && hotIsReady) { + workbench.validation.getValidationResults(); if (workbench.validation?.validationMode === 'static' && !isUploaded) - workbench.utils?.toggleCellTypes( + workbench.utils.toggleCellTypes( 'invalidCells', 'remove', spreadsheetContainer.current ); - workbench.cells!.flushIndexedCellData = true; + workbench.cells.flushIndexedCellData = true; } - }, [dataset.rowresults]); + }, [dataset.rowresults, hotIsReady]); return ( <> @@ -289,7 +286,7 @@ export function WbViewReact({ checkDeletedFail={checkDeletedFail} spreadSheetUpToDate={spreadSheetUpToDate} workbench={workbench} - toggleResults={toggleResults} + onToggleResults={toggleResults} />
{showToolkit && ( @@ -310,9 +307,9 @@ export function WbViewReact({ hotRef={hotRef} isUploaded={isUploaded} data={data} - validation={workbench.validation!} - cells={workbench.cells!} - disambiguation={workbench.disambiguation!} + validation={workbench.validation} + cells={workbench.cells} + disambiguation={workbench.disambiguation} hooks={hooks} />
@@ -322,7 +319,7 @@ export function WbViewReact({ dataSetId={dataset.id} dataSetName={dataset.name} isUploaded={isUploaded} - recordCounts={workbench.validation!.uploadResults.recordCounts} + recordCounts={workbench.validation.uploadResults.recordCounts} onClose={closeResults} /> @@ -332,7 +329,7 @@ export function WbViewReact({ isUploaded={isUploaded} searchPreferences={searchPreferences} cellCounts={cellCounts} - utils={workbench.utils!} + utils={workbench.utils} spreadsheetContainer={spreadsheetContainer} /> From 0257f82efa8b5d5631a0b3245d6959a8ba55d811 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 2 Apr 2024 15:04:33 -0400 Subject: [PATCH 035/153] Clean up Action files - Rename props to follow naming conventions - Remove unnecessary function (changeMode) - Remove unused class names - Use ternary operator instead of && for conditional renders --- .../WorkBench/Actions/WbActions.tsx | 98 +++++++++---------- .../WorkBench/Actions/WbResults.tsx | 1 - .../components/WorkBench/Actions/WbRevert.tsx | 25 +++-- .../WorkBench/Actions/WbRollback.tsx | 5 +- .../components/WorkBench/Actions/WbSave.tsx | 11 +-- .../components/WorkBench/Actions/WbUpload.tsx | 5 +- .../WorkBench/Actions/WbValidate.tsx | 2 +- 7 files changed, 71 insertions(+), 76 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx index 0d1aa712963..662c37ae1cb 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx @@ -26,18 +26,18 @@ import { WbSave } from './WbSave'; export function useWbActions({ datasetId, - triggerRefresh, - checkDeletedFail, - openStatus, workbench, + checkDeletedFail, + onRefresh: handleRefresh, + onOpenStatus: handleOpenStatus, }: { readonly datasetId: number; - readonly triggerRefresh: () => void; - readonly checkDeletedFail: (statusCode: number) => void; - readonly openStatus: () => void; readonly workbench: Workbench; + readonly checkDeletedFail: (statusCode: number) => void; + readonly onRefresh: () => void; + readonly onOpenStatus: () => void; }) { - const mode = React.useRef(undefined); + const modeRef = React.useRef(undefined); const refreshInitiatorAborted = React.useRef(false); const startUpload = (newMode: WbStatus): void => { @@ -56,8 +56,8 @@ export function useWbActions({ }; const triggerStatusComponent = (newMode: WbStatus): void => { - mode.current = newMode; - openStatus(); + modeRef.current = newMode; + handleOpenStatus(); }; const checkConflictFail = (statusCode: number): boolean => { @@ -66,12 +66,12 @@ export function useWbActions({ * Upload/Validation/Un-Upload has been initialized by another session * Need to reload the page to display the new state */ - triggerRefresh(); + handleRefresh(); return statusCode === Http.CONFLICT; }; return { - mode, + modeRef, refreshInitiatorAborted, startUpload, triggerStatusComponent, @@ -82,21 +82,21 @@ export function WbActionsComponent({ dataset, hasUnSavedChanges, isUploaded, - triggerRefresh, + workbench, mappings, checkDeletedFail, - spreadSheetUpToDate, - workbench, + onRefresh: handleRefresh, + onSpreadsheetUpToDate: handleSpreadsheetUpToDate, onToggleResults, }: { readonly dataset: Dataset; readonly hasUnSavedChanges: boolean; readonly isUploaded: boolean; - readonly triggerRefresh: () => void; + readonly workbench: Workbench; readonly mappings: WbMapping; readonly checkDeletedFail: (statusCode: number) => void; - readonly spreadSheetUpToDate: () => void; - readonly workbench: Workbench; + readonly onRefresh: () => void; + readonly onSpreadsheetUpToDate: () => void; readonly onToggleResults: () => void; }): JSX.Element { const [canLiveValidate] = userPreferences.use( @@ -110,11 +110,11 @@ export function WbActionsComponent({ useBooleanState(); const [operationCompleted, openOperationCompleted, closeOperationCompleted] = useBooleanState(); - const { mode, refreshInitiatorAborted, ...actions } = useWbActions({ + const { modeRef, refreshInitiatorAborted, ...actions } = useWbActions({ datasetId: dataset.id, - triggerRefresh, + onRefresh: handleRefresh, checkDeletedFail, - openStatus, + onOpenStatus: openStatus, workbench, }); @@ -179,7 +179,7 @@ export function WbActionsComponent({ return ( <> - {noUploadPlan && ( + {noUploadPlan ? ( @@ -194,49 +194,49 @@ export function WbActionsComponent({ > {wbPlanText.noUploadPlanDescription()} - )} - {!isUploaded && hasPermission('/workbench/dataset', 'validate') && ( + ) : undefined} + {!isUploaded && hasPermission('/workbench/dataset', 'validate') ? ( - )} + ) : undefined} - {isUploaded && hasPermission('/workbench/dataset', 'unupload') && ( + {isUploaded && hasPermission('/workbench/dataset', 'unupload') ? ( - )} - {!isUploaded && hasPermission('/workbench/dataset', 'upload') && ( + ) : undefined} + {!isUploaded && hasPermission('/workbench/dataset', 'upload') ? ( - )} - {!isUploaded && hasPermission('/workbench/dataset', 'update') && ( + ) : undefined} + {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( <> - )} - {mode.current && showStatus && ( + ) : undefined} + {modeRef.current && showStatus ? ( - )} - {operationCompleted && ( + ) : undefined} + {operationCompleted ? ( {cells.cellCounts?.invalidCells === 0 && - mode.current === 'upload' && ( + modeRef.current === 'upload' && ( { - mode.current = undefined; + modeRef.current = undefined; refreshInitiatorAborted.current = false; closeOperationCompleted(); }} @@ -285,31 +285,31 @@ export function WbActionsComponent({ {commonText.close()} } - header={messages[mode.current!].header} + header={messages[modeRef.current!].header} onClose={closeOperationCompleted} > - {messages[mode.current!].message} + {messages[modeRef.current!].message} - )} - {operationAborted && ( + ) : undefined} + {operationAborted ? ( - {mode.current === 'validate' + {modeRef.current === 'validate' ? wbText.validationCanceledDescription() - : mode.current === 'unupload' + : modeRef.current === 'unupload' ? wbText.rollbackCanceledDescription() : wbText.uploadCanceledDescription()} - )} + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx index bbabcc05acc..0f2a928d002 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx @@ -15,7 +15,6 @@ export function WbResults({ <> void; - readonly spreadSheetUpToDate: () => void; + readonly onRefresh: () => void; + readonly onSpreadsheetUpToDate: () => void; }): JSX.Element { const [showRevert, openRevert, closeRevert] = useBooleanState(); const handleRevert = () => { - triggerRefresh(); + handleRefresh(); closeRevert(); - spreadSheetUpToDate(); + handleSpreadsheetUpToDate(); }; return ( <> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx index acec7d07c2e..829f4e6555e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx @@ -22,18 +22,17 @@ export function WbRollback({ {wbText.rollback()} - {rollback && ( + {rollback ? ( - )} + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx index 1daf42fb336..fba9e2ca41a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx @@ -16,12 +16,12 @@ export function WbSave({ workbench, hasUnSavedChanges, checkDeletedFail, - spreadSheetUpToDate, + onSpreadsheetUpToDate: handleSpreadsheetUpToDate, }: { readonly workbench: Workbench; readonly hasUnSavedChanges: boolean; readonly checkDeletedFail: (statusCode: number) => void; - readonly spreadSheetUpToDate: () => void; + readonly onSpreadsheetUpToDate: () => void; }): JSX.Element { const [showProgressBar, openProgressBar, closeProgressBar] = useBooleanState(); @@ -44,7 +44,7 @@ export function WbSave({ }) .then((status) => checkDeletedFail(status)) .then(() => { - spreadSheetUpToDate(); + handleSpreadsheetUpToDate(); workbench.cells.cellMeta = []; // TODO: Figure out how to rework searchCells for SettingsChange as input // workbench.utils?.searchCells({ key: 'SettingsChange' }); @@ -57,14 +57,13 @@ export function WbSave({ <> {commonText.save()} - {showProgressBar && ( + {showProgressBar ? ( {loadingBar} - )} + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx index 078b4d34858..f8543c26305 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx @@ -38,14 +38,13 @@ export function WbUpload({ <> {wbText.upload()} - {showUpload && ( + {showUpload ? ( @@ -60,7 +59,7 @@ export function WbUpload({ > {wbText.startUploadDescription()} - )} + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx index 79bcb825ac9..359e75656d0 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx @@ -23,7 +23,7 @@ export function WbValidate({ return ( <> From ccb7ddef8753ea8166a3687d664ee58f7ace3fd8 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 2 Apr 2024 16:31:58 -0400 Subject: [PATCH 036/153] Update prop names in WbView --- .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index c2b5bcefd75..f4ad02bd51b 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -281,10 +281,10 @@ export function WbViewReact({ dataset={dataset} hasUnSavedChanges={hasUnSavedChanges} isUploaded={isUploaded} - triggerRefresh={triggerRefresh} + onRefresh={triggerRefresh} mappings={mappings as WbMapping} checkDeletedFail={checkDeletedFail} - spreadSheetUpToDate={spreadSheetUpToDate} + onSpreadsheetUpToDate={spreadSheetUpToDate} workbench={workbench} onToggleResults={toggleResults} /> From bde9eb9e87008a09709f3989f42674cb4ce07e94 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 3 Apr 2024 09:50:40 -0400 Subject: [PATCH 037/153] Improve hot typings --- .../WorkBench/Toolkit/GeoLocate.tsx | 3 +- .../WorkBench/Toolkit/WbLeafletMap.tsx | 4 +- .../WorkBench/Toolkit/WbToolkit.tsx | 15 +- .../components/WorkBench/WbSpreadsheet.tsx | 142 ++++++++++-------- .../lib/components/WorkBench/WbUtils.tsx | 5 - .../lib/components/WorkBench/WbView.tsx | 29 ++-- .../js_src/lib/components/WorkBench/hooks.ts | 2 +- .../js_src/lib/components/WorkBench/index.tsx | 4 +- 8 files changed, 111 insertions(+), 93 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx index 99ede0de483..a5d661fddf5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx @@ -149,7 +149,7 @@ function GeoLocate({ // Generate Locality iterator. Able to handle multiple localities in a row export function getSelectedLocalities( - hot: Handsontable, + hot: Handsontable | null | undefined, columns: RA, localityColumns: RA>, // If false, treat single cell selection as entire spreadsheet selection @@ -167,6 +167,7 @@ export function getSelectedLocalities( }; } | undefined { + if (!hot) return undefined; const selectedRegions = getSelectedRegions(hot); const selectedVirtualColumns = f.unique( diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx index 6cb548b87b0..bd1f866c60e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx @@ -19,7 +19,7 @@ export function WbLeafletMap({ mappings, }: { readonly hasLocality: boolean; - readonly hot: Handsontable; + readonly hot: Handsontable | null | undefined; readonly dataset: Dataset; readonly mappings: WbMapping; }): JSX.Element { @@ -37,7 +37,7 @@ export function WbLeafletMap({ return getLocalitiesDataFromSpreadsheet( mappings.localityColumns, selection.visualRows.map((visualRow) => hot!.getDataAtRow(visualRow)), - getVisualHeaders(hot, dataset.columns), + getVisualHeaders(hot!, dataset.columns), selection.visualRows ); }, [mappings.localityColumns]); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx index 786b5f9dc30..85470d27dbb 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { HotTable } from '@handsontable/react'; import { hasPermission, hasTablePermission } from '../../Permissions/helpers'; import { WbChangeOwner } from './ChangeOwner'; @@ -23,18 +24,20 @@ export function WbToolkit({ data, handleDatasetDelete, hasUnSavedChanges, - triggerRefresh + triggerRefresh, }: { readonly dataset: Dataset; - readonly hotRef: any; + readonly hotRef: React.RefObject; readonly mappings: WbMapping; readonly data: RA>; readonly handleDatasetDelete: () => void; readonly hasUnSavedChanges: boolean; readonly triggerRefresh: () => void; }): JSX.Element { - const hot = hotRef.current.hotInstance; - + const hot = React.useMemo( + () => hotRef.current?.hotInstance, + [hotRef.current] + ); const handleExport = React.useCallback((): void => { const delimiter = userPreferences.get( 'workBench', @@ -86,12 +89,12 @@ export function WbToolkit({ dataset={dataset} data={data} mappings={mappings} - hot={hot} + hot={hot!} hasLocality={hasLocality} /> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index c80b49e33ab..9702e889e7a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -11,6 +11,7 @@ import { HotTable } from '@handsontable/react'; import Handsontable from 'handsontable'; import React from 'react'; import type { Settings } from 'handsontable/plugins/contextMenu'; +import type { Events } from 'handsontable/pluginHooks'; import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; @@ -21,7 +22,7 @@ import { getTable } from '../DataModel/tables'; import { hasPermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { parseWbMappings } from './mapping'; +import { parseWbMappings, WbMapping } from './mapping'; import { getSelectedRegions } from './hotHelpers'; import { WbValidationReact } from './WbValidation'; import { WbCellMetaReact } from './CellMeta'; @@ -42,6 +43,46 @@ import { DisambiguationDialog } from './Disambiguation'; import { mappingPathToString } from '../WbPlanView/mappingHelpers'; import { MappingPath } from '../WbPlanView/Mapper'; +// Context menu item definitions (common for fillUp and fillDown) +const fillCellsContextMenuItem = ( + hotRef: React.RefObject, + mode: 'down' | 'up' +): Handsontable.plugins.ContextMenu.MenuItemConfig => { + return { + name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), + disabled: () => + // typeof this.wbView.uploadedView === 'function' || + // typeof this.wbView.coordinateConverterView === 'function' || + !hasPermission('/workbench/dataset', 'update') || + (hotRef.current + ?.hotInstance!?.getSelected() + ?.every((selection) => selection[0] === selection[2]) ?? + false), + callback: (_: any, selections: any) => + selections.forEach((selection: any) => + Array.from( + new Array(selection.end.col + 1 - selection.start.col).keys() + ).forEach((index) => { + const startRow = + mode === 'up' ? selection.start.row + 1 : selection.start.row; + const endRow = selection.end.row; + const col = selection.start.col + index; + const value = + mode === 'up' + ? hotRef.current?.hotInstance!.getDataAtCell(endRow, col) + : hotRef.current?.hotInstance!.getDataAtCell(startRow, col); + hotRef.current?.hotInstance!.setDataAtCell( + Array.from({ length: endRow - startRow }, (_, index) => [ + startRow + index + 1, + col, + value, + ]) + ); + }) + ), + }; +}; + export function WbSpreadsheet({ dataset, hotRef, @@ -51,23 +92,24 @@ export function WbSpreadsheet({ cells, disambiguation, hooks, + mappings, }: { readonly dataset: Dataset; - readonly hotRef: any; + readonly hotRef: React.RefObject; readonly isUploaded: boolean; readonly data: RA>; readonly validation: WbValidationReact; readonly cells: WbCellMetaReact; readonly disambiguation: DisambiguationReact; - readonly hooks: any; + readonly hooks: Partial; + readonly mappings: WbMapping; }): JSX.Element { - const mappings = parseWbMappings(dataset); const physicalColToMappingCol = (physicalCol: number): number | undefined => mappings?.lines.findIndex( ({ headerName }) => headerName === dataset.columns[physicalCol] ); - const [disambiguationMatches, setMatches] = React.useState<{ + const [disambiguationMatches, setDisambiguationMatches] = React.useState<{ readonly physicalCols: RA; readonly mappingPath: MappingPath; readonly ids: RA; @@ -76,17 +118,23 @@ export function WbSpreadsheet({ const [disambiguationPhysicalRow, setPhysicalRow] = React.useState(); const [disambiguationResource, setResource] = React.useState>(); - const [noDisambiguationDialog, openNoDisamb, closeNoDisamb] = - useBooleanState(); + const [ + noDisambiguationDialog, + openNoDisambiguationDialog, + closeNoDisambiguationDialog, + ] = useBooleanState(); const [disambiguationDialog, openDisambiguation, closeDisambiguation] = useBooleanState(); const openDisambiguationDialog = () => { if (mappings === undefined || !hotRef.current) return; - const [visualRow, visualCol] = getSelectedLast(hotRef.current.hotInstance); - const physicalRow = hotRef.current.hotInstance.toPhysicalRow(visualRow); - const physicalCol = hotRef.current.hotInstance.toPhysicalColumn(visualCol); + const [visualRow, visualCol] = getSelectedLast( + hotRef.current?.hotInstance! + ); + const physicalRow = hotRef.current?.hotInstance!.toPhysicalRow(visualRow); + const physicalCol = + hotRef.current?.hotInstance!.toPhysicalColumn(visualCol); const matches = validation?.uploadResults.ambiguousMatches[ physicalRow @@ -106,7 +154,7 @@ export function WbSpreadsheet({ : Promise.resolve(resources) ).then(({ models }) => { if (models.length === 0) { - openNoDisamb(); + openNoDisambiguationDialog(); return; } @@ -135,52 +183,13 @@ export function WbSpreadsheet({ * // onClose: globalThis.clearInterval(interval); */ - setMatches(matches); + setDisambiguationMatches(matches); setResource(resources); setPhysicalRow(physicalRow); openDisambiguation(); }); }; - // Context menu item definitions (common for fillUp and fillDown) - const fillCellsContextMenuItem = ( - mode: 'down' | 'up' - ): Handsontable.plugins.ContextMenu.MenuItemConfig => { - return { - name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), - disabled: () => - // typeof this.wbView.uploadedView === 'function' || - // typeof this.wbView.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update') || - ((hotRef.current.hotInstance as Handsontable) - ?.getSelected() - ?.every((selection) => selection[0] === selection[2]) ?? - false), - callback: (_: any, selections: any) => - selections.forEach((selection: any) => - Array.from( - new Array(selection.end.col + 1 - selection.start.col).keys() - ).forEach((index) => { - const startRow = - mode === 'up' ? selection.start.row + 1 : selection.start.row; - const endRow = selection.end.row; - const col = selection.start.col + index; - const value = - mode === 'up' - ? hotRef.current.hotInstance!.getDataAtCell(endRow, col) - : hotRef.current.hotInstance!.getDataAtCell(startRow, col); - hotRef.current.hotInstance?.setDataAtCell( - Array.from({ length: endRow - startRow }, (_, index) => [ - startRow + index + 1, - col, - value, - ]) - ); - }) - ), - }; - }; - const contextMenuConfig = { items: ensure< IR< @@ -221,7 +230,7 @@ export function WbSpreadsheet({ wrapper.setAttribute( 'class', `${wrapper.getAttribute('class')} flex flex-col !m-0 - pb-1 wb-uploaded-view-context-menu` + pb-1` ); wrapper.innerHTML = createdRecords .map(([tableName, recordId, label]) => { @@ -254,12 +263,14 @@ export function WbSpreadsheet({ : ({ row_above: { disabled: () => + // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || // typeof this.coordinateConverterView === 'function' || !hasPermission('/workbench/dataset', 'update'), }, row_below: { disabled: () => + // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || // typeof this.coordinateConverterView === 'function' || !hasPermission('/workbench/dataset', 'update'), @@ -267,6 +278,7 @@ export function WbSpreadsheet({ remove_row: { disabled: () => { if ( + // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || // typeof this.coordinateConverterView === 'function' || !hasPermission('/workbench/dataset', 'update') @@ -274,7 +286,7 @@ export function WbSpreadsheet({ return true; // Or if called on the last row const selectedRegions = getSelectedRegions( - hotRef.current.hotInstance + hotRef.current?.hotInstance! ); return ( selectedRegions.length === 1 && @@ -286,6 +298,7 @@ export function WbSpreadsheet({ disambiguate: { name: wbText.disambiguate(), disabled: (): boolean => + // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || // typeof this.coordinateConverterView === 'function' || !disambiguation.isAmbiguousCell() || @@ -293,19 +306,21 @@ export function WbSpreadsheet({ callback: () => openDisambiguationDialog(), }, separator_1: '---------', - fill_down: fillCellsContextMenuItem('down'), - fill_up: fillCellsContextMenuItem('up'), + fill_down: fillCellsContextMenuItem(hotRef, 'down'), + fill_up: fillCellsContextMenuItem(hotRef, 'up'), separator_2: '---------', undo: { disabled: () => + // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || - hotRef.current.hotInstance.isUndoAvailable() !== true || + hotRef.current?.hotInstance!.isUndoAvailable() !== true || !hasPermission('/workbench/dataset', 'update'), }, redo: { disabled: () => + // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || - hotRef.current.hotInstance.isRedoAvailable() !== true || + hotRef.current?.hotInstance!.isRedoAvailable() !== true || !hasPermission('/workbench/dataset', 'update'), }, } as const) @@ -377,14 +392,14 @@ export function WbSpreadsheet({ // Hide the disambiguation column columns: [dataset.columns.length], indicators: false, - // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error // Typing doesn't match for handsontable 12.1.0, fixed in 14 // copyPasteEnabled: false, }} hiddenRows={{ rows: [], indicators: false, - // Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error // Typing doesn't match for handsontable 12.1.0, fixed in 14 // copyPasteEnabled: false, }} @@ -403,6 +418,7 @@ export function WbSpreadsheet({ placeholderCellClassName="htPlaceholder" ref={hotRef} rowHeaders={true} + // @ts-expect-error sortIndicator={true} stretchH="all" tabMoves={ @@ -419,7 +435,7 @@ export function WbSpreadsheet({ {wbText.noDisambiguationResultsDescription()} @@ -435,18 +451,18 @@ export function WbSpreadsheet({ selected.id ); validation?.startValidateRow(disambiguationPhysicalRow!); - hotRef.current.hotInstance?.render(); + hotRef.current?.hotInstance!.render(); }} onSelectedAll={(selected): void => // Loop backwards so the live validation will go from top to bottom - hotRef.current.hotInstance?.batch(() => { + hotRef.current!.hotInstance!.batch(() => { for ( let visualRow = data.length - 1; visualRow >= 0; visualRow-- ) { const physicalRow = - hotRef.current.hotInstance!.toPhysicalRow(visualRow); + hotRef.current!.hotInstance!.toPhysicalRow(visualRow); if ( !validation?.uploadResults.ambiguousMatches[ physicalRow diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 8f5ca61f541..c9e235fa7ab 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -400,7 +400,6 @@ export class WbUtilsReact { spreadsheetContainer: any ){ if (!this.workbench.hot) return; - console.log("here? MATE") /* * Don't rerun search on live search if search query did not change * (e.x, if Ctrl/Cmd+A is clicked in the search box) @@ -468,7 +467,6 @@ export class WbUtilsReact { lastVisibleColumn >= visualCol ) { cell = this.workbench.hot.getCell(visualRow, visualCol) ?? undefined; - console.log("CELL", cell) render = Boolean(cell); } @@ -488,7 +486,6 @@ export class WbUtilsReact { } // navigationTotalElement.textContent = resultsCount.toString(); - console.log("COUNT", resultsCount.toString()); // Navigate to the first search result when hitting Enter if (event.key === 'Enter') @@ -498,8 +495,6 @@ export class WbUtilsReact { event.key === 'Enter', event.key === 'Enter' ? [0, 0] : undefined ); - - console.log("here? MATE FINISHED", this.workbench.cells!.cellMeta) } parseSearchQuery(searchQueryElement: HTMLInputElement) { diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index f4ad02bd51b..ee765198d84 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -12,8 +12,9 @@ import '../../../css/workbench.css'; -import Handsontable from 'handsontable'; import React from 'react'; +import Handsontable from 'handsontable'; +import { HotTable } from '@handsontable/react'; import { commonText } from '../../localization/common'; import { LANGUAGE } from '../../localization/utils/config'; @@ -82,7 +83,7 @@ export function WbViewReact({ spreadsheetContainer, }: { readonly dataset: Dataset; - readonly hotRef: any; + readonly hotRef: React.RefObject; readonly handleDatasetDelete: () => void; readonly triggerRefresh: () => void; readonly spreadsheetContainer: any; @@ -112,6 +113,7 @@ export function WbViewReact({ useBooleanState(); const [hotIsReady, setHotToReady] = useBooleanState(); + // TODO: try putting a loader until hotRef loads? React.useEffect(() => { const getPickLists = async () => mappings === undefined @@ -126,7 +128,7 @@ export function WbViewReact({ const configureHotAndPickLists = async () => { const pickLists = await getPickLists(); // Await getPickLists() here configureHandsontable( - hotRef.current.hotInstance, + hotRef.current!.hotInstance!, mappings, dataset, pickLists @@ -147,7 +149,7 @@ export function WbViewReact({ const workbench: Workbench = { data, dataset, - hot: hotRef.current?.hotInstance, + hot: hotRef.current?.hotInstance!, mappings: mappings as WbMapping, searchPreferences, throttleRate, @@ -183,9 +185,9 @@ export function WbViewReact({ // Makes the hot changes required for upload view results React.useEffect(() => { - if (!showResults) return; + if (!showResults || !workbench.hot) return; - workbench.hot.updateSettings({ readOnly: true }); + workbench.hot?.updateSettings({ readOnly: true }); const initialHiddenRows = getHotPlugin( workbench.hot, 'hiddenRows' @@ -258,8 +260,8 @@ export function WbViewReact({ getRowCount={() => hotRef.current?.hotInstance ? dataset.rows.length - : hotRef.current.hotInstance.countRows() - - hotRef.current.hotInstance.countEmptyRows(true) + : hotRef.current!.hotInstance!.countRows() - + hotRef.current!.hotInstance!.countEmptyRows(true) } />
@@ -289,17 +291,17 @@ export function WbViewReact({ onToggleResults={toggleResults} />
- {showToolkit && ( + {showToolkit ? ( - )} + ) : undefined}
- {showResults && ( + {showResults ? ( - )} + ) : undefined}
number | undefined, spreadSheetChanged: () => void, checkDeletedFail: (statusCode: number) => boolean, -) { +) : Partial { let sortConfigIsSet: boolean = false; let hotCommentsContainerRepositionTimeout: | ReturnType diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 0df4d424095..9d4c6a36dc5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import { HotTable } from '@handsontable/react'; import { useAsyncState } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; @@ -18,7 +19,6 @@ import { NotFoundView } from '../Router/NotFoundView'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbViewReact } from './WbView'; - const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); const fetchDataSet = async ( @@ -54,7 +54,7 @@ export function WorkBenchReact(): JSX.Element | null { const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); const navigate = useNavigate(); - const hotRef = React.useRef(null); + const hotRef = React.useRef(null); const spreadsheetContainer = React.useRef(null); // temporary null check, replace with loading screen? From 78e23f5f3604942c39ee612467d9563fd5a71e93 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:10:03 -0400 Subject: [PATCH 038/153] Remove code smells - Remove console.logs - Make variable names more consistent --- .../WorkBench/Actions/WbActions.tsx | 14 ++-- .../WorkBench/Actions/WbResults.tsx | 9 +-- .../components/WorkBench/Actions/WbRevert.tsx | 6 +- .../components/WorkBench/Actions/WbSave.tsx | 6 +- .../components/WorkBench/Actions/WbUpload.tsx | 8 +-- .../WorkBench/Actions/WbValidate.tsx | 8 +-- .../WorkBench/Toolkit/ChangeOwner.tsx | 8 +-- .../WorkBench/Toolkit/DevShowPlan.tsx | 1 - .../WorkBench/Toolkit/WbToolkit.tsx | 13 ++-- .../components/WorkBench/WbSpreadsheet.tsx | 8 ++- .../lib/components/WorkBench/WbUtils.tsx | 27 +++---- .../lib/components/WorkBench/WbView.tsx | 43 ++++++----- .../js_src/lib/components/WorkBench/hooks.ts | 71 +++++++++---------- .../js_src/lib/utils/cache/definitions.ts | 6 +- 14 files changed, 117 insertions(+), 111 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx index 662c37ae1cb..499fe8e5fab 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx @@ -80,7 +80,7 @@ export function useWbActions({ export function WbActionsComponent({ dataset, - hasUnSavedChanges, + hasUnsavedChanges, isUploaded, workbench, mappings, @@ -90,7 +90,7 @@ export function WbActionsComponent({ onToggleResults, }: { readonly dataset: Dataset; - readonly hasUnSavedChanges: boolean; + readonly hasUnsavedChanges: boolean; readonly isUploaded: boolean; readonly workbench: Workbench; readonly mappings: WbMapping; @@ -197,14 +197,14 @@ export function WbActionsComponent({ ) : undefined} {!isUploaded && hasPermission('/workbench/dataset', 'validate') ? ( ) : undefined} {isUploaded && hasPermission('/workbench/dataset', 'unupload') ? ( @@ -215,7 +215,7 @@ export function WbActionsComponent({ ) : undefined} {!isUploaded && hasPermission('/workbench/dataset', 'upload') ? ( void; }): JSX.Element { + return ( <> {commonText.results()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx index 367f9aabf2d..78fe58a1444 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx @@ -7,11 +7,11 @@ import { commonText } from '../../../localization/common'; import { Dialog } from '../../Molecules/Dialog'; export function WbRevert({ - hasUnSavedChanges, + hasUnsavedChanges, onRefresh: handleRefresh, onSpreadsheetUpToDate: handleSpreadsheetUpToDate, }: { - readonly hasUnSavedChanges: boolean; + readonly hasUnsavedChanges: boolean; readonly onRefresh: () => void; readonly onSpreadsheetUpToDate: () => void; }): JSX.Element { @@ -28,7 +28,7 @@ export function WbRevert({ {wbText.revert()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx index fba9e2ca41a..7d350ecd9b6 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx @@ -14,12 +14,12 @@ import { Http } from '../../../utils/ajax/definitions'; export function WbSave({ workbench, - hasUnSavedChanges, + hasUnsavedChanges, checkDeletedFail, onSpreadsheetUpToDate: handleSpreadsheetUpToDate, }: { readonly workbench: Workbench; - readonly hasUnSavedChanges: boolean; + readonly hasUnsavedChanges: boolean; readonly checkDeletedFail: (statusCode: number) => void; readonly onSpreadsheetUpToDate: () => void; }): JSX.Element { @@ -59,7 +59,7 @@ export function WbSave({ aria-haspopup="dialog" variant={className.saveButton} onClick={handleSave} - disabled={!hasUnSavedChanges} + disabled={!hasUnsavedChanges} > {commonText.save()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx index f8543c26305..cde7b8bdb81 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx @@ -9,12 +9,12 @@ import { wbText } from '../../../localization/workbench'; import { commonText } from '../../../localization/common'; export function WbUpload({ - hasUnSavedChanges, + hasUnsavedChanges, mappings, openNoUploadPlan, startUpload, }: { - readonly hasUnSavedChanges: boolean; + readonly hasUnsavedChanges: boolean; readonly mappings: WbMapping; readonly openNoUploadPlan: () => void; readonly startUpload: (mode: WbStatus) => void; @@ -39,8 +39,8 @@ export function WbUpload({ {wbText.upload()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx index 359e75656d0..46091e6f234 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx @@ -8,12 +8,12 @@ import { wbText } from '../../../localization/workbench'; export function WbValidate({ canLiveValidate, - hasUnSavedChanges, + hasUnsavedChanges, startUpload, validation, }: { readonly canLiveValidate: boolean; - readonly hasUnSavedChanges: boolean; + readonly hasUnsavedChanges: boolean; readonly startUpload: (mode: WbStatus) => void; readonly validation: WbValidationReact; }): JSX.Element { @@ -40,8 +40,8 @@ export function WbValidate({ aria-haspopup="dialog" className="wb-validate" onClick={handleValidate} - disabled={hasUnSavedChanges} - title={hasUnSavedChanges ? wbText.unavailableWhileEditing() : ''} + disabled={hasUnsavedChanges} + title={hasUnsavedChanges ? wbText.unavailableWhileEditing() : ''} > {wbText.validate()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx index c1539b10535..0138acd2d51 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx @@ -21,10 +21,10 @@ import { commonText } from '../../../localization/common'; import { wbText } from "../../../localization/workbench"; export function WbChangeOwner({ - hasUnSavedChanges, + hasUnsavedChanges, dataset, }: { - readonly hasUnSavedChanges: boolean; + readonly hasUnsavedChanges: boolean; readonly dataset: Dataset; }): JSX.Element { const [showChangeOwner, openChangeOwner, closeChangeOwner] = @@ -36,8 +36,8 @@ export function WbChangeOwner({ aria-pressed={showChangeOwner} className="wb-change-data-set-owner" onClick={openChangeOwner} - disabled={hasUnSavedChanges} - title={hasUnSavedChanges ? wbText.unavailableWhileEditing() : ''} + disabled={hasUnsavedChanges} + title={hasUnsavedChanges ? wbText.unavailableWhileEditing() : ''} > {wbText.changeOwner()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx index 0e2c31c2818..f33cef39f60 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx @@ -31,7 +31,6 @@ export function WbDevPlan({ {wbText.uploadPlan()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx index 85470d27dbb..0c64543a1f3 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx @@ -23,7 +23,7 @@ export function WbToolkit({ mappings, data, handleDatasetDelete, - hasUnSavedChanges, + hasUnsavedChanges, triggerRefresh, }: { readonly dataset: Dataset; @@ -31,7 +31,7 @@ export function WbToolkit({ readonly mappings: WbMapping; readonly data: RA>; readonly handleDatasetDelete: () => void; - readonly hasUnSavedChanges: boolean; + readonly hasUnsavedChanges: boolean; readonly triggerRefresh: () => void; }): JSX.Element { const hot = React.useMemo( @@ -57,14 +57,14 @@ export function WbToolkit({ return (
{hasPermission('/workbench/dataset', 'transfer') && hasTablePermission('SpecifyUser', 'read') ? ( <> ) : undefined} {commonText.export()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 9702e889e7a..a2ffd0945a1 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -58,8 +58,8 @@ const fillCellsContextMenuItem = ( ?.hotInstance!?.getSelected() ?.every((selection) => selection[0] === selection[2]) ?? false), - callback: (_: any, selections: any) => - selections.forEach((selection: any) => + callback: (_, selections) => + selections.forEach((selection) => Array.from( new Array(selection.end.col + 1 - selection.start.col).keys() ).forEach((index) => { @@ -83,7 +83,7 @@ const fillCellsContextMenuItem = ( }; }; -export function WbSpreadsheet({ +function WbSpreadsheetComponent({ dataset, hotRef, isUploaded, @@ -489,3 +489,5 @@ export function WbSpreadsheet({ ); } + +export const WbSpreadsheet = React.memo(WbSpreadsheetComponent); \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index c9e235fa7ab..6fa83274c99 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -25,7 +25,7 @@ import { getInitialSearchPreferences, WbAdvancedSearch, } from './AdvancedSearch'; -import type { WbCellCounts, WbCellMetaReact } from './CellMeta'; +import type { WbCellCounts } from './CellMeta'; import { getSelectedLocalities, WbGeoLocate } from './Toolkit/GeoLocate'; import { getHotPlugin } from './handsontable'; import { getSelectedLast, getVisualHeaders } from './hotHelpers'; @@ -36,8 +36,7 @@ import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { commonText } from '../../localization/common'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { Dataset } from '../WbPlanView/Wrapped'; -import { WbMapping } from './mapping'; +import { icons } from '../Atoms/Icons'; function Navigation({ name, @@ -53,7 +52,7 @@ function Navigation({ readonly spreadsheetContainer: any; }): JSX.Element { const [currentPosition, setCurrentPosition] = React.useState(0); - const [buttonIsPressed, press, unpress, togglePress] = useBooleanState(); + const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); const handleTypeToggle = () => { togglePress(); utils.toggleCellTypes(name, "toggle", spreadsheetContainer.current); @@ -78,7 +77,7 @@ function Navigation({ variant="bg-inherit text-gray-800 dark:text-gray-100" onClick={handlePrevious} > - {'<'} + {icons.chevronLeft} - {'>'} + {icons.chevronRight} ); @@ -128,8 +127,7 @@ export function WbUtilsComponent({ const replaceRef = React.useRef(null); const handleSearch = (event: KeyboardEvent) => { - // check if debounce is needed here - console.log("here?", event.key) + // TODO: check if debounce is needed here utils.searchCells(event, searchRef.current as HTMLInputElement, spreadsheetContainer.current); } @@ -171,10 +169,11 @@ export function WbUtilsComponent({ />
) : undefined} - + { + // TODO: figure out this callback // searchPreferences = newSearchPreferences; // if ( // searchPreferences.navigation.direction !== @@ -195,7 +194,7 @@ export function WbUtilsComponent({ @@ -203,7 +202,7 @@ export function WbUtilsComponent({ @@ -211,7 +210,7 @@ export function WbUtilsComponent({ @@ -219,7 +218,7 @@ export function WbUtilsComponent({ @@ -485,6 +484,8 @@ export class WbUtilsReact { } } + this.workbench.cells.updateCellInfoStats(); + // navigationTotalElement.textContent = resultsCount.toString(); // Navigate to the first search result when hitting Enter diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index ee765198d84..bd63dc981a7 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -95,10 +95,12 @@ export function WbViewReact({ : dataset.rows, [dataset.rows] ); - const isMapped = Boolean(dataset.uploadplan); - const isUploaded = - dataset.uploadresult !== null && dataset.uploadresult.success; - const canUpdate = hasPermission('/workbench/dataset', 'update'); + + const isUploaded = React.useMemo( + () => dataset.uploadresult !== null && dataset.uploadresult.success, + [dataset] + ); + const mappings = React.useMemo( (): WbMapping | undefined => parseWbMappings(dataset), [dataset] @@ -106,10 +108,8 @@ export function WbViewReact({ const searchPreferences = getInitialSearchPreferences(); const throttleRate = Math.ceil(clamp(10, data.length / 10, 2000)); - const [hasUnSavedChanges, spreadSheetChanged, spreadSheetUpToDate] = - useBooleanState(); - const [showToolkit, _openToolkit, _closeToolkit, toggleToolkit] = + const [hasUnsavedChanges, spreadsheetChanged, spreadsheetUpToDate] = useBooleanState(); const [hotIsReady, setHotToReady] = useBooleanState(); @@ -175,12 +175,23 @@ export function WbViewReact({ if (statusCode === Http.NOT_FOUND) handleDatasetDelete(); return statusCode === Http.NOT_FOUND; }; - const hooks = getHotHooksReact( - workbench, - physicalColToMappingCol, - spreadSheetChanged, - checkDeletedFail + const hooks = React.useMemo( + () => + getHotHooksReact( + workbench, + physicalColToMappingCol, + spreadsheetChanged, + checkDeletedFail + ), + [dataset] ); + + const isMapped = Boolean(dataset.uploadplan); + const canUpdate = hasPermission('/workbench/dataset', 'update'); + + const [showToolkit, _openToolkit, _closeToolkit, toggleToolkit] = + useBooleanState(); + const [showResults, _, closeResults, toggleResults] = useBooleanState(); // Makes the hot changes required for upload view results @@ -281,12 +292,12 @@ export function WbViewReact({ ) : undefined} @@ -298,7 +309,7 @@ export function WbViewReact({ mappings={mappings!} data={data} handleDatasetDelete={handleDatasetDelete} - hasUnSavedChanges={hasUnSavedChanges} + hasUnsavedChanges={hasUnsavedChanges} triggerRefresh={triggerRefresh} /> ) : undefined} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index b9a9bafc9cd..6a030a2453b 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -21,9 +21,9 @@ import { Workbench } from './WbView'; export function getHotHooksReact( workbench: Workbench, physicalColToMappingCol: (physicalCol: number) => number | undefined, - spreadSheetChanged: () => void, - checkDeletedFail: (statusCode: number) => boolean, -) : Partial { + spreadsheetChanged: () => void, + checkDeletedFail: (statusCode: number) => boolean +): Partial { let sortConfigIsSet: boolean = false; let hotCommentsContainerRepositionTimeout: | ReturnType @@ -57,7 +57,7 @@ export function getHotHooksReact( if (physicalCol >= workbench.dataset.columns.length) return; const metaArray = workbench.cells?.cellMeta?.[physicalRow]?.[physicalCol]; if (workbench.cells?.getCellMetaFromArray(metaArray!, 'isModified')) - workbench.cells.runMetaUpdateEffects( + workbench.cells.runMetaUpdateEffects( td, 'isModified', true, @@ -65,7 +65,7 @@ export function getHotHooksReact( visualCol ); if (workbench.cells?.getCellMetaFromArray(metaArray!, 'isNew')) - workbench.cells?.runMetaUpdateEffects( + workbench.cells?.runMetaUpdateEffects( td, 'isNew', true, @@ -73,19 +73,13 @@ export function getHotHooksReact( visualCol ); if (workbench.cells!.getCellMetaFromArray(metaArray!, 'isSearchResult')) - workbench.cells?.runMetaUpdateEffects( + workbench.cells?.runMetaUpdateEffects( td, 'isSearchResult', true, visualRow, visualCol ); - // if (workbench.cells!.getCellMetaFromArray(metaArray!, 'issues')) { - // const issues = workbench.cells!.getCellMetaFromArray(metaArray!, 'issues') - // td?.classList[issues.length > 0 ? 'add' : 'remove']( - // 'htCommentCell' - // ); - // } if (workbench.mappings?.mappedHeaders?.[physicalCol] === undefined) td.classList.add('text-gray-500'); if (workbench.mappings?.coordinateColumns?.[physicalCol] !== undefined) @@ -148,7 +142,7 @@ export function getHotHooksReact( ), ]); if (JSON.stringify(issues) !== JSON.stringify(newIssues)) - workbench.cells!.updateCellMeta( + workbench.cells!.updateCellMeta( physicalRow, physicalCol, 'issues', @@ -179,7 +173,8 @@ export function getHotHooksReact( if (source !== 'CopyPaste.paste') return true; const filteredChanges = unfilteredChanges.filter( - ([, property]) => (property as number) < workbench.dataset.columns.length + ([, property]) => + (property as number) < workbench.dataset.columns.length ); if ( filteredChanges.length === unfilteredChanges.length || @@ -220,7 +215,7 @@ export function getHotHooksReact( typeof property === 'number' ? property : workbench.hot!.toPhysicalColumn( - workbench.hot!.propToCol(property as number | string) + workbench.hot!.propToCol(property as number | string) ), oldValue, newValue, @@ -245,8 +240,7 @@ export function getHotHooksReact( changes // Ignore changes to unmapped columns .filter( - ({ physicalCol }) => - physicalColToMappingCol(physicalCol) !== -1 + ({ physicalCol }) => physicalColToMappingCol(physicalCol) !== -1 ) .sort(sortFunction(({ visualRow }) => visualRow)) .map(({ physicalRow }) => physicalRow) @@ -284,10 +278,14 @@ export function getHotHooksReact( 'originalValue', oldValue ); - workbench.cells?.recalculateIsModifiedState(physicalRow, physicalCol, { - visualRow, - visualCol, - }); + workbench.cells?.recalculateIsModifiedState( + physicalRow, + physicalCol, + { + visualRow, + visualCol, + } + ); if ( workbench.utils?.searchPreferences.search.liveUpdate && workbench.utils?.searchQuery !== undefined @@ -302,7 +300,7 @@ export function getHotHooksReact( } ); - spreadSheetChanged(); + spreadsheetChanged(); workbench.cells?.updateCellInfoStats(); if (workbench.dataset.uploadplan) @@ -341,10 +339,9 @@ export function getHotHooksReact( addedRows .filter((physicalRow) => physicalRow < workbench.cells!.cellMeta.length) .forEach((physicalRow) => - workbench.cells?.cellMeta.splice(physicalRow, 0, []) + workbench.cells?.cellMeta.splice(physicalRow, 0, []) ); - if (workbench.hot && source !== 'auto') - spreadSheetChanged(); + if (workbench.hot && source !== 'auto') spreadsheetChanged(); return true; }, @@ -368,7 +365,7 @@ export function getHotHooksReact( workbench.cells!.flushIndexedCellData = true; if (workbench.hot && source !== 'auto') { - spreadSheetChanged(); + spreadsheetChanged(); workbench.cells!.updateCellInfoStats(); } @@ -404,7 +401,8 @@ export function getHotHooksReact( })) .map(({ physicalCol, ...rest }) => ({ ...rest, - rankGroup: workbench.mappings!.treeRanks?.map((rankGroup, groupIndex) => ({ + rankGroup: workbench + .mappings!.treeRanks?.map((rankGroup, groupIndex) => ({ rankId: rankGroup.find( (mapping) => mapping.physicalCol === physicalCol )?.rankId, @@ -491,16 +489,12 @@ export function getHotHooksReact( // !wbView.uploadedView && // !wbView.coordinateConverterView && // An ugly fix for jQuery's dialogs conflicting with HOT - (dropIndex !== undefined || !workbench.hot), + dropIndex !== undefined || !workbench.hot, // Save new visualOrder on the back end afterColumnMove: (_columnIndexes, _finalIndex, dropIndex) => { // An ugly fix for jQuery's dialogs conflicting with HOT - if ( - dropIndex === undefined || - !workbench.hot - ) - return; + if (dropIndex === undefined || !workbench.hot) return; workbench.cells!.flushIndexedCellData = true; @@ -510,7 +504,9 @@ export function getHotHooksReact( if ( workbench.dataset.visualorder === null || - columnOrder.some((i, index) => i !== workbench.dataset.visualorder![index]) + columnOrder.some( + (i, index) => i !== workbench.dataset.visualorder![index] + ) ) { overwriteReadOnly(workbench.dataset, 'visualorder', columnOrder); ping(`/api/workbench/dataset/${workbench.dataset.id}/`, { @@ -544,8 +540,8 @@ export function getHotHooksReact( // Make sure cell has comments if ( - workbench.cells!.getCellMeta(physicalRow, physicalCol, 'issues').length === - 0 + workbench.cells!.getCellMeta(physicalRow, physicalCol, 'issues') + .length === 0 ) return; @@ -622,7 +618,7 @@ function afterUndoRedoReact( data: Action ): void { if ( - // workbench.undoRedoIsHandled || + // workbench.undoRedoIsHandled || data.actionType !== 'change' || data.changes.length !== 1 || workbench.hot === undefined @@ -658,7 +654,6 @@ function afterUndoRedoReact( else workbench.disambiguation!.afterChangeDisambiguation(physicalRow); } - export function getHotHooks(wbView: WbView) { let sortConfigIsSet: boolean = false; let hotCommentsContainerRepositionTimeout: diff --git a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts index 6beb4394d92..bf23119527b 100644 --- a/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts +++ b/specifyweb/frontend/js_src/lib/utils/cache/definitions.ts @@ -4,7 +4,7 @@ * @module */ -import type { SortOrderType } from 'handsontable/plugins/columnSorting'; +import type hot from 'handsontable'; import type { State } from 'typesafe-reducer'; import type { AppResourcesConformation } from '../../components/AppResources/Aside'; @@ -90,9 +90,7 @@ export type CacheDefinitions = { * WorkBench column sort setting in a given dataset * {Collection ID}_{Dataset ID} */ - [key in `${number}_${number}`]: RA<{ - readonly column: number; - readonly sortOrder: SortOrderType; + [key in `${number}_${number}`]: RA & { readonly physicalCol: number; }>; }; From 336f42530c06447fdbe5ec86670da199cdcf69c4 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:23:09 -0400 Subject: [PATCH 039/153] Move components into WbActions and WbToolkit folders --- .../components/{WorkBench/Actions => WbActions}/WbActions.tsx | 0 .../components/{WorkBench/Actions => WbActions}/WbResults.tsx | 0 .../components/{WorkBench/Actions => WbActions}/WbRevert.tsx | 0 .../{WorkBench/Actions => WbActions}/WbRollback.tsx | 0 .../components/{WorkBench/Actions => WbActions}/WbSave.tsx | 0 .../components/{WorkBench/Actions => WbActions}/WbUpload.tsx | 0 .../{WorkBench/Actions => WbActions}/WbValidate.tsx | 0 .../{WorkBench/Toolkit => WbToolkit}/ChangeOwner.tsx | 0 .../{WorkBench/Toolkit => WbToolkit}/CoordinateConverter.tsx | 0 .../{WorkBench/Toolkit => WbToolkit}/DevShowPlan.tsx | 0 .../components/{WorkBench/Toolkit => WbToolkit}/GeoLocate.tsx | 0 .../{WorkBench/Toolkit => WbToolkit}/WbLeafletMap.tsx | 0 .../components/{WorkBench/Toolkit => WbToolkit}/WbToolkit.tsx | 0 .../frontend/js_src/lib/components/WorkBench/WbUtils.tsx | 2 +- .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 4 ++-- 15 files changed, 3 insertions(+), 3 deletions(-) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Actions => WbActions}/WbActions.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Actions => WbActions}/WbResults.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Actions => WbActions}/WbRevert.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Actions => WbActions}/WbRollback.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Actions => WbActions}/WbSave.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Actions => WbActions}/WbUpload.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Actions => WbActions}/WbValidate.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Toolkit => WbToolkit}/ChangeOwner.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Toolkit => WbToolkit}/CoordinateConverter.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Toolkit => WbToolkit}/DevShowPlan.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Toolkit => WbToolkit}/GeoLocate.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Toolkit => WbToolkit}/WbLeafletMap.tsx (100%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/Toolkit => WbToolkit}/WbToolkit.tsx (100%) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbActions.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbResults.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRevert.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbRollback.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbSave.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbUpload.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Actions/WbValidate.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/ChangeOwner.tsx rename to specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/CoordinateConverter.tsx rename to specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/DevShowPlan.tsx rename to specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/GeoLocate.tsx rename to specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbLeafletMap.tsx rename to specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Toolkit/WbToolkit.tsx rename to specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 6fa83274c99..2a8b2473ae6 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -26,7 +26,7 @@ import { WbAdvancedSearch, } from './AdvancedSearch'; import type { WbCellCounts } from './CellMeta'; -import { getSelectedLocalities, WbGeoLocate } from './Toolkit/GeoLocate'; +import { getSelectedLocalities, WbGeoLocate } from './WbToolkit/GeoLocate'; import { getHotPlugin } from './handsontable'; import { getSelectedLast, getVisualHeaders } from './hotHelpers'; import type { WbView, Workbench } from './WbView'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index bd63dc981a7..792a2983de8 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -49,13 +49,13 @@ import { parseWbMappings } from './mapping'; import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; import { wbViewTemplate } from './Template'; -import { WbActionsComponent } from './Actions/WbActions'; +import { WbActionsComponent } from './WbActions/WbActions'; import { WbUtils, WbUtilsComponent, WbUtilsReact } from './WbUtils'; import { WbValidation, WbValidationReact } from './WbValidation'; import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { WbToolkit } from './Toolkit/WbToolkit'; +import { WbToolkit } from './WbToolkit/WbToolkit'; import { getInitialSearchPreferences } from './AdvancedSearch'; import type { WbSearchPreferences } from './AdvancedSearch'; From f94741a981065632cd7c54663a9cece7408f7eda Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 3 Apr 2024 16:46:08 -0400 Subject: [PATCH 040/153] Fix import paths --- .../lib/components/WbActions/WbActions.tsx | 34 ++++++++--------- .../lib/components/WbActions/WbResults.tsx | 6 +-- .../lib/components/WbActions/WbRevert.tsx | 10 ++--- .../lib/components/WbActions/WbRollback.tsx | 10 ++--- .../lib/components/WbActions/WbSave.tsx | 22 +++++------ .../lib/components/WbActions/WbUpload.tsx | 14 +++---- .../lib/components/WbActions/WbValidate.tsx | 10 ++--- .../lib/components/WbToolkit/ChangeOwner.tsx | 38 +++++++++---------- .../WbToolkit/CoordinateConverter.tsx | 30 +++++++-------- .../lib/components/WbToolkit/DevShowPlan.tsx | 28 +++++++------- .../lib/components/WbToolkit/GeoLocate.tsx | 30 +++++++-------- .../lib/components/WbToolkit/WbLeafletMap.tsx | 18 ++++----- .../lib/components/WbToolkit/WbToolkit.tsx | 20 +++++----- .../lib/components/WorkBench/WbUtils.tsx | 2 +- .../lib/components/WorkBench/WbView.tsx | 4 +- 15 files changed, 138 insertions(+), 138 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index 499fe8e5fab..7ab7d9f5ebb 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -1,22 +1,22 @@ import React from 'react'; -import { commonText } from '../../../localization/common'; -import { wbPlanText } from '../../../localization/wbPlan'; -import { wbText } from '../../../localization/workbench'; -import { Http } from '../../../utils/ajax/definitions'; -import { ping } from '../../../utils/ajax/ping'; -import { Button } from '../../Atoms/Button'; -import { Link } from '../../Atoms/Link'; -import { Dialog } from '../../Molecules/Dialog'; -import type { Status } from '../../WbPlanView/Wrapped'; -import { CreateRecordSetButton } from '../RecordSet'; -import { WbStatus as WbStatusComponent } from '../Status'; -import type { WbStatus, Workbench } from '../WbView'; -import type { Dataset } from '../../WbPlanView/Wrapped'; -import type { WbMapping } from '../mapping'; -import { hasPermission } from '../../Permissions/helpers'; -import { userPreferences } from '../../Preferences/userPreferences'; -import { useBooleanState } from '../../../hooks/useBooleanState'; +import { commonText } from '../../localization/common'; +import { wbPlanText } from '../../localization/wbPlan'; +import { wbText } from '../../localization/workbench'; +import { Http } from '../../utils/ajax/definitions'; +import { ping } from '../../utils/ajax/ping'; +import { Button } from '../Atoms/Button'; +import { Link } from '../Atoms/Link'; +import { Dialog } from '../Molecules/Dialog'; +import type { Status } from '../WbPlanView/Wrapped'; +import { CreateRecordSetButton } from '../WorkBench/RecordSet'; +import { WbStatus as WbStatusComponent } from '../WorkBench/Status'; +import type { WbStatus, Workbench } from '../WorkBench/WbView'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { WbMapping } from '../WorkBench/mapping'; +import { hasPermission } from '../Permissions/helpers'; +import { userPreferences } from '../Preferences/userPreferences'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { WbResults } from './WbResults'; import { WbValidate } from './WbValidate'; import { WbRollback } from './WbRollback'; diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx index 9d1f6c35f6f..5834843e412 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { Button } from '../../Atoms/Button'; -import { wbText } from '../../../localization/workbench'; -import { commonText } from '../../../localization/common'; +import { Button } from '../Atoms/Button'; +import { wbText } from '../../localization/workbench'; +import { commonText } from '../../localization/common'; export function WbResults({ hasUnsavedChanges, diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx index 78fe58a1444..b5d4e8a91f1 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import { Button } from '../../Atoms/Button'; -import { wbText } from '../../../localization/workbench'; -import { commonText } from '../../../localization/common'; -import { Dialog } from '../../Molecules/Dialog'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { Button } from '../Atoms/Button'; +import { wbText } from '../../localization/workbench'; +import { commonText } from '../../localization/common'; +import { Dialog } from '../Molecules/Dialog'; export function WbRevert({ hasUnsavedChanges, diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx index 829f4e6555e..8b3ce7778d7 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import type { WbStatus } from '../WbView'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import { Button } from '../../Atoms/Button'; -import { wbText } from '../../../localization/workbench'; -import { RollbackConfirmation } from '../Components'; +import type { WbStatus } from '../WorkBench/WbView'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { Button } from '../Atoms/Button'; +import { wbText } from '../../localization/workbench'; +import { RollbackConfirmation } from '../WorkBench/Components'; export function WbRollback({ datasetId, diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index 7d350ecd9b6..3e094f19e37 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -1,16 +1,16 @@ import React from 'react'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import { Button } from '../../Atoms/Button'; -import { className } from '../../Atoms/className'; -import { Dialog } from '../../Molecules/Dialog'; -import { commonText } from '../../../localization/common'; -import { wbText } from '../../../localization/workbench'; -import { loadingBar } from '../../Molecules'; -import { Workbench } from '../WbView'; -import { overwriteReadOnly } from '../../../utils/types'; -import { ping } from '../../../utils/ajax/ping'; -import { Http } from '../../../utils/ajax/definitions'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { Button } from '../Atoms/Button'; +import { className } from '../Atoms/className'; +import { Dialog } from '../Molecules/Dialog'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; +import { loadingBar } from '../Molecules'; +import { Workbench } from '../WorkBench/WbView'; +import { overwriteReadOnly } from '../../utils/types'; +import { ping } from '../../utils/ajax/ping'; +import { Http } from '../../utils/ajax/definitions'; export function WbSave({ workbench, diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx index cde7b8bdb81..9d32d919887 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import type { WbMapping } from '../mapping'; -import type { WbStatus } from '../WbView'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import { Button } from '../../Atoms/Button'; -import { Dialog } from '../../Molecules/Dialog'; -import { wbText } from '../../../localization/workbench'; -import { commonText } from '../../../localization/common'; +import type { WbMapping } from '../WorkBench/mapping'; +import type { WbStatus } from '../WorkBench/WbView'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { Button } from '../Atoms/Button'; +import { Dialog } from '../Molecules/Dialog'; +import { wbText } from '../../localization/workbench'; +import { commonText } from '../../localization/common'; export function WbUpload({ hasUnsavedChanges, diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index 46091e6f234..b9c5e9e377e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import type { WbStatus } from '../WbView'; -import { WbValidationReact } from '../WbValidation'; -import { Button } from '../../Atoms/Button'; -import { commonText } from '../../../localization/common'; -import { wbText } from '../../../localization/workbench'; +import type { WbStatus } from '../WorkBench/WbView'; +import { WbValidationReact } from '../WorkBench/WbValidation'; +import { Button } from '../Atoms/Button'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; export function WbValidate({ canLiveValidate, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx index 0138acd2d51..be543fdc914 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx @@ -1,24 +1,24 @@ import React from "react"; -import { useAsyncState } from '../../../hooks/useAsyncState'; -import { formData } from '../../../utils/ajax/helpers'; -import type { RA } from '../../../utils/types'; -import { Form, Label, Select } from '../../Atoms/Form'; -import { fetchCollection } from '../../DataModel/collection'; -import type { SerializedResource } from '../../DataModel/helperTypes'; -import { userInformation } from '../../InitialContext/userInformation'; -import type { SpecifyUser } from '../../DataModel/types'; -import { useId } from '../../../hooks/useId'; -import type { Dataset } from "../../WbPlanView/Wrapped"; -import { useBooleanState } from "../../../hooks/useBooleanState"; -import { ping } from '../../../utils/ajax/ping'; -import { Submit } from '../../Atoms/Submit'; -import { LoadingContext } from '../../Core/Contexts'; -import { Dialog } from '../../Molecules/Dialog'; -import { unsafeNavigate } from '../../Router/Router'; -import { Button } from '../../Atoms/Button'; -import { commonText } from '../../../localization/common'; -import { wbText } from "../../../localization/workbench"; +import { useAsyncState } from '../../hooks/useAsyncState'; +import { formData } from '../../utils/ajax/helpers'; +import type { RA } from '../../utils/types'; +import { Form, Label, Select } from '../Atoms/Form'; +import { fetchCollection } from '../DataModel/collection'; +import type { SerializedResource } from '../DataModel/helperTypes'; +import { userInformation } from '../InitialContext/userInformation'; +import type { SpecifyUser } from '../DataModel/types'; +import { useId } from '../../hooks/useId'; +import type { Dataset } from "../WbPlanView/Wrapped"; +import { useBooleanState } from "../../hooks/useBooleanState"; +import { ping } from '../../utils/ajax/ping'; +import { Submit } from '../Atoms/Submit'; +import { LoadingContext } from '../Core/Contexts'; +import { Dialog } from '../Molecules/Dialog'; +import { unsafeNavigate } from '../Router/Router'; +import { Button } from '../Atoms/Button'; +import { commonText } from '../../localization/common'; +import { wbText } from "../../localization/workbench"; export function WbChangeOwner({ hasUnsavedChanges, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 4e1b921c91f..3ced37ca468 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -1,21 +1,21 @@ import type Handsontable from 'handsontable'; import React from 'react'; -import { useCachedState } from '../../../hooks/useCachedState'; -import { commonText } from '../../../localization/common'; -import { localityText } from '../../../localization/locality'; -import { wbText } from '../../../localization/workbench'; -import type { ConversionFunction } from '../../../utils/latLong'; -import { Lat, Long } from '../../../utils/latLong'; -import type { RA, RR } from '../../../utils/types'; -import { Ul } from '../../Atoms'; -import { Button } from '../../Atoms/Button'; -import { Input, Label } from '../../Atoms/Form'; -import { Dialog } from '../../Molecules/Dialog'; -import { getSelectedCells, getSelectedLast, setHotData } from '../hotHelpers'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import type { Dataset } from '../../WbPlanView/Wrapped'; -import type { WbMapping } from '../mapping'; +import { useCachedState } from '../../hooks/useCachedState'; +import { commonText } from '../../localization/common'; +import { localityText } from '../../localization/locality'; +import { wbText } from '../../localization/workbench'; +import type { ConversionFunction } from '../../utils/latLong'; +import { Lat, Long } from '../../utils/latLong'; +import type { RA, RR } from '../../utils/types'; +import { Ul } from '../Atoms'; +import { Button } from '../Atoms/Button'; +import { Input, Label } from '../Atoms/Form'; +import { Dialog } from '../Molecules/Dialog'; +import { getSelectedCells, getSelectedLast, setHotData } from '../WorkBench/hotHelpers'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { WbMapping } from '../WorkBench/mapping'; export function WbConvertCoordinates({ hasLocality, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx index f33cef39f60..93e1ea341f1 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx @@ -1,19 +1,19 @@ import React from 'react'; -import { commonText } from '../../../localization/common'; -import { wbPlanText } from '../../../localization/wbPlan'; -import { Http } from '../../../utils/ajax/definitions'; -import { ping } from '../../../utils/ajax/ping'; -import { Button } from '../../Atoms/Button'; -import { LoadingContext } from '../../Core/Contexts'; -import { AutoGrowTextArea } from '../../Molecules/AutoGrowTextArea'; -import { Dialog } from '../../Molecules/Dialog'; -import { downloadFile } from '../../Molecules/FilePicker'; -import type { UploadPlan } from '../../WbPlanView/uploadPlanParser'; -import { overwriteReadOnly } from '../../../utils/types'; -import type { Dataset } from '../../WbPlanView/Wrapped'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import { wbText } from '../../../localization/workbench'; +import { commonText } from '../../localization/common'; +import { wbPlanText } from '../../localization/wbPlan'; +import { Http } from '../../utils/ajax/definitions'; +import { ping } from '../../utils/ajax/ping'; +import { Button } from '../Atoms/Button'; +import { LoadingContext } from '../Core/Contexts'; +import { AutoGrowTextArea } from '../Molecules/AutoGrowTextArea'; +import { Dialog } from '../Molecules/Dialog'; +import { downloadFile } from '../Molecules/FilePicker'; +import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; +import { overwriteReadOnly } from '../../utils/types'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { wbText } from '../../localization/workbench'; export function WbDevPlan({ diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index a5d661fddf5..4ca97ccc02d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -1,24 +1,24 @@ import type Handsontable from 'handsontable'; import React from 'react'; -import { commonText } from '../../../localization/common'; -import { f } from '../../../utils/functools'; -import type { IR, RA } from '../../../utils/types'; -import { filterArray } from '../../../utils/types'; -import { sortFunction } from '../../../utils/utils'; -import { Button } from '../../Atoms/Button'; +import { commonText } from '../../localization/common'; +import { f } from '../../utils/functools'; +import type { IR, RA } from '../../utils/types'; +import { filterArray } from '../../utils/types'; +import { sortFunction } from '../../utils/utils'; +import { Button } from '../Atoms/Button'; import { getLocalityCoordinate, getSelectedLocalityColumns, -} from '../../Leaflet/wbLocalityDataExtractor'; -import type { GeoLocatePayload } from '../../Molecules/GeoLocate'; -import { GenericGeoLocate } from '../../Molecules/GeoLocate'; -import { getSelectedRegions, getVisualHeaders, setHotData } from '../hotHelpers'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import type { Dataset } from '../../WbPlanView/Wrapped'; -import type { WbMapping } from '../mapping'; -import { wbText } from '../../../localization/workbench'; -import { localityText } from '../../../localization/locality'; +} from '../Leaflet/wbLocalityDataExtractor'; +import type { GeoLocatePayload } from '../Molecules/GeoLocate'; +import { GenericGeoLocate } from '../Molecules/GeoLocate'; +import { getSelectedRegions, getVisualHeaders, setHotData } from '../WorkBench/hotHelpers'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { WbMapping } from '../WorkBench/mapping'; +import { wbText } from '../../localization/workbench'; +import { localityText } from '../../localization/locality'; export function WbGeoLocate({ hasLocality, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx index bd1f866c60e..b8779b1d619 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx @@ -2,15 +2,15 @@ import React from "react"; import Handsontable from "handsontable"; import { getSelectedLocalities } from './GeoLocate'; -import { getLocalitiesDataFromSpreadsheet } from '../../Leaflet/wbLocalityDataExtractor'; -import { getSelectedLast, getVisualHeaders } from '../hotHelpers'; -import { LeafletMap } from '../../Leaflet/Map'; -import { useBooleanState } from '../../../hooks/useBooleanState'; -import type { Dataset } from "../../WbPlanView/Wrapped"; -import type { WbMapping } from "../mapping"; -import { Button } from "../../Atoms/Button"; -import { localityText } from "../../../localization/locality"; -import { wbText } from "../../../localization/workbench"; +import { getLocalitiesDataFromSpreadsheet } from '../Leaflet/wbLocalityDataExtractor'; +import { getSelectedLast, getVisualHeaders } from '../WorkBench/hotHelpers'; +import { LeafletMap } from '../Leaflet/Map'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import type { Dataset } from "../WbPlanView/Wrapped"; +import type { WbMapping } from "../WorkBench/mapping"; +import { Button } from "../Atoms/Button"; +import { localityText } from "../../localization/locality"; +import { wbText } from "../../localization/workbench"; export function WbLeafletMap({ hasLocality, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx index 0c64543a1f3..c3f3d01d94d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx @@ -1,18 +1,18 @@ import React from 'react'; import { HotTable } from '@handsontable/react'; -import { hasPermission, hasTablePermission } from '../../Permissions/helpers'; +import { hasPermission, hasTablePermission } from '../Permissions/helpers'; import { WbChangeOwner } from './ChangeOwner'; import { WbConvertCoordinates } from './CoordinateConverter'; -import { commonText } from '../../../localization/common'; -import { wbText } from '../../../localization/workbench'; -import type { RA } from '../../../utils/types'; -import { Button } from '../../Atoms/Button'; -import { raise } from '../../Errors/Crash'; -import { userPreferences } from '../../Preferences/userPreferences'; -import type { Dataset } from '../../WbPlanView/Wrapped'; -import { downloadDataSet } from '../helpers'; -import type { WbMapping } from '../mapping'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; +import type { RA } from '../../utils/types'; +import { Button } from '../Atoms/Button'; +import { raise } from '../Errors/Crash'; +import { userPreferences } from '../Preferences/userPreferences'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import { downloadDataSet } from '../WorkBench/helpers'; +import type { WbMapping } from '../WorkBench/mapping'; import { WbDevPlan } from './DevShowPlan'; import { WbGeoLocate } from './GeoLocate'; import { WbLeafletMap } from './WbLeafletMap'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 2a8b2473ae6..0f494ff7652 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -26,7 +26,7 @@ import { WbAdvancedSearch, } from './AdvancedSearch'; import type { WbCellCounts } from './CellMeta'; -import { getSelectedLocalities, WbGeoLocate } from './WbToolkit/GeoLocate'; +// import { getSelectedLocalities, WbGeoLocate } from './WbToolkit/GeoLocate'; import { getHotPlugin } from './handsontable'; import { getSelectedLast, getVisualHeaders } from './hotHelpers'; import type { WbView, Workbench } from './WbView'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 792a2983de8..e2621b73e4c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -49,13 +49,13 @@ import { parseWbMappings } from './mapping'; import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; import { wbViewTemplate } from './Template'; -import { WbActionsComponent } from './WbActions/WbActions'; +import { WbActionsComponent } from '../WbActions/WbActions'; import { WbUtils, WbUtilsComponent, WbUtilsReact } from './WbUtils'; import { WbValidation, WbValidationReact } from './WbValidation'; import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { WbToolkit } from './WbToolkit/WbToolkit'; +import { WbToolkit } from '../WbToolkit/WbToolkit'; import { getInitialSearchPreferences } from './AdvancedSearch'; import type { WbSearchPreferences } from './AdvancedSearch'; From fdbcbad96547070252b3ba8209d42642453acb19 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:45:26 -0400 Subject: [PATCH 041/153] Fix cell count values not displaying correctly - minor change: disable upload button when errors exist --- .../lib/components/WbActions/WbActions.tsx | 1 + .../lib/components/WbActions/WbUpload.tsx | 13 ++++- .../lib/components/WorkBench/CellMeta.ts | 53 ++----------------- .../lib/components/WorkBench/WbView.tsx | 6 +-- 4 files changed, 20 insertions(+), 53 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index 7ab7d9f5ebb..dc41fff9fde 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -219,6 +219,7 @@ export function WbActionsComponent({ mappings={mappings} openNoUploadPlan={openNoUploadPlan} startUpload={actions.startUpload} + cellCounts={workbench.cellCounts[0]} /> ) : undefined} {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx index 9d32d919887..c4107a138e8 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx @@ -7,17 +7,20 @@ import { Button } from '../Atoms/Button'; import { Dialog } from '../Molecules/Dialog'; import { wbText } from '../../localization/workbench'; import { commonText } from '../../localization/common'; +import { WbCellCounts } from '../WorkBench/CellMeta'; export function WbUpload({ hasUnsavedChanges, mappings, openNoUploadPlan, startUpload, + cellCounts, }: { readonly hasUnsavedChanges: boolean; readonly mappings: WbMapping; readonly openNoUploadPlan: () => void; readonly startUpload: (mode: WbStatus) => void; + readonly cellCounts: WbCellCounts; }): JSX.Element { const [showUpload, openUpload, closeUpload] = useBooleanState(); @@ -39,8 +42,14 @@ export function WbUpload({ 0} + title={ + hasUnsavedChanges + ? wbText.unavailableWhileEditing() + : cellCounts.invalidCells > 0 + ? wbText.uploadUnavailableWhileHasErrors() + : '' + } > {wbText.upload()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index 9249497bbd4..4c1d1dabaa5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -52,8 +52,6 @@ export class WbCellMetaReact { // eslint-disable-next-line functional/prefer-readonly-type public cellMeta: WritableArray> = []; - public cellCounts: WbCellCounts | undefined = undefined; - // Meta data for each cell (indexed by visual columns) // eslint-disable-next-line functional/prefer-readonly-type private indexedCellMeta: RA> | undefined = undefined; @@ -159,7 +157,6 @@ export class WbCellMetaReact { * If cell was disambiguated, it should show up as changed, even if value * is unchanged */ - // @ts-expect-error return this.workbench.disambiguation?.cellWasDisambiguated( physicalRow, physicalCol @@ -201,7 +198,7 @@ export class WbCellMetaReact { visualRow: number, visualCol: number ) { - if (this.workbench.hot === undefined) return; + if (!this.workbench.hot) return; if (key === 'isNew') cell?.classList[value === true ? 'add' : 'remove']('wb-no-match-cell'); @@ -259,7 +256,7 @@ export class WbCellMetaReact { readonly visualCol?: number; } = {} ) { - if (this.workbench.hot === undefined) return; + if (!this.workbench.hot) return; const isValueChanged = this.setCellMeta( physicalRow, physicalCol, @@ -335,7 +332,7 @@ export class WbCellMetaReact { updateCellInfoStats() { const cellMeta = this.cellMeta.flat(); - this.cellCounts = { + const cellCounts = { newCells: cellMeta.reduce( (count, info) => count + (this.getCellMetaFromArray(info, 'isNew') ? 1 : 0), @@ -359,48 +356,8 @@ export class WbCellMetaReact { ), }; - // below line breaks error cell highlighting - // this.workbench.setCellCounts(this.cellCounts); - - // OLD CODE: NEEDS TO BE REMOVED AT THE END - // Update navigation information - // needs to be changed - // Object.values( - // this.wbView.el.getElementsByClassName('wb-navigation-total') - // ).forEach((navigationTotalElement) => { - // const navigationContainer = navigationTotalElement.closest( - // '.wb-navigation-section' - // ); - // if (navigationContainer === null) return; - // const navigationType = navigationContainer.getAttribute( - // 'data-navigation-type' - // ) as keyof WbCellCounts | null; - // if (navigationType === null) return; - // navigationTotalElement.textContent = - // cellCounts[navigationType]?.toString(); - - // if (cellCounts[navigationType] === 0) { - // const currentPositionElement = - // navigationContainer.getElementsByClassName( - // 'wb-navigation-position' - // )?.[0]; - // if (typeof currentPositionElement === 'object') - // currentPositionElement.textContent = '0'; - // } - // }); - - // const uploadButton = - // this.wbView.el.querySelector('.wb-upload'); - // if (uploadButton === null) return; - // const title = wbText.uploadUnavailableWhileHasErrors(); - // if ( - // !uploadButton.disabled || - // uploadButton.getAttribute('title') === title - // ) { - // const hasErrors = cellCounts.invalidCells > 0; - // uploadButton.toggleAttribute('disabled', hasErrors); - // uploadButton.setAttribute('title', hasErrors ? title : ''); - // } + const [_, setCellCounts] = this.workbench.cellCounts; + setCellCounts(cellCounts); } public cellIsType(metaArray: WbMetaArray, type: keyof WbCellCounts): boolean { diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index e2621b73e4c..2deb0907ac4 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -22,7 +22,7 @@ import { wbPlanText } from '../../localization/wbPlan'; import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; import { f } from '../../utils/functools'; -import type { IR, RA, WritableArray } from '../../utils/types'; +import type { GetSet, IR, RA, WritableArray } from '../../utils/types'; import { ensure, overwriteReadOnly, writable } from '../../utils/types'; import { clamp, throttle } from '../../utils/utils'; import { Button } from '../Atoms/Button'; @@ -72,7 +72,7 @@ export type Workbench = { throttleRate: number; mappings: WbMapping; utils: WbUtilsReact; - setCellCounts: (cellCounts: WbCellCounts) => void; + cellCounts: GetSet; }; export function WbViewReact({ @@ -157,7 +157,7 @@ export function WbViewReact({ disambiguation: undefined!, validation: undefined!, utils: undefined!, - setCellCounts, + cellCounts: [cellCounts, setCellCounts], }; workbench.cells = new WbCellMetaReact(workbench); workbench.disambiguation = new DisambiguationReact(workbench); From 252f12dd02fb3aba792f692df0e74232d038aa58 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:30:50 -0400 Subject: [PATCH 042/153] Fix cell navigation position - Removes old WbUtils code - Removes search pref duplication --- .../lib/components/WorkBench/WbUtils.tsx | 977 ++---------------- .../lib/components/WorkBench/WbView.tsx | 14 +- 2 files changed, 116 insertions(+), 875 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 0f494ff7652..890022cdf48 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -6,19 +6,13 @@ * */ -import type Handsontable from 'handsontable'; import React from 'react'; import _ from 'underscore'; import { wbText } from '../../localization/workbench'; import { f } from '../../utils/functools'; import type { RA, WritableArray } from '../../utils/types'; -import { filterArray } from '../../utils/types'; -import { camelToKebab, clamp } from '../../utils/utils'; -import { Backbone } from '../DataModel/backbone'; -import { softFail } from '../Errors/Crash'; -import { LeafletMap } from '../Leaflet/Map'; -import { getLocalitiesDataFromSpreadsheet } from '../Leaflet/wbLocalityDataExtractor'; +import { camelToKebab } from '../../utils/utils'; import { hasPermission } from '../Permissions/helpers'; import type { WbSearchPreferences } from './AdvancedSearch'; import { @@ -26,10 +20,9 @@ import { WbAdvancedSearch, } from './AdvancedSearch'; import type { WbCellCounts } from './CellMeta'; -// import { getSelectedLocalities, WbGeoLocate } from './WbToolkit/GeoLocate'; import { getHotPlugin } from './handsontable'; -import { getSelectedLast, getVisualHeaders } from './hotHelpers'; -import type { WbView, Workbench } from './WbView'; +import { getSelectedLast } from './hotHelpers'; +import type { Workbench } from './WbView'; import { Input } from '../Atoms/Form'; import { LocalizedString } from 'typesafe-i18n'; import { Button } from '../Atoms/Button'; @@ -43,28 +36,38 @@ function Navigation({ label, totalCount, utils, - spreadsheetContainer + spreadsheetContainer, }: { readonly name: keyof WbCellCounts; readonly label: LocalizedString; readonly totalCount: number; - readonly utils: WbUtilsReact; + readonly utils: WbUtils; readonly spreadsheetContainer: any; }): JSX.Element { const [currentPosition, setCurrentPosition] = React.useState(0); const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); const handleTypeToggle = () => { togglePress(); - utils.toggleCellTypes(name, "toggle", spreadsheetContainer.current); - } + utils.toggleCellTypes(name, 'toggle', spreadsheetContainer.current); + }; const handlePrevious = () => { - setCurrentPosition((prev) => Math.max(0, prev - 1)); - utils.navigateCells(name, 'previous'); - } + const [_, position] = utils.navigateCells({ + type: name, + direction: 'previous', + currentCellPosition: currentPosition, + totalCount, + }); + setCurrentPosition(position); + }; const handleNext = () => { - setCurrentPosition((prev) => Math.min(totalCount, prev + 1)); - utils.navigateCells(name, 'next'); - } + const [_, position] = utils.navigateCells({ + type: name, + direction: 'next', + currentCellPosition: currentPosition, + totalCount, + }); + setCurrentPosition(position); + }; return ( (null); const replaceRef = React.useRef(null); - + const handleSearch = (event: KeyboardEvent) => { // TODO: check if debounce is needed here - utils.searchCells(event, searchRef.current as HTMLInputElement, spreadsheetContainer.current); - } + utils.searchCells( + event, + searchRef.current as HTMLInputElement, + spreadsheetContainer.current + ); + }; const handleReplace = (event: KeyboardEvent) => { utils.replaceCells(event, replaceRef.current as HTMLInputElement); - } - + }; + return ( <>
{ - // TODO: figure out this callback - // searchPreferences = newSearchPreferences; - // if ( - // searchPreferences.navigation.direction !== - // initialNavigationDirection - // ) { - // this.wbView.cells.flushIndexedCellData = true; - // initialNavigationDirection = - // searchPreferences.navigation.direction; - // } - // if (searchPreferences.search.liveUpdate) - // this.searchCells({ + if ( + newSearchPreferences.navigation.direction !== + utils.searchPreferences.navigation.direction + ) { + // TODO: add workbench or cells to parent component + // cells.flushIndexedCellData = true; + } + utils.searchPreferences = newSearchPreferences; + // TODO: figure out what searchCells with SettingsChange does + // if (utils.searchPreferences.search.liveUpdate) + // utils.searchCells({ // key: 'SettingsChange', // }).catch(softFail); }} @@ -229,7 +233,7 @@ export function WbUtilsComponent({ } /* eslint-disable functional/no-this-expression */ -export class WbUtilsReact { +export class WbUtils { // eslint-disable-next-line functional/prefer-readonly-type public searchQuery: RegExp | string | undefined = undefined; @@ -239,49 +243,38 @@ export class WbUtilsReact { // eslint-disable-next-line functional/prefer-readonly-type public searchPreferences: WbSearchPreferences = getInitialSearchPreferences(); - constructor(private readonly workbench: Workbench) { - const debounced = _.debounce( - this.searchCells, - Math.ceil(clamp(10, this.workbench.data.length / 20, 200)), - false - ).bind(this); - // Workaround for _.debounce not working with async functions - // this.searchCells = async (event): Promise => debounced(event, null); - } + constructor(private readonly workbench: Workbench) {} - navigateCells( - type: keyof WbCellCounts | null, - direction: 'previous' | 'next', - // If true and current cell is of correct type, don't navigate away + navigateCells({ + type, + direction, + currentCellPosition = 0, + totalCount = 0, matchCurrentCell = false, + currentCell = undefined, + }: { + type: keyof WbCellCounts; + direction: 'previous' | 'next'; + currentCellPosition?: number; + totalCount?: number; + // If true and current cell is of correct type, don't navigate away + matchCurrentCell?: boolean; /* * Overwrite what is considered to be a current cell * Setting to [0,0] and matchCurrentCell=true allows navigation to the first * cell of type (used on hitting "Enter" in the Search Box) */ - currentCell: readonly [number, number] | undefined = undefined - ): - | { - readonly visualRow: number; - readonly visualCol: number; - } - | undefined { - /* - * Can get data-* via button.dataset, but this way is better as can find - * usages this way easily (the button.dataset converts keys to camelCase) - */ - - if (type === null) return undefined; - // const currentPositionElement = buttonParent.getElementsByClassName( - // 'wb-navigation-position' - // )[0]; - // const totalCountElement = buttonParent.getElementsByClassName( - // 'wb-navigation-total' - // )[0]; - const totalCount = 0; - - const cellMetaObject = this.workbench.cells!.getCellMetaObject(); - + currentCell?: readonly [number, number] | undefined; + }): + | [ + { + readonly visualRow: number; + readonly visualCol: number; + }, + number + ] + | [undefined, number] { + const cellMetaObject = this.workbench.cells.getCellMetaObject(); /* * The cellMetaObject is transposed if navigation direction is "Column * first". @@ -372,32 +365,30 @@ export class WbUtilsReact { const boundaryCell = direction === 'next' ? totalCount : 1; - // const initialCellRelativePosition = Number.parseInt( - // currentPositionElement.textContent ?? '0' - // ); - const initialCellRelativePosition = 0; - // if ( - // cellRelativePosition !== 0 || - // initialCellRelativePosition !== boundaryCell || - // totalCount === 0 - // ) - // currentPositionElement.textContent = cellRelativePosition.toString(); + let finalCellPosition = currentCellPosition; + if ( + cellRelativePosition !== 0 || + currentCellPosition !== boundaryCell || + totalCount === 0 + ) + finalCellPosition = cellRelativePosition; - if (matchedCell === undefined) return undefined; + if (matchedCell === undefined) return [undefined, finalCellPosition]; this.workbench.hot.selectCell(matchedCell.visualRow, matchedCell.visualCol); // Turn on the respective cell type if it was hidden + // TODO: figure out if this is needed // this.toggleCellTypes(event, 'remove'); - return matchedCell; + return [matchedCell, finalCellPosition]; } searchCells( event: KeyboardEvent | { readonly key: 'SettingsChange' }, searchQueryElement: HTMLInputElement, spreadsheetContainer: any - ){ + ) { if (!this.workbench.hot) return; /* * Don't rerun search on live search if search query did not change @@ -420,9 +411,6 @@ export class WbUtilsReact { } this.toggleCellTypes('searchResults', 'remove', spreadsheetContainer); - // const navigationButton = - // navigationContainer.getElementsByClassName('wb-cell-navigation'); - let resultsCount = 0; const data = this.workbench.dataset.rows; const firstVisibleRow = @@ -430,11 +418,15 @@ export class WbUtilsReact { const lastVisibleRow = getHotPlugin(this.workbench.hot, 'autoRowSize').getLastVisibleRow() + 3; const firstVisibleColumn = - getHotPlugin(this.workbench.hot, 'autoColumnSize').getFirstVisibleColumn() - - 3; + getHotPlugin( + this.workbench.hot, + 'autoColumnSize' + ).getFirstVisibleColumn() - 3; const lastVisibleColumn = - getHotPlugin(this.workbench.hot, 'autoColumnSize').getLastVisibleColumn() + - 3; + getHotPlugin( + this.workbench.hot, + 'autoColumnSize' + ).getLastVisibleColumn() + 3; for (let visualRow = 0; visualRow < data.length; visualRow++) { const physicalRow = this.workbench.hot.toPhysicalRow(visualRow); @@ -490,12 +482,12 @@ export class WbUtilsReact { // Navigate to the first search result when hitting Enter if (event.key === 'Enter') - this.navigateCells( - "searchResults", - "next", - event.key === 'Enter', - event.key === 'Enter' ? [0, 0] : undefined - ); + this.navigateCells({ + type: 'searchResults', + direction: 'next', + matchCurrentCell: event.key === 'Enter', + currentCell: event.key === 'Enter' ? [0, 0] : undefined, + }); } parseSearchQuery(searchQueryElement: HTMLInputElement) { @@ -516,9 +508,9 @@ export class WbUtilsReact { try { if (this.searchPreferences.search.fullMatch) { if (!this.searchQuery.startsWith('^')) - this.searchQuery = `^${this.searchQuery}`; + this.searchQuery = `^${this.searchQuery}`; if (!this.searchQuery.endsWith('$')) - this.searchQuery = `${this.searchQuery}$`; + this.searchQuery = `${this.searchQuery}$`; } // Regex may be coming from the user, thus disable strict mode @@ -543,12 +535,13 @@ export class WbUtilsReact { toggleCellTypes( navigationType: keyof WbCellCounts, action: 'add' | 'remove' | 'toggle' = 'toggle', - spreadsheetContainer?: any, + spreadsheetContainer?: any ): void { const groupName = camelToKebab(navigationType); const cssClassName = `wb-hide-${groupName}`; - if (spreadsheetContainer) spreadsheetContainer.classList[action](cssClassName); - } + if (spreadsheetContainer) + spreadsheetContainer.classList[action](cssClassName); + } searchFunction(initialCellValue = '') { let cellValue = initialCellValue; @@ -566,7 +559,10 @@ export class WbUtilsReact { : cellValue.includes(this.searchQuery as string); } - replaceCells(event: KeyboardEvent, replacementValueElement: HTMLInputElement) { + replaceCells( + event: KeyboardEvent, + replacementValueElement: HTMLInputElement + ) { if ( event.key !== 'Enter' || (this.searchPreferences.search.useRegex && @@ -575,10 +571,6 @@ export class WbUtilsReact { ) return; - // const buttonContainer = button.parentElement; - // const replacementValueElement = buttonContainer?.getElementsByClassName( - // 'wb-replace-value' - // )[0] as HTMLInputElement | undefined; if (replacementValueElement === undefined) return; const replacementValue = this.searchPreferences.search.useRegex ? replacementValueElement.value @@ -629,11 +621,11 @@ export class WbUtilsReact { this.workbench.hot.setDataAtCell(modifications); } else { const nextCellOfType = () => - this.navigateCells( - "searchResults", - "next", - false - ); + this.navigateCells({ + type: 'searchResults', + direction: 'next', + matchCurrentCell: false, + }); const [currentRow, currentCol] = getSelectedLast(this.workbench.hot); const physicalRow = this.workbench.hot.toPhysicalRow(currentRow); const physicalCol = this.workbench.hot.toPhysicalColumn(currentCol); @@ -644,7 +636,7 @@ export class WbUtilsReact { 'searchResults' ) ) { - const next = nextCellOfType(); + const [next, _] = nextCellOfType(); if (typeof next === 'object') { const { visualRow, visualCol } = next; nextCell = [visualRow, visualCol]; @@ -662,748 +654,3 @@ export class WbUtilsReact { } } } -/* eslint-enable functional/no-this-expression */ - -// REFACTOR: rewrite to React -/* eslint-disable functional/no-this-expression */ -export class WbUtils extends Backbone.View { - // eslint-disable-next-line functional/prefer-readonly-type - public searchQuery: RegExp | string | undefined = undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - private rawSearchQuery: string | undefined = undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - public searchPreferences: WbSearchPreferences = getInitialSearchPreferences(); - - // eslint-disable-next-line functional/prefer-readonly-type - private geoLocateDialog: (() => void) | undefined = undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - private advancedSearch: (() => void) | undefined = undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - private geoMapDialog: (() => void) | undefined = undefined; - - constructor(private readonly wbView: WbView) { - super({ - el: wbView.el, - events: { - 'click .wb-cell-navigation': 'navigateCells', - 'click .wb-navigation-text': 'toggleCellTypes', - 'keydown .wb-search-query': 'searchCells', - 'keydown .wb-replace-value': 'replaceCells', - 'click .wb-show-toolkit': 'toggleToolkit', - 'click .wb-geolocate': 'showGeoLocate', - 'click .wb-leafletmap': 'showLeafletMap', - 'click .wb-convert-coordinates': 'showCoordinateConversion', - }, - }); - - const debounced = _.debounce( - this.searchCells, - Math.ceil(clamp(10, this.wbView.data.length / 20, 200)), - false - ).bind(this); - // Workaround for _.debounce not working with async functions - this.searchCells = async (event): Promise => debounced(event); - } - - render() { - let initialNavigationDirection = - this.searchPreferences.navigation.direction; - this.advancedSearch = this.wbView.options.display( - { - this.searchPreferences = newSearchPreferences; - if ( - this.searchPreferences.navigation.direction !== - initialNavigationDirection - ) { - this.wbView.cells.flushIndexedCellData = true; - initialNavigationDirection = - this.searchPreferences.navigation.direction; - } - if (this.searchPreferences.search.liveUpdate) - this.searchCells({ - key: 'SettingsChange', - }).catch(softFail); - }} - />, - this.el.getElementsByClassName( - 'wb-advanced-search-wrapper' - )[0] as HTMLElement - ); - - return this; - } - - remove(): this { - this.advancedSearch?.(); - Backbone.View.prototype.remove.call(this); - return this; - } - - navigateCells( - event: { readonly target: Element }, - // If true and current cell is of correct type, don't navigate away - matchCurrentCell = false, - /* - * Overwrite what is considered to be a current cell - * Setting to [0,0] and matchCurrentCell=true allows navigation to the first - * cell of type (used on hitting "Enter" in the Search Box) - */ - currentCell: readonly [number, number] | undefined = undefined - ): - | { - readonly visualRow: number; - readonly visualCol: number; - } - | undefined { - const button = event.target as HTMLButtonElement | null; - if (this.wbView.hot === undefined || button === null) return undefined; - - /* - * Can get data-* via button.dataset, but this way is better as can find - * usages this way easily (the button.dataset converts keys to camelCase) - */ - const direction = button.getAttribute('data-navigation-direction'); - const buttonParent = button.parentElement; - if (buttonParent === null) return undefined; - const type = buttonParent.getAttribute('data-navigation-type') as - | keyof WbCellCounts - | null; - if (type === null) return undefined; - const currentPositionElement = buttonParent.getElementsByClassName( - 'wb-navigation-position' - )[0]; - const totalCountElement = buttonParent.getElementsByClassName( - 'wb-navigation-total' - )[0]; - const totalCount = Number.parseInt(totalCountElement.textContent ?? '0'); - - const cellMetaObject = this.wbView.cells.getCellMetaObject(); - - /* - * The cellMetaObject is transposed if navigation direction is "Column - * first". - * In that case, the meaning of visualRow and visualCol is swapped. - * resolveIndex exists to resolve the canonical visualRow/visualCol - */ - const resolveIndex = ( - visualRow: number, - visualCol: number, - first: boolean - ): number => - (this.searchPreferences.navigation.direction === 'rowFirst') === first - ? visualRow - : visualCol; - - const [currentRow, currentCol] = - currentCell ?? getSelectedLast(this.wbView.hot); - - const [currentTransposedRow, currentTransposedCol] = [ - resolveIndex(currentRow, currentCol, true), - resolveIndex(currentRow, currentCol, false), - ]; - - const compareRows = - direction === 'next' - ? (visualRow: number) => visualRow >= currentTransposedRow - : (visualRow: number) => visualRow <= currentTransposedRow; - - const compareCols = - direction === 'next' - ? matchCurrentCell - ? (visualCol: number) => visualCol >= currentTransposedCol - : (visualCol: number) => visualCol > currentTransposedCol - : matchCurrentCell - ? (visualCol: number) => visualCol <= currentTransposedCol - : (visualCol: number) => visualCol < currentTransposedCol; - - let matchedCell: - | { - readonly visualRow: number; - readonly visualCol: number; - } - | undefined; - let cellIsTypeCount = 0; - - const orderIt = - direction === 'next' - ? f.id - : (array: RA): RA => Array.from(array).reverse(); - - orderIt(Object.entries(cellMetaObject)).find(([visualRowString, metaRow]) => - typeof metaRow === 'object' - ? orderIt(Object.entries(metaRow)).find( - ([visualColString, metaArray]) => { - /* - * This is 10 times faster then Number.parseInt because of a slow - * Babel polyfill - */ - const visualRow = (visualRowString as unknown as number) | 0; - const visualCol = (visualColString as unknown as number) | 0; - - const cellTypeMatches = this.wbView.cells.cellIsType( - metaArray, - type - ); - cellIsTypeCount += cellTypeMatches ? 1 : 0; - - const isWithinBounds = - compareRows(visualRow) && - (visualRow !== currentTransposedRow || compareCols(visualCol)); - - const matches = cellTypeMatches && isWithinBounds; - if (matches) - matchedCell = { - visualRow: resolveIndex(visualRow, visualCol, true), - visualCol: resolveIndex(visualRow, visualCol, false), - }; - return matches; - } - ) - : undefined - ); - - let cellRelativePosition; - if (matchedCell === undefined) cellRelativePosition = 0; - else if (direction === 'next') cellRelativePosition = cellIsTypeCount; - else cellRelativePosition = totalCount - cellIsTypeCount + 1; - - const boundaryCell = direction === 'next' ? totalCount : 1; - - const initialCellRelativePosition = Number.parseInt( - currentPositionElement.textContent ?? '0' - ); - if ( - cellRelativePosition !== 0 || - initialCellRelativePosition !== boundaryCell || - totalCount === 0 - ) - currentPositionElement.textContent = cellRelativePosition.toString(); - - if (matchedCell === undefined) return undefined; - - this.wbView.hot.selectCell(matchedCell.visualRow, matchedCell.visualCol); - - // Turn on the respective cell type if it was hidden - this.toggleCellTypes(event, 'remove'); - - return matchedCell; - } - - toggleCellTypes( - event: keyof WbCellCounts | { readonly target: Element }, - action: 'add' | 'remove' | 'toggle' = 'toggle' - ): boolean { - console.log(event, 'toggle event'); - let navigationType: keyof WbCellCounts | undefined; - let buttonContainer: HTMLElement | undefined; - if (typeof event === 'string') { - navigationType = event; - buttonContainer = - this.wbView.el.querySelector( - `.wb-navigation-section[data-navigation-type="${navigationType}"]` - ) ?? undefined; - } else { - const button = event.target as HTMLButtonElement | null; - buttonContainer = button?.closest('.wb-navigation-section') ?? undefined; - navigationType = (buttonContainer?.getAttribute('data-navigation-type') ?? - undefined) as keyof WbCellCounts | undefined; - } - if (buttonContainer === undefined || navigationType === undefined) - return false; - - const groupName = camelToKebab(navigationType); - const cssClassName = `wb-hide-${groupName}`; - this.el.classList[action](cssClassName); - const newState = this.el.classList.contains(cssClassName); - const indicator = - buttonContainer.getElementsByClassName('wb-navigation-text')[0]; - indicator.setAttribute('aria-pressed', newState ? 'true' : 'false'); - indicator.classList[newState ? 'add' : 'remove']('brightness-50'); - return newState; - } - - parseSearchQuery() { - const searchQueryElement = - this.el.querySelector('.wb-search-query'); - if (searchQueryElement === null) return; - - this.rawSearchQuery = searchQueryElement.value; - - this.searchQuery = this.searchPreferences.search.useRegex - ? this.rawSearchQuery - : this.rawSearchQuery.trim(); - - if (this.searchQuery === '') { - this.searchQuery = undefined; - return; - } - - if (this.searchPreferences.search.useRegex) - try { - if (this.searchPreferences.search.fullMatch) { - if (!this.searchQuery.startsWith('^')) - this.searchQuery = `^${this.searchQuery}`; - if (!this.searchQuery.endsWith('$')) - this.searchQuery = `${this.searchQuery}$`; - } - // Regex may be coming from the user, thus disable strict mode - - this.searchQuery = new RegExp( - this.searchQuery, - this.searchPreferences.search.caseSensitive ? '' : 'i' - ); - } catch (error) { - searchQueryElement.setCustomValidity((error as SyntaxError).message); - searchQueryElement.reportValidity(); - this.searchQuery = undefined; - return; - } - else if (!this.searchPreferences.search.caseSensitive) - this.searchQuery = this.searchQuery.toLowerCase(); - - searchQueryElement.setCustomValidity(''); - - return this.searchQuery; - } - - async searchCells( - event: KeyboardEvent | { readonly key: 'SettingsChange' } - ): Promise { - if (this.wbView.hot === undefined) return; - const searchQueryElement = this.el.getElementsByClassName( - 'wb-search-query' - )[0] as HTMLInputElement; - - /* - * Don't rerun search on live search if search query did not change - * (e.x, if Ctrl/Cmd+A is clicked in the search box) - */ - if ( - searchQueryElement.value === this.rawSearchQuery && - !['SettingsChange', 'Enter'].includes(event.key) - ) - return; - - // Don't handle onKeyDown event if live search is disabled - if (event.key !== 'Enter' && !this.searchPreferences.search.liveUpdate) - return; - - const navigationContainer = - this.el.querySelector( - '.wb-navigation-section[data-navigation-type="searchResults"]' - ) ?? undefined; - const navigationTotalElement = navigationContainer?.getElementsByClassName( - 'wb-navigation-total' - )[0]; - - if ( - navigationContainer === undefined || - navigationTotalElement === undefined - ) - return; - if (this.parseSearchQuery() === undefined) { - navigationTotalElement.textContent = '0'; - this.toggleCellTypes('searchResults', 'add'); - return; - } - this.toggleCellTypes('searchResults', 'remove'); - - const navigationButton = - navigationContainer.getElementsByClassName('wb-cell-navigation'); - - let resultsCount = 0; - const data = this.wbView.dataset.rows; - const firstVisibleRow = - getHotPlugin(this.wbView.hot, 'autoRowSize').getFirstVisibleRow() - 3; - const lastVisibleRow = - getHotPlugin(this.wbView.hot, 'autoRowSize').getLastVisibleRow() + 3; - const firstVisibleColumn = - getHotPlugin(this.wbView.hot, 'autoColumnSize').getFirstVisibleColumn() - - 3; - const lastVisibleColumn = - getHotPlugin(this.wbView.hot, 'autoColumnSize').getLastVisibleColumn() + - 3; - - for (let visualRow = 0; visualRow < this.wbView.data.length; visualRow++) { - const physicalRow = this.wbView.hot.toPhysicalRow(visualRow); - for ( - let visualCol = 0; - visualCol < this.wbView.dataset.columns.length; - visualCol++ - ) { - const physicalCol = this.wbView.hot.toPhysicalColumn(visualCol); - const isSearchResult = this.searchFunction( - (data[physicalRow][physicalCol] || - this.wbView.mappings?.defaultValues[physicalCol]) ?? - '' - ); - - let cell = undefined; - let render = false; - - /* - * Calling this.wbView.hot.getCell only if cell is within the render - * bounds. - * While hot.getCell is supposed to check for this too, doing it this - * way makes search about 25% faster - */ - if ( - firstVisibleRow <= visualRow && - lastVisibleRow >= visualRow && - firstVisibleColumn <= visualCol && - lastVisibleColumn >= visualCol - ) { - cell = this.wbView.hot.getCell(visualRow, visualCol) ?? undefined; - render = Boolean(cell); - } - - this.wbView.cells[render ? 'updateCellMeta' : 'setCellMeta']( - physicalRow, - physicalCol, - 'isSearchResult', - isSearchResult, - { - cell, - visualRow, - visualCol, - } - ); - if (isSearchResult) resultsCount += 1; - } - } - - navigationTotalElement.textContent = resultsCount.toString(); - - // Navigate to the first search result when hitting Enter - if (event.key === 'Enter') - this.navigateCells( - { target: navigationButton[1] }, - event.key === 'Enter', - event.key === 'Enter' ? [0, 0] : undefined - ); - } - - replaceCells(event: KeyboardEvent) { - const button = event.target as HTMLButtonElement | null; - if ( - event.key !== 'Enter' || - button === null || - (this.searchPreferences.search.useRegex && - this.searchQuery === undefined) || - this.wbView.hot === undefined - ) - return; - - const buttonContainer = button.parentElement; - const replacementValueElement = buttonContainer?.getElementsByClassName( - 'wb-replace-value' - )[0] as HTMLInputElement | undefined; - if (replacementValueElement === undefined) return; - const replacementValue = this.searchPreferences.search.useRegex - ? replacementValueElement.value - : replacementValueElement.value.trim(); - - const getNewCellValue = this.searchPreferences.search.fullMatch - ? (): string => replacementValue - : (cellValue: string): string => - this.searchPreferences.search.useRegex - ? cellValue.replaceAll( - // Regex may be coming from the user, thus disable strict mode - // eslint-disable-next-line require-unicode-regexp - new RegExp(this.searchQuery!, 'g'), - replacementValue - ) - : cellValue.split(this.searchQuery ?? '').join(replacementValue); - - if (this.searchPreferences.replace.replaceMode === 'replaceAll') { - // eslint-disable-next-line functional/prefer-readonly-type - const modifications: WritableArray<[number, number, string]> = []; - Object.entries(this.wbView.cells.cellMeta).forEach( - ([physicalRow, metaRow]) => - Object.entries(metaRow).forEach(([physicalCol, metaArray]) => { - if ( - !this.wbView.cells.getCellMetaFromArray( - metaArray, - 'isSearchResult' - ) - ) - return; - const visualRow = this.wbView.hot!.toVisualRow( - (physicalRow as unknown as number) | 0 - ); - const visualCol = this.wbView.hot!.toVisualColumn( - (physicalCol as unknown as number) | 0 - ); - const cellValue = - this.wbView.hot!.getDataAtCell(visualRow, visualCol) || ''; - // Don't replace cells with default values - if (cellValue === '') return; - modifications.push([ - visualRow, - visualCol, - getNewCellValue(cellValue), - ]); - }) - ); - this.wbView.hot.setDataAtCell(modifications); - } else { - const nextCellOfType = () => - this.navigateCells( - { - target: document.querySelector( - `.wb-navigation-section[data-navigation-type="searchResults"] - .wb-cell-navigation[data-navigation-direction="next"]` - )!, - }, - false - ); - const [currentRow, currentCol] = getSelectedLast(this.wbView.hot); - const physicalRow = this.wbView.hot.toPhysicalRow(currentRow); - const physicalCol = this.wbView.hot.toPhysicalColumn(currentCol); - let nextCell = [currentRow, currentCol] as const; - if ( - !this.wbView.cells.cellIsType( - this.wbView.cells.cellMeta[physicalRow]?.[physicalCol], - 'searchResults' - ) - ) { - const next = nextCellOfType(); - if (typeof next === 'object') { - const { visualRow, visualCol } = next; - nextCell = [visualRow, visualCol]; - } - } - - if (!Array.isArray(nextCell)) return; - - this.wbView.hot.setDataAtCell( - ...nextCell, - getNewCellValue(this.wbView.hot.getDataAtCell(...nextCell)) - ); - - nextCellOfType(); - } - } - - searchFunction(initialCellValue = '') { - let cellValue = initialCellValue; - - if (this.searchQuery === undefined) return false; - - if (!this.searchPreferences.search.caseSensitive) - cellValue = cellValue.toLowerCase(); - - if (this.searchPreferences.search.useRegex) - return cellValue.search(this.searchQuery) !== -1; - - return this.searchPreferences.search.fullMatch - ? cellValue === this.searchQuery - : cellValue.includes(this.searchQuery as string); - } - - toggleToolkit(event: MouseEvent): void { - const toolkit = this.el.getElementsByClassName( - 'wb-toolkit' - )[0] as HTMLElement; - const target = event.target as HTMLElement | null; - target?.toggleAttribute('aria-pressed', toolkit.style.display === 'none'); - toolkit.style.display = toolkit.style.display === 'none' ? '' : 'none'; - this.wbView.handleResize(); - } - - // Context menu item definitions (common for fillUp and fillDown) - fillCellsContextMenuItem( - mode: 'down' | 'up' - ): Handsontable.plugins.ContextMenu.MenuItemConfig { - return { - name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), - disabled: () => - typeof this.wbView.uploadedView === 'function' || - typeof this.wbView.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update') || - (this.wbView.hot - ?.getSelected() - ?.every((selection) => selection[0] === selection[2]) ?? - false), - callback: (_: any, selections: any) => - selections.forEach((selection: any) => - Array.from( - new Array(selection.end.col + 1 - selection.start.col).keys() - ).forEach((index) => { - const startRow = - mode === 'up' ? selection.start.row + 1 : selection.start.row; - const endRow = selection.end.row; - const col = selection.start.col + index; - const value = - mode === 'up' - ? this.wbView.hot!.getDataAtCell(endRow, col) - : this.wbView.hot!.getDataAtCell(startRow, col); - this.wbView.hot?.setDataAtCell( - Array.from({ length: endRow - startRow }, (_, index) => [ - startRow + index + 1, - col, - value, - ]) - ); - }) - ), - }; - } - - public findLocalityColumns(): void { - const leafletButton = this.el.getElementsByClassName( - 'wb-leafletmap' - )[0] as HTMLButtonElement; - // These buttons only exist if user has data set update permission - const geoLocaleButton = this.el.getElementsByClassName( - 'wb-geolocate' - )[0] as HTMLButtonElement; - const coordinateConverterButton = this.el.getElementsByClassName( - 'wb-convert-coordinates' - )[0] as HTMLButtonElement; - - const localityColumns = this.wbView.mappings?.localityColumns ?? []; - if (localityColumns.length === 0) return; - leafletButton.disabled = false; - if (this.wbView.isUploaded) - filterArray([geoLocaleButton, coordinateConverterButton]).map((button) => - button.setAttribute('title', wbText.unavailableWhenUploaded()) - ); - else { - if (typeof geoLocaleButton === 'object') geoLocaleButton.disabled = false; - if (typeof coordinateConverterButton === 'object') - coordinateConverterButton.disabled = false; - } - } - - protected showGeoLocate(event: MouseEvent): void { - if (this.wbView.hot === undefined || this.wbView.mappings === undefined) - return; - const target = event.target as HTMLElement; - // Don't allow opening more than one window) - if (this.geoLocateDialog !== undefined) { - this.geoLocateDialog(); - return; - } - - this.geoLocateDialog = this.wbView.options.display( - this.geoLocateDialog?.()} - />, - undefined, - () => { - target.toggleAttribute('aria-pressed', false); - this.geoLocateDialog = undefined; - } - ); - - target.toggleAttribute('aria-pressed', true); - } - - protected showLeafletMap(event: MouseEvent): void { - if (this.wbView.hot === undefined || this.wbView.mappings === undefined) - return; - if (this.geoMapDialog !== undefined) { - this.geoMapDialog(); - return; - } - const target = event.target as HTMLElement; - target.toggleAttribute('aria-pressed', true); - - const selection = getSelectedLocalities( - this.wbView.hot, - this.wbView.dataset.columns, - this.wbView.mappings.localityColumns, - false - ); - - if (selection === undefined) return; - - const localityPoints = getLocalitiesDataFromSpreadsheet( - this.wbView.mappings.localityColumns, - selection.visualRows.map((visualRow) => - this.wbView.hot!.getDataAtRow(visualRow) - ), - getVisualHeaders(this.wbView.hot, this.wbView.dataset.columns), - selection.visualRows - ); - - this.geoMapDialog = this.wbView.options.display( - this.geoMapDialog?.()} - onMarkerClick={(localityPoint): void => { - const rowNumber = localityPoints[localityPoint].rowNumber.value; - if (typeof rowNumber !== 'number') - throw new Error('rowNumber must be a number'); - const [_currentRow, currentCol] = getSelectedLast(this.wbView.hot!); - this.wbView.hot?.scrollViewportTo(rowNumber, currentCol); - // Select entire row - this.wbView.hot?.selectRows(rowNumber); - }} - />, - undefined, - () => { - this.geoMapDialog = undefined; - target.toggleAttribute('aria-pressed', false); - } - ); - } - - protected showCoordinateConversion(): void { - if ( - this.wbView.coordinateConverterView !== undefined || - this.wbView.mappings === undefined || - this.wbView.hot === undefined - ) - return; - - const buttons = [ - 'wb-leafletmap', - 'wb-geolocate', - 'wb-convert-coordinates', - 'wb-replace-value', - ] - .map( - (className) => - this.el.getElementsByClassName(className)[0] as HTMLButtonElement - ) - .map((button) => [button, button.disabled] as const); - const originalReadOnlyState = this.wbView.hot.getSettings().readOnly; - this.wbView.hot.updateSettings({ - readOnly: true, - }); - this.el.classList.add('wb-focus-coordinates'); - - this.wbView.coordinateConverterView = this.wbView.options.display( - this.wbView.coordinateConverterView?.()} - />, - undefined, - () => { - this.wbView.coordinateConverterView = undefined; - buttons.forEach(([button, isDisabled]) => { - button.disabled = isDisabled; - }); - this.wbView.hot?.updateSettings({ - readOnly: originalReadOnlyState, - }); - this.el.classList.remove('wb-focus-coordinates'); - } - ); - } -} -/* eslint-enable functional/no-this-expression */ diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 2deb0907ac4..ac67d623cde 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -50,14 +50,12 @@ import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; import { wbViewTemplate } from './Template'; import { WbActionsComponent } from '../WbActions/WbActions'; -import { WbUtils, WbUtilsComponent, WbUtilsReact } from './WbUtils'; +import { WbUtils, WbUtilsComponent } from './WbUtils'; import { WbValidation, WbValidationReact } from './WbValidation'; import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; import { WbToolkit } from '../WbToolkit/WbToolkit'; -import { getInitialSearchPreferences } from './AdvancedSearch'; -import type { WbSearchPreferences } from './AdvancedSearch'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -67,11 +65,10 @@ export type Workbench = { disambiguation: DisambiguationReact; validation: WbValidationReact; data: RA>; - searchPreferences: WbSearchPreferences; hot: Handsontable; throttleRate: number; mappings: WbMapping; - utils: WbUtilsReact; + utils: WbUtils; cellCounts: GetSet; }; @@ -106,7 +103,6 @@ export function WbViewReact({ [dataset] ); - const searchPreferences = getInitialSearchPreferences(); const throttleRate = Math.ceil(clamp(10, data.length / 10, 2000)); const [hasUnsavedChanges, spreadsheetChanged, spreadsheetUpToDate] = @@ -150,8 +146,7 @@ export function WbViewReact({ data, dataset, hot: hotRef.current?.hotInstance!, - mappings: mappings as WbMapping, - searchPreferences, + mappings: mappings!, throttleRate, cells: undefined!, disambiguation: undefined!, @@ -162,7 +157,7 @@ export function WbViewReact({ workbench.cells = new WbCellMetaReact(workbench); workbench.disambiguation = new DisambiguationReact(workbench); workbench.validation = new WbValidationReact(workbench); - workbench.utils = new WbUtilsReact(workbench); + workbench.utils = new WbUtils(workbench); return workbench; }, [dataset, hotIsReady]); @@ -341,7 +336,6 @@ export function WbViewReact({
Date: Thu, 4 Apr 2024 15:46:30 -0400 Subject: [PATCH 043/153] Remove old CellMeta code --- .../lib/components/WorkBench/CellMeta.ts | 389 +----------------- .../lib/components/WorkBench/WbView.tsx | 6 +- 2 files changed, 10 insertions(+), 385 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index 4c1d1dabaa5..d97131f128f 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -1,9 +1,8 @@ import { backEndText } from '../../localization/backEnd'; -import { wbText } from '../../localization/workbench'; import type { RA, WritableArray } from '../../utils/types'; import { throttle } from '../../utils/utils'; import { getHotPlugin } from './handsontable'; -import type { WbView, Workbench } from './WbView'; +import type { Workbench } from './WbView'; const metaKeys = [ 'isNew', @@ -47,7 +46,7 @@ const defaultMetaValues = Object.freeze([ ] as const); /* eslint-disable functional/no-this-expression */ -export class WbCellMetaReact { +export class WbCellMeta { // Meta data for each cell (indexed by physical columns) // eslint-disable-next-line functional/prefer-readonly-type public cellMeta: WritableArray> = []; @@ -59,9 +58,7 @@ export class WbCellMetaReact { // eslint-disable-next-line functional/prefer-readonly-type public flushIndexedCellData: boolean = true; - public constructor( - private readonly workbench: Workbench - ) { + public constructor(private readonly workbench: Workbench) { this.updateCellInfoStats = throttle( this.updateCellInfoStats.bind(this), this.workbench.throttleRate @@ -269,7 +266,8 @@ export class WbCellMetaReact { initialVisualRow ?? this.workbench.hot.toVisualRow(physicalRow); const visualCol = initialVisualCol ?? this.workbench.hot.toVisualColumn(physicalCol); - const cell = initialCell ?? this.workbench.hot.getCell(visualRow, visualCol); + const cell = + initialCell ?? this.workbench.hot.getCell(visualRow, visualCol); this.runMetaUpdateEffects( cell ?? undefined, key, @@ -301,7 +299,7 @@ export class WbCellMetaReact { visualCol: number, first: boolean ) => - (this.workbench.searchPreferences.navigation.direction === + (this.workbench.utils.searchPreferences.navigation.direction === 'rowFirst') === first ? visualRow @@ -355,384 +353,11 @@ export class WbCellMetaReact { 0 ), }; - + const [_, setCellCounts] = this.workbench.cellCounts; setCellCounts(cellCounts); } -public cellIsType(metaArray: WbMetaArray, type: keyof WbCellCounts): boolean { - switch (type) { - case 'invalidCells': { - return this.getCellMetaFromArray(metaArray, 'issues').length > 0; - } - case 'newCells': { - return this.getCellMetaFromArray(metaArray, 'isNew'); - } - case 'modifiedCells': { - return this.getCellMetaFromArray(metaArray, 'isModified'); - } - case 'searchResults': { - return this.getCellMetaFromArray(metaArray, 'isSearchResult'); - } - default: { - return false; - } - } - } -} -/* eslint-enable functional/no-this-expression */ - - -/* eslint-disable functional/no-this-expression */ -export class WbCellMeta { - // Meta data for each cell (indexed by physical columns) - // eslint-disable-next-line functional/prefer-readonly-type - public cellMeta: WritableArray> = []; - - // Meta data for each cell (indexed by visual columns) - // eslint-disable-next-line functional/prefer-readonly-type - private indexedCellMeta: RA> | undefined = undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - public flushIndexedCellData: boolean = true; - - public constructor(private readonly wbView: WbView) { - this.updateCellInfoStats = throttle( - this.updateCellInfoStats.bind(this), - this.wbView.throttleRate - ); - } - - getCellMeta( - physicalRow: number, - physicalCol: number, - key: KEY - ): WbMeta[KEY] { - const index = metaKeys.indexOf(key); - return (this.cellMeta[physicalRow]?.[physicalCol]?.[index] ?? - defaultMetaValues[index]) as unknown as WbMeta[KEY]; - } - - getCellMetaFromArray( - metaCell: WbMetaArray, - key: KEY - ): WbMeta[KEY] { - const index = metaKeys.indexOf(key); - return (metaCell?.[index] ?? defaultMetaValues[index]) as WbMeta[KEY]; - } - - /* - * This does not run visual side effects - * For changing meta with side effects, use this.updateCellMeta - */ - setCellMeta( - physicalRow: number, - physicalCol: number, - key: KEY, - value: WbMeta[KEY] - ) { - const currentValue = this.getCellMeta(physicalRow, physicalCol, key); - - const issuesChanged = - key === 'issues' && - ((currentValue as RA).length !== (value as RA).length || - JSON.stringify(currentValue) !== JSON.stringify(value)); - const cellValueChanged = key === 'originalValue'; - const metaValueChanged = - issuesChanged || - cellValueChanged || - (['isNew', 'isModified', 'isSearchResult'].includes(key) && - currentValue !== value); - - if (!metaValueChanged) return false; - - const index = metaKeys.indexOf(key); - this.cellMeta[physicalRow] ??= []; - this.cellMeta[physicalRow][physicalCol] ??= Array.from( - defaultMetaValues - ) as unknown as WbMetaArray; - this.cellMeta[physicalRow][physicalCol][index] = value; - - this.flushIndexedCellData = true; - - return true; - } - - /** - * Figuring out if a cell was modified is more complicated then it might seem - */ - private isCellModified(physicalRow: number, physicalCol: number): boolean { - // For now, only readOnly picklists are validated on the front-end - const hasFrontEndValidationErrors = this.getCellMeta( - physicalRow, - physicalCol, - 'issues' - ).some((issue) => - issue.endsWith(backEndText.failedParsingPickList({ value: '' })) - ); - if (hasFrontEndValidationErrors) - /* - * Since isModified state has higher priority then issues, we need to - * remove the isModified state if front end validation errors are to be - * visible - */ - return false; - - const originalCellValue = this.getCellMeta( - physicalRow, - physicalCol, - 'originalValue' - ); - const cellValueChanged = - originalCellValue !== undefined && - (originalCellValue?.toString() ?? '') !== - (this.wbView.data[physicalRow][physicalCol]?.toString() ?? ''); - if (cellValueChanged) return true; - - /* - * If cell was disambiguated, it should show up as changed, even if value - * is unchanged - */ - return this.wbView.disambiguation.cellWasDisambiguated( - physicalRow, - physicalCol - ); - } - - /** - * Updates cell's isModified meta state - */ - public recalculateIsModifiedState( - physicalRow: number, - physicalCol: number, - { - // Can optionally provide this to improve performance - visualRow = undefined, - // Can optionally provide this to improve performance - visualCol = undefined, - }: { - readonly visualRow?: number; - readonly visualCol?: number; - } = {} - ): void { - const isModified = this.isCellModified(physicalRow, physicalCol); - this.updateCellMeta(physicalRow, physicalCol, 'isModified', isModified, { - visualRow, - visualCol, - }); - } - - /** - * Note: "cell" is undefined when setting meta for off-screen cell. - * This is the case when rendering off-screen issues (which don't require a cell - * reference) - */ - runMetaUpdateEffects( - cell: HTMLTableCellElement | undefined, - key: KEY, - value: WbMeta[KEY], - visualRow: number, - visualCol: number - ) { - if (this.wbView.hot === undefined) return; - - if (key === 'isNew') - cell?.classList[value === true ? 'add' : 'remove']('wb-no-match-cell'); - else if (key === 'isModified') - cell?.classList[value === true ? 'add' : 'remove']('wb-modified-cell'); - else if (key === 'isSearchResult') - cell?.classList[value === true ? 'add' : 'remove']( - 'wb-search-match-cell' - ); - else if (key === 'issues') { - const issues = value as RA; - if (issues.length === 0) - getHotPlugin(this.wbView.hot, 'comments').removeCommentAtCell( - visualRow, - visualCol - ); - else { - getHotPlugin(this.wbView.hot, 'comments').setCommentAtCell( - visualRow, - visualCol, - issues.join('\n') - ); - getHotPlugin(this.wbView.hot, 'comments').updateCommentMeta( - visualRow, - visualCol, - { - readOnly: true, - } - ); - } - } else - throw new Error( - `Tried to set unknown metaData record ${key}=${JSON.stringify( - value - )} for cell - ${visualRow}x${visualCol}` - ); - } - - updateCellMeta( - physicalRow: number, - physicalCol: number, - key: KEY, - value: WbMeta[KEY], - { - // Can optionally provide this to improve performance - cell: initialCell = undefined, - // Can optionally provide this to improve performance - visualRow: initialVisualRow = undefined, - // Can optionally provide this to improve performance - visualCol: initialVisualCol = undefined, - }: { - readonly cell?: HTMLTableCellElement; - readonly visualRow?: number; - readonly visualCol?: number; - } = {} - ) { - if (this.wbView.hot === undefined) return; - const isValueChanged = this.setCellMeta( - physicalRow, - physicalCol, - key, - value - ); - if (!isValueChanged) return false; - - const visualRow = - initialVisualRow ?? this.wbView.hot.toVisualRow(physicalRow); - const visualCol = - initialVisualCol ?? this.wbView.hot.toVisualColumn(physicalCol); - const cell = initialCell ?? this.wbView.hot.getCell(visualRow, visualCol); - this.runMetaUpdateEffects( - cell ?? undefined, - key, - value, - visualRow, - visualCol - ); - - return true; - } - - /* - * Returns this.cellMeta, but instead of indexing by physical row/col, - * indexes by visual row/col - * - * This is used for navigation among cells. - * - * Also, if navigation direction is set to ColByCol, the resulting array - * is transposed. - * - * this.flushIndexedCellData is set to true whenever visual indexes change - * - */ - public getCellMetaObject(): RA> { - if (this.flushIndexedCellData || this.indexedCellMeta === undefined) { - if (this.wbView.hot === undefined) return []; - const resolveIndex = ( - visualRow: number, - visualCol: number, - first: boolean - ) => - (this.wbView.wbUtils.searchPreferences.navigation.direction === - 'rowFirst') === - first - ? visualRow - : visualCol; - - const indexedCellMeta: WritableArray> = []; - Object.entries(this.cellMeta).forEach(([physicalRow, metaRow]) => - Object.entries(metaRow).forEach(([physicalCol, cellMeta]) => { - const visualRow = this.wbView.hot!.toVisualRow( - (physicalRow as unknown as number) | 0 - ); - const visualCol = this.wbView.hot!.toVisualColumn( - (physicalCol as unknown as number) | 0 - ); - indexedCellMeta[resolveIndex(visualRow, visualCol, true)] ??= []; - indexedCellMeta[resolveIndex(visualRow, visualCol, true)][ - resolveIndex(visualRow, visualCol, false) - ] = cellMeta; - }) - ); - this.indexedCellMeta = indexedCellMeta; - } - this.flushIndexedCellData = false; - return this.indexedCellMeta; - } - - // MetaData - updateCellInfoStats() { - const cellMeta = this.cellMeta.flat(); - - const cellCounts: WbCellCounts = { - newCells: cellMeta.reduce( - (count, info) => - count + (this.getCellMetaFromArray(info, 'isNew') ? 1 : 0), - 0 - ), - invalidCells: cellMeta.reduce( - (count, info) => - count + - (this.getCellMetaFromArray(info, 'issues').length > 0 ? 1 : 0), - 0 - ), - searchResults: cellMeta.reduce( - (count, info) => - count + (this.getCellMetaFromArray(info, 'isSearchResult') ? 1 : 0), - 0 - ), - modifiedCells: cellMeta.reduce( - (count, info) => - count + (this.getCellMetaFromArray(info, 'isModified') ? 1 : 0), - 0 - ), - }; - - // Update navigation information - Object.values( - this.wbView.el.getElementsByClassName('wb-navigation-total') - ).forEach((navigationTotalElement) => { - const navigationContainer = navigationTotalElement.closest( - '.wb-navigation-section' - ); - if (navigationContainer === null) return; - const navigationType = navigationContainer.getAttribute( - 'data-navigation-type' - ) as keyof WbCellCounts | null; - if (navigationType === null) return; - navigationTotalElement.textContent = - cellCounts[navigationType]?.toString(); - - if (cellCounts[navigationType] === 0) { - const currentPositionElement = - navigationContainer.getElementsByClassName( - 'wb-navigation-position' - )?.[0]; - if (typeof currentPositionElement === 'object') - currentPositionElement.textContent = '0'; - } - }); - - const uploadButton = - this.wbView.el.querySelector('.wb-upload'); - if (uploadButton === null) return; - const title = wbText.uploadUnavailableWhileHasErrors(); - if ( - !uploadButton.disabled || - uploadButton.getAttribute('title') === title - ) { - const hasErrors = cellCounts.invalidCells > 0; - uploadButton.toggleAttribute('disabled', hasErrors); - uploadButton.setAttribute('title', hasErrors ? title : ''); - } - - this.wbView.actions.operationCompletedMessage(cellCounts); - } - public cellIsType(metaArray: WbMetaArray, type: keyof WbCellCounts): boolean { switch (type) { case 'invalidCells': { diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index ac67d623cde..b00bead75d5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -38,7 +38,7 @@ import { hasPermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { WbCellCounts, WbCellMeta, WbCellMetaReact } from './CellMeta'; +import { WbCellCounts, WbCellMeta } from './CellMeta'; import { Disambiguation, DisambiguationReact } from './DisambiguationLogic'; import { configureHandsontable, getHotPlugin } from './handsontable'; import { downloadDataSet } from './helpers'; @@ -61,7 +61,7 @@ export type WbStatus = 'unupload' | 'upload' | 'validate'; export type Workbench = { dataset: Dataset; - cells: WbCellMetaReact; + cells: WbCellMeta; disambiguation: DisambiguationReact; validation: WbValidationReact; data: RA>; @@ -154,7 +154,7 @@ export function WbViewReact({ utils: undefined!, cellCounts: [cellCounts, setCellCounts], }; - workbench.cells = new WbCellMetaReact(workbench); + workbench.cells = new WbCellMeta(workbench); workbench.disambiguation = new DisambiguationReact(workbench); workbench.validation = new WbValidationReact(workbench); workbench.utils = new WbUtils(workbench); From bbcd0ab91006dd8d3eccae4054141535f5e52a93 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:56:59 -0400 Subject: [PATCH 044/153] Remove old WbValidation code --- .../lib/components/WbActions/WbValidate.tsx | 4 +- .../lib/components/WorkBench/WbValidation.tsx | 404 +----------------- .../lib/components/WorkBench/WbView.tsx | 6 +- 3 files changed, 11 insertions(+), 403 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index b9c5e9e377e..6db4cff8155 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type { WbStatus } from '../WorkBench/WbView'; -import { WbValidationReact } from '../WorkBench/WbValidation'; +import { WbValidation } from '../WorkBench/WbValidation'; import { Button } from '../Atoms/Button'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; @@ -15,7 +15,7 @@ export function WbValidate({ readonly canLiveValidate: boolean; readonly hasUnsavedChanges: boolean; readonly startUpload: (mode: WbStatus) => void; - readonly validation: WbValidationReact; + readonly validation: WbValidation; }): JSX.Element { const handleValidate = () => startUpload('validate'); const handleToggleDataCheck = () => validation.toggleDataCheck(); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index 41bcb46e6e1..ca20e15ee3c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -1,4 +1,3 @@ -import { commonText } from '../../localization/common'; import { whitespaceSensitive } from '../../localization/utils'; import { wbText } from '../../localization/workbench'; import { ajax } from '../../utils/ajax'; @@ -12,19 +11,13 @@ import { formatToManyIndex, formatTreeRank, } from '../WbPlanView/mappingHelpers'; -import type { WbCellMetaReact, WbMeta } from './CellMeta'; +import type { WbMeta } from './CellMeta'; import type { UploadResult } from './resultsParser'; import { resolveValidationMessage } from './resultsParser'; -import type { WbView, Workbench } from './WbView'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import Handsontable from 'handsontable'; -import { WbMapping } from './mapping'; -import { WbUtils } from './WbUtils'; -import { WbActionsReact } from './WbActions'; -import { Dispatch, SetStateAction } from 'react'; +import type { Workbench } from './WbView'; /* eslint-disable functional/no-this-expression */ -export class WbValidationReact { +export class WbValidation { // eslint-disable-next-line functional/prefer-readonly-type public liveValidationStack: WritableArray = []; @@ -96,6 +89,7 @@ export class WbValidationReact { (_, visualRow) => this.workbench.hot!.toPhysicalRow(visualRow) ).reverse(); this.triggerLiveValidation(); + // TODO: toggleCells needs refactor // this.workbench.utils?.toggleCellTypes('newCells', 'remove'); // this.workbench.utils?.toggleCellTypes('invalidCells', 'remove'); break; @@ -151,6 +145,7 @@ export class WbValidationReact { } updateValidationButton(): void { + // TODO: figure out this function // if (!this.setCount) return; // this.setCount(this.liveValidationStack.length); } @@ -395,391 +390,4 @@ export class WbValidationReact { } } -/* eslint-enable functional/no-this-expression */ - - -/* eslint-disable functional/no-this-expression */ -export class WbValidation { - // eslint-disable-next-line functional/prefer-readonly-type - public liveValidationStack: WritableArray = []; - - // eslint-disable-next-line functional/prefer-readonly-type - private liveValidationActive: boolean = false; - - // eslint-disable-next-line functional/prefer-readonly-type - public validationMode: 'live' | 'off' | 'static'; - - // eslint-disable-next-line functional/prefer-readonly-type - public uploadResults: { - readonly ambiguousMatches: WritableArray< - WritableArray<{ - readonly physicalCols: RA; - readonly mappingPath: MappingPath; - readonly ids: RA; - readonly key: string; - }> - >; - readonly recordCounts: Partial, number>>; - readonly newRecords: Partial< - WritableArray< - WritableArray< - WritableArray< - Readonly< - readonly [ - tableName: Lowercase, - id: number, - alternativeLabel: string | '' - ] - > - > - > - > - >; - } = { - ambiguousMatches: [], - recordCounts: {}, - newRecords: [], - }; - - public constructor(private readonly wbView: WbView) { - this.stopLiveValidation(); - this.validationMode = - this.wbView.dataset.rowresults === null ? 'off' : 'static'; - } - - toggleDataCheck(event: MouseEvent): void { - const target = event.target as HTMLElement | null; - if (this.wbView.hot === undefined || target === null) return; - - this.validationMode = - this.validationMode === 'live' || - (this.wbView.mappings?.lines ?? []).length === 0 - ? 'off' - : 'live'; - - this.uploadResults = { - ambiguousMatches: [], - recordCounts: {}, - newRecords: [], - }; - this.wbView.cells.cellMeta = []; - - switch (this.validationMode) { - case 'live': { - this.liveValidationStack = Array.from( - { length: this.wbView.hot.countRows() }, - (_, visualRow) => this.wbView.hot!.toPhysicalRow(visualRow) - ).reverse(); - this.triggerLiveValidation(); - this.wbView.wbUtils.toggleCellTypes('newCells', 'remove'); - this.wbView.wbUtils.toggleCellTypes('invalidCells', 'remove'); - target.toggleAttribute('aria-pressed', true); - break; - } - case 'off': { - this.liveValidationStack = []; - this.liveValidationActive = false; - target.toggleAttribute('aria-pressed', false); - break; - } - } - - this.wbView.hot.render(); - this.updateValidationButton(); - } - - public startValidateRow(physicalRow: number): void { - if (this.validationMode !== 'live') return; - this.liveValidationStack = this.liveValidationStack - .filter((row) => row !== physicalRow) - .concat(physicalRow); - this.triggerLiveValidation(); - } - - triggerLiveValidation() { - const pumpValidation = (): void => { - this.updateValidationButton(); - if (this.liveValidationStack.length === 0) { - this.liveValidationActive = false; - return; - } - this.liveValidationActive = true; - const physicalRow = this.liveValidationStack.pop(); - if (physicalRow === undefined || this.wbView.hot === undefined) return; - const rowData = this.wbView.hot.getSourceDataAtRow(physicalRow); - ajax<{ - readonly result: UploadResult; - } | null>(`/api/workbench/validate_row/${this.wbView.dataset.id}/`, { - method: 'POST', - body: rowData, - headers: { Accept: 'application/json' }, - }) - .then(({ data: result }) => { - const uploads = result?.result; - if (typeof uploads === 'object' && uploads !== null) - this.gotRowValidationResult(physicalRow, uploads); - }) - .then(pumpValidation); - }; - - if (!this.liveValidationActive) { - pumpValidation(); - } - } - - updateValidationButton(): void { - const button = this.wbView.el.querySelector('.wb-data-check'); - if (button === null) return; - button.textContent = - this.validationMode === 'live' - ? this.liveValidationStack.length > 0 - ? commonText.countLine({ - resource: wbText.dataCheckOn(), - count: this.liveValidationStack.length, - }) - : wbText.dataCheckOn() - : wbText.dataCheck(); - } - - gotRowValidationResult(physicalRow: number, result: UploadResult): void { - if ( - this.validationMode !== 'live' || - this.wbView.hot?.isDestroyed !== false - ) - return; - this.uploadResults.ambiguousMatches[physicalRow] = []; - this.wbView.hot.batch(() => - this.applyRowValidationResults(physicalRow, result) - ); - this.wbView.cells.updateCellInfoStats(); - } - - getHeadersFromMappingPath( - mappingPathFilter: RA, - tryBest = true - ): RA { - if (this.wbView.mappings === undefined) return []; - if (!tryBest) - // Find all columns with the shared parent mapping path - return this.wbView.mappings.lines - .filter(({ mappingPath }) => - pathStartsWith(mappingPath, mappingPathFilter) - ) - .map(({ headerName }) => headerName); - return ( - mappedFind(mappingPathFilter, (_, index) => { - const columns = this.wbView - .mappings!.lines.filter(({ mappingPath }) => - pathStartsWith( - mappingPath, - mappingPathFilter.slice(0, index === 0 ? undefined : -1 * index) - ) - ) - .map(({ headerName }) => headerName); - return columns.length > 0 ? columns : undefined; - }) ?? [] - ); - } - - resolveValidationColumns( - initialColumns: RA, - inferColumnsCallback: (() => RA) | undefined = undefined - ) { - // See https://github.com/specify/specify7/issues/810 - let columns: RA = initialColumns.filter(Boolean); - if (typeof inferColumnsCallback === 'function') { - if (columns.length === 0) columns = inferColumnsCallback(); - if (columns.length === 0) columns = this.wbView.dataset.columns; - } - // Convert to physicalCol and filter out unknown columns - return columns - .map((column) => this.wbView.dataset.columns.indexOf(column)) - .filter((physicalCol) => physicalCol !== -1); - } - - applyRowValidationResults(physicalRow: number, result: UploadResult) { - const rowMeta: WritableArray>> = - this.wbView.dataset.columns.map(() => ({ - isNew: false, - issues: [], - })); - - const setMeta = ( - key: KEY, - value: KEY extends 'issues' ? string : WbMeta[KEY], - columns: RA, - inferColumnsCallback: (() => RA) | undefined - ): void => - this.resolveValidationColumns(columns, inferColumnsCallback).forEach( - (physicalCol) => { - if (key === 'issues') - (rowMeta[physicalCol][key] as WritableArray).push( - capitalize(value as string) - ); - else rowMeta[physicalCol][key as 'isNew'] = value as boolean; - } - ); - - this.parseRowValidationResults(result, setMeta, physicalRow); - - rowMeta.forEach((cellMeta, physicalCol) => { - // To make errors discovered by live validation visible, isModified must be unset - if (cellMeta.issues?.length !== 0 && this.validationMode === 'live') - cellMeta.isModified = false; - Object.entries(cellMeta).map(([key, value]) => - this.wbView.cells.updateCellMeta(physicalRow, physicalCol, key, value) - ); - }); - } - - parseRowValidationResults( - result: UploadResult, - setMetaCallback: ( - key: KEY, - value: KEY extends 'issues' ? string : WbMeta[KEY], - columns: RA, - inferColumnsCallback: (() => RA) | undefined - ) => void, - physicalRow: number, - initialMappingPath: MappingPath | undefined = [] - ) { - const uploadResult = result.UploadResult; - const uploadStatus = Object.keys(uploadResult.record_result)[0]; - const statusData = uploadResult.record_result[uploadStatus]; - const data = uploadResult.record_result; - - const isTree = 'info' in statusData && statusData.info?.treeInfo !== null; - const mappingPath = isTree - ? [...initialMappingPath, formatTreeRank(statusData.info.treeInfo.rank)] - : initialMappingPath; - - const resolveColumns = this.getHeadersFromMappingPath.bind( - this, - mappingPath - ); - - // Ignore these statuses - if (['NullRecord', 'PropagatedFailure', 'Matched'].includes(uploadStatus)) { - } else if (uploadStatus === 'ParseFailures') - data.ParseFailures.failures.forEach((line) => { - const [issueMessage, payload, column] = - line.length === 2 ? [line[0], {}, line[1]] : line; - setMetaCallback( - 'issues', - whitespaceSensitive( - resolveValidationMessage(issueMessage, payload ?? {}) - ), - [column], - resolveColumns - ); - }); - else if (uploadStatus === 'NoMatch') - setMetaCallback( - 'issues', - wbText.noMatchErrorMessage(), - data.NoMatch.info.columns, - resolveColumns - ); - else if (uploadStatus === 'FailedBusinessRule') - setMetaCallback( - 'issues', - whitespaceSensitive( - resolveValidationMessage( - data.FailedBusinessRule.message, - data.FailedBusinessRule.payload ?? {} - ) - ), - data.FailedBusinessRule.info.columns, - resolveColumns - ); - else if (uploadStatus === 'MatchedMultiple') { - this.uploadResults.ambiguousMatches[physicalRow] ??= []; - this.uploadResults.ambiguousMatches[physicalRow].push({ - physicalCols: this.resolveValidationColumns( - data.MatchedMultiple.info.columns, - resolveColumns - ), - mappingPath, - ids: data.MatchedMultiple.ids, - key: data.MatchedMultiple.key, - }); - setMetaCallback( - 'issues', - whitespaceSensitive(wbText.matchedMultipleErrorMessage()), - data.MatchedMultiple.info.columns, - resolveColumns - ); - } else if (uploadStatus === 'Uploaded') { - setMetaCallback('isNew', true, data.Uploaded.info.columns, undefined); - const tableName = toLowerCase(data.Uploaded.info.tableName); - this.uploadResults.recordCounts[tableName] ??= 0; - this.uploadResults.recordCounts[tableName]! += 1; - this.uploadResults.newRecords[physicalRow] ??= []; - this.resolveValidationColumns(data.Uploaded.info.columns, undefined).map( - (physicalCol) => { - this.uploadResults.newRecords[physicalRow]![physicalCol] ??= []; - this.uploadResults.newRecords[physicalRow]![physicalCol].push([ - tableName, - data.Uploaded.id, - data.Uploaded.info?.treeInfo - ? `${data.Uploaded.info.treeInfo.name} (${data.Uploaded.info.treeInfo.rank})` - : '', - ]); - } - ); - } else - raise( - new Error( - `Trying to parse unknown uploadStatus type "${uploadStatus}" at - row ${this.wbView.hot?.toVisualRow(physicalRow) ?? ''}` - ) - ); - - Object.entries(uploadResult.toOne).forEach(([fieldName, uploadResult]) => - this.parseRowValidationResults( - uploadResult, - setMetaCallback, - physicalRow, - fieldName === 'parent' && isTree - ? mappingPath.slice(0, -1) - : [...mappingPath, fieldName] - ) - ); - - Object.entries(uploadResult.toMany).forEach(([fieldName, uploadResults]) => - uploadResults.forEach((uploadResult, toManyIndex) => - this.parseRowValidationResults( - uploadResult, - setMetaCallback, - physicalRow, - [...mappingPath, fieldName, formatToManyIndex(toManyIndex + 1)] - ) - ) - ); - } - - getValidationResults(): void { - if (this.wbView.actions.status !== undefined || !this.wbView.mappings) - return; - - if (this.wbView.dataset.rowresults === null) { - this.validationMode = 'off'; - this.updateValidationButton(); - return; - } - - this.wbView.dataset.rowresults.forEach((result, physicalRow) => { - this.applyRowValidationResults(physicalRow, result); - }); - - this.wbView.cells.updateCellInfoStats(); - } - - stopLiveValidation(): void { - this.liveValidationStack = []; - this.liveValidationActive = false; - this.validationMode = 'off'; - } -} - -/* eslint-enable functional/no-this-expression */ +/* eslint-enable functional/no-this-expression */ \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index b00bead75d5..866d99444a6 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -51,7 +51,7 @@ import { WbUploaded } from './Results'; import { wbViewTemplate } from './Template'; import { WbActionsComponent } from '../WbActions/WbActions'; import { WbUtils, WbUtilsComponent } from './WbUtils'; -import { WbValidation, WbValidationReact } from './WbValidation'; +import { WbValidation } from './WbValidation'; import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; @@ -63,7 +63,7 @@ export type Workbench = { dataset: Dataset; cells: WbCellMeta; disambiguation: DisambiguationReact; - validation: WbValidationReact; + validation: WbValidation; data: RA>; hot: Handsontable; throttleRate: number; @@ -156,7 +156,7 @@ export function WbViewReact({ }; workbench.cells = new WbCellMeta(workbench); workbench.disambiguation = new DisambiguationReact(workbench); - workbench.validation = new WbValidationReact(workbench); + workbench.validation = new WbValidation(workbench); workbench.utils = new WbUtils(workbench); return workbench; }, [dataset, hotIsReady]); From 7b7d1928639bb7ae3124501e00a5f79976e5592a Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:59:42 -0400 Subject: [PATCH 045/153] Remove old Disambiguation code --- .../WorkBench/DisambiguationLogic.tsx | 255 ++---------------- .../lib/components/WorkBench/WbView.tsx | 6 +- 2 files changed, 24 insertions(+), 237 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx index 8405e578d79..59c388fe0ad 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx @@ -1,25 +1,12 @@ -import React from 'react'; - -import { commonText } from '../../localization/common'; -import { wbText } from '../../localization/workbench'; import type { IR } from '../../utils/types'; -import type { AnySchema } from '../DataModel/helperTypes'; -import type { Collection } from '../DataModel/specifyTable'; -import { strictGetTable } from '../DataModel/tables'; -import { Dialog } from '../Molecules/Dialog'; -import { hasTablePermission } from '../Permissions/helpers'; import type { MappingPath } from '../WbPlanView/Mapper'; import { mappingPathToString } from '../WbPlanView/mappingHelpers'; -import { getTableFromMappingPath } from '../WbPlanView/navigator'; -import { DisambiguationDialog } from './Disambiguation'; import { getSelectedLast } from './hotHelpers'; -import type { WbView, Workbench } from './WbView'; +import type { Workbench } from './WbView'; /* eslint-disable functional/no-this-expression */ -export class DisambiguationReact { - public constructor( - private readonly workbench: Workbench - ) {} +export class Disambiguation { + public constructor(private readonly workbench: Workbench) {} public getDisambiguation(physicalRow: number): IR { const cols = this.workbench.dataset.columns.length; @@ -32,7 +19,10 @@ export class DisambiguationReact { } public isAmbiguousCell(): boolean { - if (this.workbench.mappings === undefined || this.workbench.hot === undefined) + if ( + this.workbench.mappings === undefined || + this.workbench.hot === undefined + ) return false; const [visualRow, visualCol] = getSelectedLast(this.workbench.hot); @@ -41,7 +31,8 @@ export class DisambiguationReact { const disambiguation = this.getDisambiguation(physicalRow); return ( - this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow] ?? [] + this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow] ?? + [] ).some( ({ physicalCols, mappingPath }) => physicalCols.includes(physicalCol) && @@ -55,7 +46,9 @@ export class DisambiguationReact { ): boolean { const da = this.getDisambiguation(physicalRow); return Boolean( - this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow]?.find( + this.workbench.validation?.uploadResults.ambiguousMatches[ + physicalRow + ]?.find( ({ physicalCols, mappingPath }) => physicalCols.includes(physicalCol) && typeof da[mappingPathToString(mappingPath)] === 'number' @@ -81,15 +74,22 @@ export class DisambiguationReact { JSON.stringify(extra), source ); + // TODO: need to add spreadsheetChanged to workbench // this.workbench.spreadSheetChanged(); this.afterChangeDisambiguation(physicalRow); } public afterChangeDisambiguation(physicalRow: number): void { - (this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow] ?? []) + ( + this.workbench.validation?.uploadResults.ambiguousMatches[physicalRow] ?? + [] + ) .flatMap(({ physicalCols }) => physicalCols) .forEach((physicalCol) => - this.workbench.cells?.recalculateIsModifiedState(physicalRow, physicalCol) + this.workbench.cells?.recalculateIsModifiedState( + physicalRow, + physicalCol + ) ); this.workbench.cells?.updateCellInfoStats(); } @@ -119,216 +119,3 @@ export class DisambiguationReact { } /* eslint-enable functional/no-this-expression */ - -/* eslint-disable functional/no-this-expression */ -export class Disambiguation { - public constructor(private readonly wbView: WbView) {} - - private getDisambiguation(physicalRow: number): IR { - const cols = this.wbView.dataset.columns.length; - const hiddenColumn = this.wbView.data[physicalRow][cols]; - const extra = - typeof hiddenColumn === 'string' && hiddenColumn.length > 0 - ? JSON.parse(hiddenColumn) - : {}; - return extra.disambiguation ?? {}; - } - - public isAmbiguousCell(): boolean { - if (this.wbView.mappings === undefined || this.wbView.hot === undefined) - return false; - - const [visualRow, visualCol] = getSelectedLast(this.wbView.hot); - const physicalRow = this.wbView.hot.toPhysicalRow(visualRow); - const physicalCol = this.wbView.hot.toPhysicalColumn(visualCol); - const disambiguation = this.getDisambiguation(physicalRow); - - return ( - this.wbView.validation.uploadResults.ambiguousMatches[physicalRow] ?? [] - ).some( - ({ physicalCols, mappingPath }) => - physicalCols.includes(physicalCol) && - typeof disambiguation[mappingPathToString(mappingPath)] !== 'number' - ); - } - - public cellWasDisambiguated( - physicalRow: number, - physicalCol: number - ): boolean { - const da = this.getDisambiguation(physicalRow); - return Boolean( - this.wbView.validation.uploadResults.ambiguousMatches[physicalRow]?.find( - ({ physicalCols, mappingPath }) => - physicalCols.includes(physicalCol) && - typeof da[mappingPathToString(mappingPath)] === 'number' - ) - ); - } - - private changeDisambiguation( - physicalRow: number, - changeFunction: (oldValue: IR) => IR, - source: 'Disambiguation.Clear' | 'Disambiguation.Set' - ): void { - if (this.wbView.hot === undefined) return; - const cols = this.wbView.dataset.columns.length; - const hidden = this.wbView.data[physicalRow][cols]; - const extra = hidden ? JSON.parse(hidden) : {}; - extra.disambiguation = changeFunction(extra.disambiguation || {}); - const visualRow = this.wbView.hot.toVisualRow(physicalRow); - const visualCol = this.wbView.hot.toVisualColumn(cols); - this.wbView.hot.setDataAtCell( - visualRow, - visualCol, - JSON.stringify(extra), - source - ); - this.wbView.actions.spreadSheetChanged(); - this.afterChangeDisambiguation(physicalRow); - } - - public afterChangeDisambiguation(physicalRow: number): void { - (this.wbView.validation.uploadResults.ambiguousMatches[physicalRow] ?? []) - .flatMap(({ physicalCols }) => physicalCols) - .forEach((physicalCol) => - this.wbView.cells.recalculateIsModifiedState(physicalRow, physicalCol) - ); - this.wbView.cells.updateCellInfoStats(); - } - - clearDisambiguation(physicalRow: number): void { - const disambiguation = this.getDisambiguation(physicalRow); - if (Object.keys(disambiguation).length === 0) - // Nothing to clear - return; - this.changeDisambiguation(physicalRow, () => ({}), 'Disambiguation.Clear'); - } - - private setDisambiguation( - physicalRow: number, - mappingPath: MappingPath, - id: number - ): void { - this.changeDisambiguation( - physicalRow, - (disambiguations) => ({ - ...disambiguations, - [mappingPathToString(mappingPath)]: id, - }), - 'Disambiguation.Set' - ); - } - - public openDisambiguationDialog() { - if (this.wbView.mappings === undefined || this.wbView.hot === undefined) - return; - - const [visualRow, visualCol] = getSelectedLast(this.wbView.hot); - const physicalRow = this.wbView.hot.toPhysicalRow(visualRow); - const physicalCol = this.wbView.hot.toPhysicalColumn(visualCol); - - const matches = this.wbView.validation.uploadResults.ambiguousMatches[ - physicalRow - ].find(({ physicalCols }) => physicalCols.includes(physicalCol)); - if (matches === undefined) return; - const tableName = getTableFromMappingPath( - this.wbView.mappings.baseTable.name, - matches.mappingPath - ); - const table = strictGetTable(tableName); - const resources = new table.LazyCollection({ - filters: { id__in: matches.ids.join(',') }, - }) as Collection; - - (hasTablePermission(table.name, 'read') - ? resources.fetch({ limit: 0 }) - : Promise.resolve(resources) - ).then(({ models }) => { - if (models.length === 0) { - const dialog = this.wbView.options.display( - dialog()} - > - {wbText.noDisambiguationResultsDescription()} - - ); - return; - } - - // Re-enable this once live validation is available again: - /* - * Disable "Apply All" if validation is still in progress. - * This is because we don't know all matches until validation is done - */ - /* - *Let applyAllAvailable = true; - *const applyAllButton = content.find('#applyAllButton'); - * - *const updateIt = () => { - * const newState = this.liveValidationStack.length === 0; - * if (newState !== applyAllAvailable) { - * applyAllAvailable = newState; - * applyAllButton.disabled = !newState; - * applyAllButton[newState ? 'removeAttribute' : 'setAttribute']( - * 'title', - * wbText.applyAllUnavailable() - * ); - * } - *}; - * - *const interval = globalThis.setInterval(updateIt, 100); - * // onClose: globalThis.clearInterval(interval); - */ - - const dialog = this.wbView.options.display( - dialog()} - onSelected={(selected) => { - this.setDisambiguation( - physicalRow, - matches.mappingPath, - selected.id - ); - this.wbView.validation.startValidateRow(physicalRow); - }} - onSelectedAll={(selected): void => - // Loop backwards so the live validation will go from top to bottom - this.wbView.hot?.batch(() => { - for ( - let visualRow = this.wbView.data.length - 1; - visualRow >= 0; - visualRow-- - ) { - const physicalRow = this.wbView.hot!.toPhysicalRow(visualRow); - if ( - !this.wbView.validation.uploadResults.ambiguousMatches[ - physicalRow - ]?.find( - ({ key, mappingPath }) => - key === matches.key && - typeof this.getDisambiguation(physicalRow)[ - mappingPathToString(mappingPath) - ] !== 'number' - ) - ) - continue; - this.setDisambiguation( - physicalRow, - matches.mappingPath, - selected.id - ); - this.wbView.validation.startValidateRow(physicalRow); - } - }) - } - /> - ); - }); - } -} - -/* eslint-enable functional/no-this-expression */ diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 866d99444a6..c16d181f22b 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -39,7 +39,7 @@ import { userPreferences } from '../Preferences/userPreferences'; import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbCellCounts, WbCellMeta } from './CellMeta'; -import { Disambiguation, DisambiguationReact } from './DisambiguationLogic'; +import { Disambiguation } from './DisambiguationLogic'; import { configureHandsontable, getHotPlugin } from './handsontable'; import { downloadDataSet } from './helpers'; import { getHotHooks, getHotHooksReact } from './hooks'; @@ -62,7 +62,7 @@ export type WbStatus = 'unupload' | 'upload' | 'validate'; export type Workbench = { dataset: Dataset; cells: WbCellMeta; - disambiguation: DisambiguationReact; + disambiguation: Disambiguation; validation: WbValidation; data: RA>; hot: Handsontable; @@ -155,7 +155,7 @@ export function WbViewReact({ cellCounts: [cellCounts, setCellCounts], }; workbench.cells = new WbCellMeta(workbench); - workbench.disambiguation = new DisambiguationReact(workbench); + workbench.disambiguation = new Disambiguation(workbench); workbench.validation = new WbValidation(workbench); workbench.utils = new WbUtils(workbench); return workbench; From 08879db2abab73118e52e7cdfee6634693d4bb41 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:09:03 -0400 Subject: [PATCH 046/153] Add loading screen to workbench --- .../frontend/js_src/lib/components/WorkBench/index.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 9d4c6a36dc5..78b9d4dbdaa 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -12,6 +12,7 @@ import { f } from '../../utils/functools'; import type { GetSet } from '../../utils/types'; import { className } from '../Atoms/className'; import { LoadingContext } from '../Core/Contexts'; +import { LoadingScreen } from '../Molecules/Dialog'; import { useMenuItem } from '../Header/MenuContext'; import { treeRanksPromise } from '../InitialContext/treeRanks'; import { Dialog } from '../Molecules/Dialog'; @@ -40,7 +41,7 @@ function useDataSet( ); } -export function WorkBenchReact(): JSX.Element | null { +export function WorkBenchReact(): JSX.Element { useMenuItem('workBench'); const [treeRanksLoaded = false] = useAsyncState(fetchTreeRanks, true); @@ -57,8 +58,7 @@ export function WorkBenchReact(): JSX.Element | null { const hotRef = React.useRef(null); const spreadsheetContainer = React.useRef(null); - // temporary null check, replace with loading screen? - if (!dataSet || !treeRanksLoaded) return null; + if (!dataSet || !treeRanksLoaded) return ; const triggerRefresh = () => { loading(fetchDataSet(dataSet!.id).then(setDataSet)); From 78f3796413b7c9f1aa82e13d5eb21bb549cad265 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 5 Apr 2024 15:36:56 -0400 Subject: [PATCH 047/153] Remove unused class names --- .../lib/components/WbActions/WbValidate.tsx | 1 - .../lib/components/WbToolkit/ChangeOwner.tsx | 1 - .../WbToolkit/CoordinateConverter.tsx | 1 - .../lib/components/WbToolkit/GeoLocate.tsx | 1 - .../lib/components/WbToolkit/WbLeafletMap.tsx | 1 - .../lib/components/WorkBench/CellMeta.ts | 18 ++++++++---------- .../lib/components/WorkBench/WbUtils.tsx | 12 +++++------- 7 files changed, 13 insertions(+), 22 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index 6db4cff8155..28e5663e751 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -38,7 +38,6 @@ export function WbValidate({
{label} ( - + {currentPosition} - /{totalCount}) + /{totalCount}) Date: Fri, 5 Apr 2024 15:51:06 -0400 Subject: [PATCH 048/153] Improve UploadResult typing --- .../lib/components/WorkBench/WbValidation.tsx | 73 ++++++++++--------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index ca20e15ee3c..cf47625d328 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -16,6 +16,33 @@ import type { UploadResult } from './resultsParser'; import { resolveValidationMessage } from './resultsParser'; import type { Workbench } from './WbView'; +type UploadResults = { + ambiguousMatches: WritableArray< + WritableArray<{ + physicalCols: RA; + mappingPath: MappingPath; + ids: RA; + key: string; + }> + >; + recordCounts: Partial, number>>; + newRecords: Partial< + WritableArray< + WritableArray< + WritableArray< + Readonly< + readonly [ + tableName: Lowercase, + id: number, + alternativeLabel: string | '' + ] + > + > + > + > + >; +}; + /* eslint-disable functional/no-this-expression */ export class WbValidation { // eslint-disable-next-line functional/prefer-readonly-type @@ -28,40 +55,13 @@ export class WbValidation { public validationMode: 'live' | 'off' | 'static'; // eslint-disable-next-line functional/prefer-readonly-type - public uploadResults: { - readonly ambiguousMatches: WritableArray< - WritableArray<{ - readonly physicalCols: RA; - readonly mappingPath: MappingPath; - readonly ids: RA; - readonly key: string; - }> - >; - readonly recordCounts: Partial, number>>; - readonly newRecords: Partial< - WritableArray< - WritableArray< - WritableArray< - Readonly< - readonly [ - tableName: Lowercase, - id: number, - alternativeLabel: string | '' - ] - > - > - > - > - >; - } = { + public uploadResults: UploadResults = { ambiguousMatches: [], recordCounts: {}, newRecords: [], }; - public constructor( - private readonly workbench: Workbench - ) { + public constructor(private readonly workbench: Workbench) { this.stopLiveValidation(); this.validationMode = this.workbench.dataset.rowresults === null ? 'off' : 'static'; @@ -177,7 +177,8 @@ export class WbValidation { .map(({ headerName }) => headerName); return ( mappedFind(mappingPathFilter, (_, index) => { - const columns = this.workbench.mappings!.lines.filter(({ mappingPath }) => + const columns = this.workbench + .mappings!.lines.filter(({ mappingPath }) => pathStartsWith( mappingPath, mappingPathFilter.slice(0, index === 0 ? undefined : -1 * index) @@ -235,7 +236,12 @@ export class WbValidation { if (cellMeta.issues?.length !== 0 && this.validationMode === 'live') cellMeta.isModified = false; Object.entries(cellMeta).map(([key, value]) => - this.workbench.cells?.updateCellMeta(physicalRow, physicalCol, key, value) + this.workbench.cells?.updateCellMeta( + physicalRow, + physicalCol, + key, + value + ) ); }); } @@ -367,8 +373,7 @@ export class WbValidation { } getValidationResults(): void { - if (!this.workbench.mappings) - return; + if (!this.workbench.mappings) return; if (this.workbench.dataset.rowresults === null) { this.validationMode = 'off'; @@ -390,4 +395,4 @@ export class WbValidation { } } -/* eslint-enable functional/no-this-expression */ \ No newline at end of file +/* eslint-enable functional/no-this-expression */ From 05b038ffdb79c91c083de53dc7c3a134e357e7d3 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 5 Apr 2024 16:23:05 -0400 Subject: [PATCH 049/153] Remove old WbView code --- .../lib/components/WorkBench/Template.tsx | 433 --------- .../lib/components/WorkBench/WbView.tsx | 871 +----------------- 2 files changed, 37 insertions(+), 1267 deletions(-) delete mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Template.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Template.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Template.tsx deleted file mode 100644 index 52739b33205..00000000000 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Template.tsx +++ /dev/null @@ -1,433 +0,0 @@ -/** - * Generate static template for the WbView using React - * All logic and event listeners would be attached in WbView.js - */ - -import React from 'react'; -import ReactDOMServer from 'react-dom/server'; -import { useNavigate, useParams } from 'react-router-dom'; -import type { LocalizedString } from 'typesafe-i18n'; - -import { useUnloadProtect } from '../../hooks/navigation'; -import { useAsyncState } from '../../hooks/useAsyncState'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { useErrorContext } from '../../hooks/useErrorContext'; -import { commonText } from '../../localization/common'; -import { localityText } from '../../localization/locality'; -import { wbPlanText } from '../../localization/wbPlan'; -import { wbText } from '../../localization/workbench'; -import { ajax } from '../../utils/ajax'; -import { f } from '../../utils/functools'; -import type { GetSet, RA } from '../../utils/types'; -import { replaceItem } from '../../utils/utils'; -import { Button } from '../Atoms/Button'; -import { className } from '../Atoms/className'; -import { Input } from '../Atoms/Form'; -import { Link } from '../Atoms/Link'; -import { LoadingContext } from '../Core/Contexts'; -import { useMenuItem } from '../Header/MenuContext'; -import { treeRanksPromise } from '../InitialContext/treeRanks'; -import { Dialog } from '../Molecules/Dialog'; -import { Portal } from '../Molecules/Portal'; -import { hasPermission, hasTablePermission } from '../Permissions/helpers'; -import { userPreferences } from '../Preferences/userPreferences'; -import { NotFoundView } from '../Router/NotFoundView'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import type { WbStatus } from './WbView'; -import { WbView as WbViewClass } from './WbView'; - -function Navigation({ - name, - label, -}: { - readonly name: string; - readonly label: LocalizedString; -}): JSX.Element { - return ( - - - {'<'} - - - {label} (0/ - 0) - - - {'>'} - - - ); -} - -function WbView({ - isUploaded, - isMapped, - dataSetId, -}: { - readonly isUploaded: boolean; - readonly isMapped: boolean; - readonly dataSetId: number; -}): JSX.Element { - const canUpdate = hasPermission('/workbench/dataset', 'update'); - const [canLiveValidate] = userPreferences.use( - 'workBench', - 'general', - 'liveValidation' - ); - return ( - <> -
-
- - {commonText.tools()} - - - {canUpdate || isMapped ? ( - - {wbPlanText.dataMapper()} - - ) : undefined} - {!isUploaded && hasPermission('/workbench/dataset', 'validate') && ( - <> - - {wbText.dataCheck()} - - - {wbText.validate()} - - - )} - - {commonText.results()} - - {isUploaded ? ( - hasPermission('/workbench/dataset', 'unupload') && ( - - {wbText.rollback()} - - ) - ) : ( - <> - {hasPermission('/workbench/dataset', 'upload') && ( - - {wbText.upload()} - - )} - {hasPermission('/workbench/dataset', 'update') && ( - <> - - {wbText.revert()} - - - {commonText.save()} - - - )} - - )} -
-
- {hasPermission('/workbench/dataset', 'transfer') && - hasTablePermission('SpecifyUser', 'read') ? ( - <> - - {wbText.changeOwner()} - - - {wbText.uploadPlan()} - - - ) : undefined} - - {commonText.export()} - - - {hasPermission('/workbench/dataset', 'update') && ( - <> - - {wbText.convertCoordinates()} - - - {localityText.geoLocate()} - - - )} - - {localityText.geoMap()} - -
-
-
-
-
- -
- -
- {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( -
- -
- ) : undefined} - - - - {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( - - ) : undefined} - - {!isUploaded && ( - - )} -
- - ); -} - -export const wbViewTemplate = ( - isUploaded: boolean, - isMapped: boolean, - dataSetId: number -): string => - ReactDOMServer.renderToStaticMarkup( - - ); - -const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); - -export function WorkBench(): JSX.Element | null { - useMenuItem('workBench'); - - const [treeRanksLoaded = false] = useAsyncState(fetchTreeRanks, true); - const { id } = useParams(); - const dataSetId = f.parseInt(id); - - const [container, setContainer] = React.useState(null); - const [dataSet, setDataSet] = useDataSet(dataSetId); - useErrorContext('dataSet', dataSet); - const loading = React.useContext(LoadingContext); - const [isDeleted, handleDeleted] = useBooleanState(); - const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); - const portals = useWbView( - dataSet, - treeRanksLoaded, - container, - handleDeleted, - handleDeletedConfirmation, - () => loading(fetchDataSet(dataSet!.id).then(setDataSet)) - ); - - const navigate = useNavigate(); - return dataSetId === undefined ? ( - - ) : isDeleted ? ( - <>{wbText.dataSetDeletedOrNotFound()} - ) : isDeletedConfirmation ? ( - navigate('/specify/', { replace: true })} - > - {wbText.dataSetDeletedDescription()} - - ) : ( - <> -
- {portals.map((portal, index) => - portal === undefined ? undefined : ( - - {portal.jsx} - - ) - )} - - ); -} - -// BUG: intercept 403 (if dataset has been transferred to another user) -function useDataSet( - dataSetId: number | undefined -): GetSet { - return useAsyncState( - React.useCallback(async () => fetchDataSet(dataSetId), [dataSetId]), - true - ); -} - -const fetchDataSet = async ( - dataSetId: number | undefined -): Promise => - typeof dataSetId === 'number' - ? ajax(`/api/workbench/dataset/${dataSetId}/`, { - headers: { Accept: 'application/json' }, - }).then(({ data }) => data) - : undefined; - -function useWbView( - dataSet: Dataset | undefined, - treeRanksLoaded: boolean, - container: HTMLElement | null, - handleDeleted: () => void, - handleDeletedConfirmation: () => void, - handleRefresh: () => void -): RA< - | { readonly jsx: JSX.Element; readonly element: HTMLElement | undefined } - | undefined -> { - const [portals, setPortals] = React.useState< - RA< - | { readonly jsx: JSX.Element; readonly element: HTMLElement | undefined } - | undefined - > - >([]); - - const mode = React.useRef(undefined); - const wasAborted = React.useRef(false); - - const [hasUnloadProtect, setUnloadProtect] = React.useState(false); - useUnloadProtect(hasUnloadProtect, wbText.wbUnloadProtect()); - - React.useEffect(() => { - if (!treeRanksLoaded || container === null || dataSet === undefined) - return undefined; - const contained = document.createElement('section'); - contained.setAttribute('class', `wbs-form ${className.containerFull}`); - container.append(contained); - const view = new WbViewClass( - dataSet, - mode.current, - wasAborted.current, - contained, - { - onSetUnloadProtect: setUnloadProtect, - onDeleted: handleDeleted, - onDeletedConfirmation: handleDeletedConfirmation, - display(jsx, element, destructor): () => void { - let index = 0; - setPortals((portals) => { - index = portals.length; - return [...portals, { jsx, element }]; - }); - return () => { - setPortals((portals) => replaceItem(portals, index, undefined)); - destructor?.(); - }; - }, - } - ) - .on('refresh', (newMode: WbStatus | undefined, newWasAborted = false) => { - setUnloadProtect(false); - mode.current = newMode; - wasAborted.current = newWasAborted; - handleRefresh(); - }) - .render(); - return () => void view.remove(); - }, [treeRanksLoaded, container, dataSet]); - - return portals; -} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index c16d181f22b..de30490adb0 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -17,38 +17,22 @@ import Handsontable from 'handsontable'; import { HotTable } from '@handsontable/react'; import { commonText } from '../../localization/common'; -import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; -import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; -import { f } from '../../utils/functools'; -import type { GetSet, IR, RA, WritableArray } from '../../utils/types'; -import { ensure, overwriteReadOnly, writable } from '../../utils/types'; -import { clamp, throttle } from '../../utils/utils'; +import type { GetSet, RA } from '../../utils/types'; +import { clamp } from '../../utils/utils'; import { Button } from '../Atoms/Button'; -import { iconClassName, legacyNonJsxIcons } from '../Atoms/Icons'; import { Link } from '../Atoms/Link'; -import { legacyLoadingContext } from '../Core/Contexts'; -import { Backbone } from '../DataModel/backbone'; -import { getTable, strictGetTable } from '../DataModel/tables'; -import { crash, raise } from '../Errors/Crash'; -import { getIcon, unknownIcon } from '../InitialContext/icons'; -import { Dialog } from '../Molecules/Dialog'; import { hasPermission } from '../Permissions/helpers'; -import { userPreferences } from '../Preferences/userPreferences'; -import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbCellCounts, WbCellMeta } from './CellMeta'; import { Disambiguation } from './DisambiguationLogic'; import { configureHandsontable, getHotPlugin } from './handsontable'; -import { downloadDataSet } from './helpers'; import { getHotHooks, getHotHooksReact } from './hooks'; -import { getSelectedRegions } from './hotHelpers'; import type { WbMapping } from './mapping'; import { parseWbMappings } from './mapping'; import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; -import { wbViewTemplate } from './Template'; import { WbActionsComponent } from '../WbActions/WbActions'; import { WbUtils, WbUtilsComponent } from './WbUtils'; import { WbValidation } from './WbValidation'; @@ -122,7 +106,7 @@ export function WbViewReact({ if (hotRef.current) { setHotToReady(); const configureHotAndPickLists = async () => { - const pickLists = await getPickLists(); // Await getPickLists() here + const pickLists = await getPickLists(); configureHandsontable( hotRef.current!.hotInstance!, mappings, @@ -178,7 +162,7 @@ export function WbViewReact({ spreadsheetChanged, checkDeletedFail ), - [dataset] + [dataset, hotIsReady] ); const isMapped = Boolean(dataset.uploadplan); @@ -187,21 +171,22 @@ export function WbViewReact({ const [showToolkit, _openToolkit, _closeToolkit, toggleToolkit] = useBooleanState(); + // TODO: move all states and logic for Results into a hook const [showResults, _, closeResults, toggleResults] = useBooleanState(); + const initialHiddenRows = React.useMemo(() => { + if (!hotRef.current) return []; + else return getHotPlugin(hotRef.current.hotInstance!, 'hiddenRows').getHiddenRows(); + }, [hotRef.current]); + const initialHiddenCols = React.useMemo(() => { + if (!hotRef.current) return []; + else return getHotPlugin(hotRef.current.hotInstance!, 'hiddenColumns').getHiddenColumns(); + }, [hotRef.current]); + // Makes the hot changes required for upload view results React.useEffect(() => { - if (!showResults || !workbench.hot) return; + if (!workbench.hot) return; - workbench.hot?.updateSettings({ readOnly: true }); - const initialHiddenRows = getHotPlugin( - workbench.hot, - 'hiddenRows' - ).getHiddenRows(); - const initialHiddenCols = getHotPlugin( - workbench.hot, - 'hiddenColumns' - ).getHiddenColumns(); const rowsToInclude = new Set(); const colsToInclude = new Set(); Object.entries(workbench.cells.cellMeta).forEach(([physicalRow, rowMeta]) => @@ -228,39 +213,35 @@ export function WbViewReact({ ) .map(workbench.hot.toVisualColumn); - getHotPlugin(workbench.hot, 'hiddenRows').hideRows(rowsToHide); - getHotPlugin(workbench.hot, 'hiddenColumns').hideColumns(colsToHide); + if (showResults) { + workbench.hot?.updateSettings({ readOnly: true }); + getHotPlugin(workbench.hot, 'hiddenRows').hideRows(rowsToHide); + getHotPlugin(workbench.hot, 'hiddenColumns').hideColumns(colsToHide); + + workbench.utils.toggleCellTypes( + 'newCells', + 'remove', + spreadsheetContainer.current + ); + } else { + getHotPlugin(workbench.hot, 'hiddenRows').showRows( + rowsToHide.filter((visualRow) => !initialHiddenRows.includes(visualRow)) + ); + getHotPlugin(workbench.hot, 'hiddenColumns').showColumns( + colsToHide.filter((visualCol) => !initialHiddenCols.includes(visualCol)) + ); + } - workbench.utils.toggleCellTypes( - 'newCells', - 'remove', - spreadsheetContainer.current - ); workbench.hot.render(); }, [showResults]); - // Calls validation results on load if dataset has been validated before - React.useEffect(() => { - if (dataset.rowresults && hotIsReady) { - workbench.validation.getValidationResults(); - if (workbench.validation?.validationMode === 'static' && !isUploaded) - workbench.utils.toggleCellTypes( - 'invalidCells', - 'remove', - spreadsheetContainer.current - ); - - workbench.cells.flushIndexedCellData = true; - } - }, [dataset.rowresults, hotIsReady]); - return ( <>
-
+
@@ -274,7 +255,6 @@ export function WbViewReact({ {commonText.tools()} @@ -309,7 +289,7 @@ export function WbViewReact({ /> ) : undefined}
-
+
{showResults ? ( -
+ ); } From c0fd41ba496446d454295710486ba500552ef80d Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:28:52 -0400 Subject: [PATCH 053/153] Rename triggerRefresh to triggerDatasetRefresh --- .../frontend/js_src/lib/components/WbActions/WbActions.tsx | 4 ++-- .../js_src/lib/components/WbToolkit/DevShowPlan.tsx | 6 +++--- .../frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index dc41fff9fde..a4c02894164 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -85,7 +85,7 @@ export function WbActionsComponent({ workbench, mappings, checkDeletedFail, - onRefresh: handleRefresh, + onDatasetRefresh: handleRefresh, onSpreadsheetUpToDate: handleSpreadsheetUpToDate, onToggleResults, }: { @@ -95,7 +95,7 @@ export function WbActionsComponent({ readonly workbench: Workbench; readonly mappings: WbMapping; readonly checkDeletedFail: (statusCode: number) => void; - readonly onRefresh: () => void; + readonly onDatasetRefresh: () => void; readonly onSpreadsheetUpToDate: () => void; readonly onToggleResults: () => void; }): JSX.Element { diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx index 36cecd11484..dfe478b6616 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx @@ -19,11 +19,11 @@ import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbDevPlan({ dataset, handleDatasetDelete, - triggerRefresh, + triggerDatasetRefresh, }: { readonly dataset: Dataset; readonly handleDatasetDelete: () => void; - readonly triggerRefresh: () => void; + readonly triggerDatasetRefresh: () => void; }): JSX.Element { const [showDevPlan, openDevPlan, closeDevPlan] = useBooleanState(); return ( @@ -43,7 +43,7 @@ export function WbDevPlan({ uploadPlan={dataset.uploadplan ?? ({} as UploadPlan)} onChanged={(plan) => { overwriteReadOnly(dataset, 'uploadplan', plan); - triggerRefresh(); + triggerDatasetRefresh(); }} onClose={closeDevPlan} onDeleted={handleDatasetDelete} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx index fc9b2d3a31e..94316776217 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx @@ -24,7 +24,7 @@ export function WbToolkit({ data, handleDatasetDelete, hasUnsavedChanges, - triggerRefresh, + triggerDatasetRefresh, }: { readonly dataset: Dataset; readonly hot: Handsontable | undefined; @@ -32,7 +32,7 @@ export function WbToolkit({ readonly data: RA>; readonly handleDatasetDelete: () => void; readonly hasUnsavedChanges: boolean; - readonly triggerRefresh: () => void; + readonly triggerDatasetRefresh: () => void; }): JSX.Element { const handleExport = React.useCallback((): void => { const delimiter = userPreferences.get( @@ -66,7 +66,7 @@ export function WbToolkit({ ) : undefined} From 9a6710189af9d9e478c9218dfecebed35b4c36c8 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Thu, 11 Apr 2024 15:31:18 -0400 Subject: [PATCH 054/153] Refactor WbSpreadsheet to use a callback ref - Memoizes HotTable props - Improves minor typings --- .../components/WorkBench/WbSpreadsheet.tsx | 316 +++--- .../lib/components/WorkBench/WbView.tsx | 907 +----------------- .../js_src/lib/components/WorkBench/hooks.ts | 2 +- .../js_src/lib/components/WorkBench/index.tsx | 8 +- 4 files changed, 231 insertions(+), 1002 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index d222dc28acd..3b07d28e84e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -4,14 +4,12 @@ * TODO: * - Upgrade to handsontable14 to fix copyPasteEnabled error * - Fix font size and color in the table - * - Add hooks to HotTable */ import { HotTable } from '@handsontable/react'; import Handsontable from 'handsontable'; import React from 'react'; import type { Settings } from 'handsontable/plugins/contextMenu'; -import type { Events } from 'handsontable/pluginHooks'; import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; @@ -24,9 +22,9 @@ import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbMapping } from './mapping'; import { getSelectedRegions } from './hotHelpers'; -import { WbValidation } from './WbValidation'; -import { WbCellMeta } from './CellMeta'; -import { Disambiguation } from './DisambiguationLogic'; +import { configureHandsontable } from './handsontable'; +import { fetchWbPickLists } from './pickLists'; +import { getHotHooks } from './hooks'; import { wbText } from '../../localization/workbench'; import { strictGetTable } from '../DataModel/tables'; import { getIcon, unknownIcon } from '../InitialContext/icons'; @@ -42,10 +40,14 @@ import { Dialog } from '../Molecules/Dialog'; import { DisambiguationDialog } from './Disambiguation'; import { mappingPathToString } from '../WbPlanView/mappingHelpers'; import { MappingPath } from '../WbPlanView/Mapper'; +import { registerAllModules } from 'handsontable/registry'; +import { Workbench } from './WbView'; + +registerAllModules(); // Context menu item definitions (common for fillUp and fillDown) const fillCellsContextMenuItem = ( - hotRef: React.RefObject, + hot: Handsontable, mode: 'down' | 'up' ): Handsontable.plugins.ContextMenu.MenuItemConfig => { return { @@ -54,9 +56,7 @@ const fillCellsContextMenuItem = ( // typeof this.wbView.uploadedView === 'function' || // typeof this.wbView.coordinateConverterView === 'function' || !hasPermission('/workbench/dataset', 'update') || - (hotRef.current - ?.hotInstance!?.getSelected() - ?.every((selection) => selection[0] === selection[2]) ?? + (hot.getSelected()?.every((selection) => selection[0] === selection[2]) ?? false), callback: (_, selections) => selections.forEach((selection) => @@ -69,9 +69,9 @@ const fillCellsContextMenuItem = ( const col = selection.start.col + index; const value = mode === 'up' - ? hotRef.current?.hotInstance!.getDataAtCell(endRow, col) - : hotRef.current?.hotInstance!.getDataAtCell(startRow, col); - hotRef.current?.hotInstance!.setDataAtCell( + ? hot.getDataAtCell(endRow, col) + : hot.getDataAtCell(startRow, col); + hot.setDataAtCell( Array.from({ length: endRow - startRow }, (_, index) => [ startRow + index + 1, col, @@ -85,30 +85,31 @@ const fillCellsContextMenuItem = ( function WbSpreadsheetComponent({ dataset, - hotRef, + setHotTable, + hot, isUploaded, data, - validation, - cells, - disambiguation, - hooks, + workbench, mappings, + checkDeletedFail, + spreadsheetChanged, }: { readonly dataset: Dataset; - readonly hotRef: React.RefObject; + readonly setHotTable: React.Ref; + readonly hot: Handsontable | undefined; readonly isUploaded: boolean; readonly data: RA>; - readonly validation: WbValidation; - readonly cells: WbCellMeta; - readonly disambiguation: Disambiguation; - readonly hooks: Partial; + readonly workbench: Workbench; readonly mappings: WbMapping; + readonly checkDeletedFail: any; + readonly spreadsheetChanged: any; }): JSX.Element { const physicalColToMappingCol = (physicalCol: number): number | undefined => mappings?.lines.findIndex( ({ headerName }) => headerName === dataset.columns[physicalCol] ); + const { validation, cells, disambiguation } = workbench; const [disambiguationMatches, setDisambiguationMatches] = React.useState<{ readonly physicalCols: RA; readonly mappingPath: MappingPath; @@ -127,14 +128,11 @@ function WbSpreadsheetComponent({ useBooleanState(); const openDisambiguationDialog = () => { - if (mappings === undefined || !hotRef.current) return; + if (mappings === undefined || hot === undefined) return; - const [visualRow, visualCol] = getSelectedLast( - hotRef.current?.hotInstance! - ); - const physicalRow = hotRef.current?.hotInstance!.toPhysicalRow(visualRow); - const physicalCol = - hotRef.current?.hotInstance!.toPhysicalColumn(visualCol); + const [visualRow, visualCol] = getSelectedLast(hot); + const physicalRow = hot.toPhysicalRow(visualRow); + const physicalCol = hot.toPhysicalColumn(visualCol); const matches = validation?.uploadResults.ambiguousMatches[ physicalRow @@ -285,9 +283,7 @@ function WbSpreadsheetComponent({ ) return true; // Or if called on the last row - const selectedRegions = getSelectedRegions( - hotRef.current?.hotInstance! - ); + const selectedRegions = getSelectedRegions(hot!); return ( selectedRegions.length === 1 && selectedRegions[0].startRow === data.length - 1 && @@ -306,135 +302,198 @@ function WbSpreadsheetComponent({ callback: () => openDisambiguationDialog(), }, separator_1: '---------', - fill_down: fillCellsContextMenuItem(hotRef, 'down'), - fill_up: fillCellsContextMenuItem(hotRef, 'up'), + fill_down: fillCellsContextMenuItem(hot!, 'down'), + fill_up: fillCellsContextMenuItem(hot!, 'up'), separator_2: '---------', undo: { disabled: () => // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || - hotRef.current?.hotInstance!.isUndoAvailable() !== true || + hot?.isUndoAvailable() !== true || !hasPermission('/workbench/dataset', 'update'), }, redo: { disabled: () => // TODO: Figure out if these commented lines can be removed // typeof this.uploadedView === 'function' || - hotRef.current?.hotInstance!.isRedoAvailable() !== true || + hot?.isRedoAvailable() !== true || !hasPermission('/workbench/dataset', 'update'), }, } as const) ), }; + React.useEffect(() => { + const getPickLists = async () => + mappings === undefined + ? {} + : await fetchWbPickLists( + dataset.columns, + mappings.tableNames, + mappings.lines + ); + if (hot) { + const configureHotAndPickLists = async () => { + const pickLists = await getPickLists(); + configureHandsontable(hot, mappings, dataset, pickLists); + }; + configureHotAndPickLists(); + } + }, [hot]); + // Highlight validation cells - React.useLayoutEffect(() => { - // TODD: Verify if previous code does anything else after validation - if (dataset.rowresults && hotRef.current) { + React.useEffect(() => { + // TODO: Verify if previous code does anything else after validation + if (dataset.rowresults && hot) { validation.getValidationResults(); + workbench.cells.flushIndexedCellData = true; } - }, [dataset.rowresults, hotRef.current]) + }, [dataset.rowresults, hot]); + + // TODO: Move hottable props to a different file? + const autoWrapCol = React.useMemo( + () => userPreferences.get('workBench', 'editor', 'autoWrapCol'), + [] + ); + const autoWrapRow = React.useMemo( + () => userPreferences.get('workBench', 'editor', 'autoWrapRow'), + [] + ); + const columns = React.useMemo( + () => + Array.from( + // Last column is invisible and contains disambiguation metadata + { length: dataset.columns.length + 1 }, + (_, physicalCol) => ({ + // Get data from nth column for nth column + data: physicalCol, + }) + ), + [] + ); + const enterMoves = React.useMemo( + () => + userPreferences.get('workBench', 'editor', 'enterMoveDirection') === 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 }, + [] + ); + + const colHeaders = React.useCallback((physicalCol: number) => { + const tableIcon = mappings?.mappedHeaders?.[physicalCol]; + const isMapped = tableIcon !== undefined; + const mappingCol = physicalColToMappingCol(physicalCol); + const tableName = + (typeof mappingCol === 'number' + ? mappings?.tableNames[mappingCol] + : undefined) ?? tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; + const tableLabel = isMapped + ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' + : ''; + // REFACTOR: use new table icons + return `
+ ${ + isMapped + ? `${tableLabel}` + : `${legacyNonJsxIcons.ban}` + } + + ${dataset.columns[physicalCol]} + +
`; + }, []); + + const enterBeginsEditing = React.useMemo( + () => userPreferences.get('workBench', 'editor', 'enterBeginsEditing'), + [] + ); + + const hiddenColumns = React.useMemo(() => { + return { + // Hide the disambiguation column + columns: [dataset.columns.length], + indicators: false, + // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // Typing doesn't match for handsontable 12.1.0, fixed in 14 + // copyPasteEnabled: false, + }; + }, []); + + const hiddenRows = React.useMemo(() => { + return { + rows: [], + indicators: false, + // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error + // Typing doesn't match for handsontable 12.1.0, fixed in 14 + // copyPasteEnabled: false, + }; + }, []); + + const minSpareRows = React.useMemo( + () => userPreferences.get('workBench', 'editor', 'minSpareRows'), + [] + ); + + const tabMoves = React.useMemo( + () => + userPreferences.get('workBench', 'editor', 'tabMoveDirection') === 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 }, + [] + ); + + const comments = React.useMemo(() => { + return { displayDelay: 100 }; + }, []); + + const hooks = React.useMemo( + () => + getHotHooks( + workbench, + physicalColToMappingCol, + spreadsheetChanged, + checkDeletedFail + ), + [hot] + ); return ( <> { - const tableIcon = mappings?.mappedHeaders?.[physicalCol]; - const isMapped = tableIcon !== undefined; - const mappingCol = physicalColToMappingCol(physicalCol); - const tableName = - (typeof mappingCol === 'number' - ? mappings?.tableNames[mappingCol] - : undefined) ?? - tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; - const tableLabel = isMapped - ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' - : ''; - // REFACTOR: use new table icons - return `
- ${ - isMapped - ? `${tableLabel}` - : `${legacyNonJsxIcons.ban}` - } - - ${dataset.columns[physicalCol]} - -
`; - }} - columns={Array.from( - // Last column is invisible and contains disambiguation metadata - { length: dataset.columns.length + 1 }, - (_, physicalCol) => ({ - // Get data from nth column for nth column - data: physicalCol, - }) - )} + autoWrapCol={autoWrapCol} + autoWrapRow={autoWrapRow} + colHeaders={colHeaders} + columns={columns} commentedCellClassName="htCommentCell" - comments={{ - displayDelay: 100, - }} + comments={comments} data={data as (string | null)[][]} - enterBeginsEditing={userPreferences.get( - 'workBench', - 'editor', - 'enterBeginsEditing' - )} - enterMoves={ - userPreferences.get('workBench', 'editor', 'enterMoveDirection') === - 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 } - } - hiddenColumns={{ - // Hide the disambiguation column - columns: [dataset.columns.length], - indicators: false, - // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error - // Typing doesn't match for handsontable 12.1.0, fixed in 14 - // copyPasteEnabled: false, - }} - hiddenRows={{ - rows: [], - indicators: false, - // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error - // Typing doesn't match for handsontable 12.1.0, fixed in 14 - // copyPasteEnabled: false, - }} + enterBeginsEditing={enterBeginsEditing} + enterMoves={enterMoves} + hiddenColumns={hiddenColumns} + hiddenRows={hiddenRows} invalidCellClassName="-" language={LANGUAGE} licenseKey="non-commercial-and-evaluation" manualColumnMove={true} manualColumnResize={true} - minSpareRows={userPreferences.get( - 'workBench', - 'editor', - 'minSpareRows' - )} + minSpareRows={minSpareRows} multiColumnSorting={true} outsideClickDeselects={false} placeholderCellClassName="htPlaceholder" - ref={hotRef} + ref={setHotTable} rowHeaders={true} - // @ts-expect-error + // @ts-expect-error typing error, possibly fixed in v14 sortIndicator={true} stretchH="all" - tabMoves={ - userPreferences.get('workBench', 'editor', 'tabMoveDirection') === - 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 } - } + tabMoves={tabMoves} readOnly={isUploaded || !hasPermission('/workbench/dataset', 'update')} contextMenu={contextMenuConfig as Settings} {...hooks} @@ -459,18 +518,17 @@ function WbSpreadsheetComponent({ selected.id ); validation?.startValidateRow(disambiguationPhysicalRow!); - hotRef.current?.hotInstance!.render(); + hot?.render(); }} onSelectedAll={(selected): void => // Loop backwards so the live validation will go from top to bottom - hotRef.current!.hotInstance!.batch(() => { + hot?.batch(() => { for ( let visualRow = data.length - 1; visualRow >= 0; visualRow-- ) { - const physicalRow = - hotRef.current!.hotInstance!.toPhysicalRow(visualRow); + const physicalRow = hot?.toPhysicalRow(visualRow); if ( !validation?.uploadResults.ambiguousMatches[ physicalRow @@ -498,4 +556,4 @@ function WbSpreadsheetComponent({ ); } -export const WbSpreadsheet = React.memo(WbSpreadsheetComponent); \ No newline at end of file +export const WbSpreadsheet = React.memo(WbSpreadsheetComponent); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 8372276d5fa..7728cac37c9 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -17,36 +17,19 @@ import Handsontable from 'handsontable'; import { HotTable } from '@handsontable/react'; import { commonText } from '../../localization/common'; -import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; -import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; -import { f } from '../../utils/functools'; -import type { GetSet, IR, RA, WritableArray } from '../../utils/types'; -import { ensure, overwriteReadOnly, writable } from '../../utils/types'; -import { clamp, throttle } from '../../utils/utils'; +import type { GetSet, RA } from '../../utils/types'; +import { clamp } from '../../utils/utils'; import { Button } from '../Atoms/Button'; -import { iconClassName, legacyNonJsxIcons } from '../Atoms/Icons'; import { Link } from '../Atoms/Link'; -import { legacyLoadingContext } from '../Core/Contexts'; -import { Backbone } from '../DataModel/backbone'; -import { getTable, strictGetTable } from '../DataModel/tables'; -import { crash, raise } from '../Errors/Crash'; -import { getIcon, unknownIcon } from '../InitialContext/icons'; -import { Dialog } from '../Molecules/Dialog'; import { hasPermission } from '../Permissions/helpers'; -import { userPreferences } from '../Preferences/userPreferences'; -import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbCellCounts, WbCellMeta } from './CellMeta'; import { Disambiguation } from './DisambiguationLogic'; -import { configureHandsontable, getHotPlugin } from './handsontable'; -import { downloadDataSet } from './helpers'; -import { getHotHooks } from './hooks'; -import { getSelectedRegions } from './hotHelpers'; +import { getHotPlugin } from './handsontable'; import type { WbMapping } from './mapping'; import { parseWbMappings } from './mapping'; -import { fetchWbPickLists } from './pickLists'; import { WbUploaded } from './Results'; import { WbActionsComponent } from '../WbActions/WbActions'; import { WbUtils, WbUtilsComponent } from './WbUtils'; @@ -64,7 +47,7 @@ export type Workbench = { disambiguation: Disambiguation; validation: WbValidation; data: RA>; - hot: Handsontable; + hot: Handsontable | undefined; throttleRate: number; mappings: WbMapping; utils: WbUtils; @@ -73,15 +56,13 @@ export type Workbench = { export function WbViewReact({ dataset, - hotRef, handleDatasetDelete, - triggerRefresh, + triggerDatasetRefresh, spreadsheetContainer, }: { readonly dataset: Dataset; - readonly hotRef: React.RefObject; readonly handleDatasetDelete: () => void; - readonly triggerRefresh: () => void; + readonly triggerDatasetRefresh: () => void; readonly spreadsheetContainer: any; }): JSX.Element { const data = React.useMemo>>( @@ -89,7 +70,13 @@ export function WbViewReact({ dataset.rows.length === 0 ? [Array.from(dataset.columns).fill(null)] : dataset.rows, - [dataset.rows] + [dataset] + ); + + const [hotTable, setHotTable] = React.useState(); + const hot = React.useMemo( + () => (hotTable?.hotInstance ? hotTable?.hotInstance : undefined), + [hotTable] ); const isUploaded = React.useMemo( @@ -106,32 +93,6 @@ export function WbViewReact({ const [hasUnsavedChanges, spreadsheetChanged, spreadsheetUpToDate] = useBooleanState(); - const [hotIsReady, setHotToReady] = useBooleanState(); - - // TODO: try putting a loader until hotRef loads? - React.useEffect(() => { - const getPickLists = async () => - mappings === undefined - ? {} - : await fetchWbPickLists( - dataset.columns, - mappings.tableNames, - mappings.lines - ); - if (hotRef.current) { - setHotToReady(); - const configureHotAndPickLists = async () => { - const pickLists = await getPickLists(); - configureHandsontable( - hotRef.current!.hotInstance!, - mappings, - dataset, - pickLists - ); - }; - configureHotAndPickLists(); // Call the async function - } - }, [hotRef.current]); const [cellCounts, setCellCounts] = React.useState({ newCells: 0, @@ -144,7 +105,7 @@ export function WbViewReact({ const workbench: Workbench = { data, dataset, - hot: hotRef.current?.hotInstance!, + hot: hot, mappings: mappings!, throttleRate, cells: undefined!, @@ -158,27 +119,12 @@ export function WbViewReact({ workbench.validation = new WbValidation(workbench); workbench.utils = new WbUtils(workbench); return workbench; - }, [dataset, hotIsReady]); - - const physicalColToMappingCol = (physicalCol: number): number | undefined => - mappings?.lines.findIndex( - ({ headerName }) => headerName === dataset.columns[physicalCol] - ); + }, [dataset, hot]); - const checkDeletedFail = (statusCode: number): boolean => { + const checkDeletedFail = React.useCallback((statusCode: number): boolean => { if (statusCode === Http.NOT_FOUND) handleDatasetDelete(); return statusCode === Http.NOT_FOUND; - }; - const hooks = React.useMemo( - () => - getHotHooks( - workbench, - physicalColToMappingCol, - spreadsheetChanged, - checkDeletedFail - ), - [dataset, hotIsReady] - ); + }, []); const isMapped = Boolean(dataset.uploadplan); const canUpdate = hasPermission('/workbench/dataset', 'update'); @@ -189,14 +135,18 @@ export function WbViewReact({ // TODO: move all states and logic for Results into a hook const [showResults, _, closeResults, toggleResults] = useBooleanState(); - const initialHiddenRows = React.useMemo(() => { - if (!hotRef.current) return []; - else return getHotPlugin(hotRef.current.hotInstance!, 'hiddenRows').getHiddenRows(); - }, [hotRef.current]); - const initialHiddenCols = React.useMemo(() => { - if (!hotRef.current) return []; - else return getHotPlugin(hotRef.current.hotInstance!, 'hiddenColumns').getHiddenColumns(); - }, [hotRef.current]); + const initialHiddenRows = React.useMemo( + () => + hot === undefined ? [] : getHotPlugin(hot, 'hiddenRows').getHiddenRows(), + [hot] + ); + const initialHiddenCols = React.useMemo( + () => + hot === undefined + ? [] + : getHotPlugin(hot, 'hiddenColumns').getHiddenColumns(), + [hot] + ); // Makes the hot changes required for upload view results React.useEffect(() => { @@ -260,10 +210,9 @@ export function WbViewReact({ - hotRef.current?.hotInstance + hot === undefined ? dataset.rows.length - : hotRef.current!.hotInstance!.countRows() - - hotRef.current!.hotInstance!.countEmptyRows(true) + : hot.countRows() - hot.countEmptyRows(true) } />
@@ -284,7 +233,7 @@ export function WbViewReact({ dataset={dataset} hasUnsavedChanges={hasUnsavedChanges} isUploaded={isUploaded} - onRefresh={triggerRefresh} + onDatasetRefresh={triggerDatasetRefresh} mappings={mappings!} checkDeletedFail={checkDeletedFail} onSpreadsheetUpToDate={spreadsheetUpToDate} @@ -295,26 +244,26 @@ export function WbViewReact({ {showToolkit ? ( ) : undefined}
} + hot={hot} isUploaded={isUploaded} data={data} - validation={workbench.validation} - cells={workbench.cells} - disambiguation={workbench.disambiguation} - hooks={hooks} + workbench={workbench} mappings={mappings!} + checkDeletedFail={checkDeletedFail} + spreadsheetChanged={spreadsheetChanged} />
{showResults ? ( @@ -338,781 +287,3 @@ export function WbViewReact({ ); } - - -// REFACTOR: when rewriting to React, add ErrorBoundaries - -// REFACTOR: rewrite to React -/* eslint-disable functional/no-this-expression */ -export class WbView extends Backbone.View { - public readonly data: RA>; - - public readonly mappings: WbMapping | undefined = undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - public hotIsReady: boolean = false; - - // eslint-disable-next-line functional/prefer-readonly-type - public hotCommentsContainer: HTMLElement | undefined = undefined; - - // eslint-disable-next-line functional/prefer-readonly-type - public undoRedoIsHandled: boolean = false; - - private readonly dataSetMeta: DataSetNameView; - - public readonly wbUtils: WbUtils; - - public readonly cells: WbCellMeta; - - public readonly validation: WbValidation; - - public readonly actions: WbActions; - - public readonly disambiguation: Disambiguation; - - // eslint-disable-next-line functional/prefer-readonly-type - public hot: Handsontable | undefined; - - /* - * If this.isUploaded, render() will: - * Add the "Uploaded" label next to DS Name - * Disable cell editing - * Disable adding/removing rows - * Still allow column sort - * Still allow column move - * - */ - public readonly isUploaded: boolean; - - // Disallow all editing and some tools while this dialog is open - // eslint-disable-next-line functional/prefer-readonly-type - public uploadedView: (() => void) | undefined = undefined; - - // Disallow all editing and all tools while this dialog is open - // eslint-disable-next-line functional/prefer-readonly-type - public coordinateConverterView: (() => void) | undefined = undefined; - - public readonly handleResize: () => void; - - public readonly throttleRate: number; - - // Constructors & Renderers - public constructor( - public readonly dataset: Dataset, - // eslint-disable-next-line functional/prefer-readonly-type - public refreshInitiatedBy: WbStatus | undefined, - // eslint-disable-next-line functional/prefer-readonly-type - public refreshInitiatorAborted: boolean, - public readonly element: HTMLElement, // shouldn't be needed in React - public readonly options: { - readonly onSetUnloadProtect: (unloadProtect: boolean) => void; - readonly onDeleted: () => void; - readonly onDeletedConfirmation: () => void; - readonly display: ( - jsx: JSX.Element, - element?: HTMLElement, - destructor?: () => void - ) => () => void; - } - ) { - super({ - el: element, - tagName: 'section', - events: { - 'click .wb-upload': 'upload', - 'click .wb-validate': 'upload', - 'click .wb-data-check': 'toggleDataCheck', - 'click .wb-show-plan': 'showPlan', - 'click .wb-revert': 'revertChanges', - 'click .wb-save': 'save', - 'click .wb-export-data-set': 'export', - 'click .wb-change-data-set-owner': 'changeOwner', - - 'click .wb-show-upload-view': 'displayUploadedView', - 'click .wb-unupload': 'unupload', - }, - }); - - this.data = - dataset.rows.length === 0 - ? [Array.from(dataset.columns).fill(null)] - : dataset.rows; - /* - * Throttle cell count update depending on the DS size (between 10ms and 2s) - * Even if throttling may not be needed for small Data Sets, wrapping the - * function in throttle allows to not worry about calling it several - * time in a very short amount of time. - * - */ - this.throttleRate = Math.ceil(clamp(10, this.data.length / 10, 2000)); - this.mappings = parseWbMappings(this.dataset); - - this.dataSetMeta = new DataSetNameView({ - dataset: this.dataset, - el: this.el, - display: this.options.display, - getRowCount: () => - this.hot === undefined - ? this.dataset.rows.length - : this.hot.countRows() - this.hot.countEmptyRows(true), - }); - this.wbUtils = new WbUtils(this); - this.cells = new WbCellMeta(this); - this.validation = new WbValidation(this); - this.actions = new WbActions(this); - this.disambiguation = new Disambiguation(this); - - this.isUploaded = - this.dataset.uploadresult !== null && this.dataset.uploadresult.success; - - this.refreshInitiatedBy = refreshInitiatedBy; - this.refreshInitiatorAborted = refreshInitiatorAborted; - - this.handleResize = throttle(() => this.hot?.render(), this.throttleRate); - } - - private readonly hooks = getHotHooks(this); - - render() { - this.$el.append( - wbViewTemplate( - this.isUploaded, - Boolean(this.dataset.uploadplan), - this.dataset.id - ) - ); - this.$el.attr('aria-label', wbText.workBench()); - - /* - * HOT Comments for last column overflow outside the viewport for a moment - * before getting repositioned by the afterOnCellMouseOver event handler. - * - * Hiding overflow prevents scroll bar from flickering on cell mouse over - * - */ - document.body.classList.add('overflow-x-hidden'); - - this.dataSetMeta.render(); - - if (typeof this.dataset.uploaderstatus === 'string') - this.actions.openStatus(this.dataset.uploaderstatus); - - this.cells.cellMeta = []; - - if (this.refreshInitiatedBy && this.refreshInitiatorAborted) - this.actions.operationAbortedMessage(); - - const initDataModelIntegration = async (): Promise => { - const pickLists = - this.mappings === undefined - ? {} - : await fetchWbPickLists( - this.dataset.columns, - this.mappings.tableNames, - this.mappings.lines - ); - this.hot?.batch(() => { - if ( - !this.isUploaded && - (this.mappings?.lines ?? []).length === 0 && - hasPermission('/workbench/dataset', 'update') - ) { - const dialog = this.options.display( - - {commonText.close()} - - {commonText.create()} - - - } - header={wbPlanText.noUploadPlan()} - onClose={() => dialog()} - > - {wbPlanText.noUploadPlanDescription()} - - ); - this.$('.wb-validate, .wb-data-check') - .prop('disabled', true) - .prop('title', wbText.wbValidateUnavailable()); - } else { - this.$('.wb-validate, .wb-data-check').prop('disabled', false); - this.$('.wb-show-upload-view') - .prop('disabled', false) - .prop('title', undefined); - } - - /* - * These methods update HOT's cells settings, which resets meta data - * Thus, need to run them first - */ - configureHandsontable( - this.hot!, - this.mappings, - this.dataset, - pickLists - ); - - console.log(this.dataset, 'LOOK DATASET'); - - if (this.dataset.rowresults) this.validation.getValidationResults(); - - // CHeck if any column is reordered - if (this.dataset.visualorder?.some((column, index) => column !== index)) - this.hot?.updateSettings({ - manualColumnMove: writable(this.dataset.visualorder), - }); - - this.wbUtils.findLocalityColumns(); - this.wbUtils.render(); - - this.hotIsReady = true; - - this.hotCommentsContainer = (document.getElementsByClassName( - 'htComments' - )[0] ?? undefined) as HTMLElement | undefined; - }); - }; - - legacyLoadingContext( - this.initHot().then(initDataModelIntegration).catch(crash) - ); - - this.validation.updateValidationButton(); - if (this.validation.validationMode === 'static' && !this.isUploaded) - this.wbUtils.toggleCellTypes('invalidCells', 'remove'); - - this.cells.flushIndexedCellData = true; - globalThis.addEventListener('resize', this.handleResize); - - return this; - } - - // Initialize Handsontable - async initHot(): Promise { - /* - * HOT and Backbone appear to conflict, unless HOT init is wrapped in - * globalThis.setTimeout(...,0) - */ - await new Promise((resolve) => setTimeout(resolve, 0)); - const hot = new Handsontable(this.$('.wb-spreadsheet')[0], { - // eslint-disable-next-line functional/prefer-readonly-type - data: this.data as (string | null)[][], - columns: Array.from( - // Last column is invisible and contains disambiguation metadata - { length: this.dataset.columns.length + 1 }, - (_, physicalCol) => ({ - // Get data from nth column for nth column - data: physicalCol, - }) - ), - colHeaders: (physicalCol) => { - const tableIcon = this.mappings?.mappedHeaders?.[physicalCol]; - const isMapped = tableIcon !== undefined; - const mappingCol = this.physicalColToMappingCol(physicalCol); - const tableName = - (typeof mappingCol === 'number' - ? this.mappings?.tableNames[mappingCol] - : undefined) ?? - tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; - const tableLabel = isMapped - ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' - : ''; - // REFACTOR: use new table icons - return `
- ${ - isMapped - ? `${tableLabel}` - : `${legacyNonJsxIcons.ban}` - } - - ${this.dataset.columns[physicalCol]} - -
`; - }, - hiddenColumns: { - // Hide the disambiguation column - columns: [this.dataset.columns.length], - indicators: false, - // @ts-expect-error Wrong Handsontable typing? - copyPasteEnabled: false, - }, - hiddenRows: { - rows: [], - indicators: false, - // @ts-expect-error Wrong Handsontable typing? - copyPasteEnabled: false, - }, - /* - * Number of blanks rows at the bottom of the spreadsheet. - * (allows to add new rows easily) - */ - minSpareRows: userPreferences.get('workBench', 'editor', 'minSpareRows'), - comments: { - displayDelay: 100, - }, - /* - * Need htCommentCell to apply default HOT comment box styles - * - * Since comments are only used for invalid cells, comment boxes - * contain the styles of invalid cells - * - */ - commentedCellClassName: 'htCommentCell', - placeholderCellClassName: 'htPlaceholder', - /* - * Disable default styles. The only type of front-end invalid cell - * right now is non-existed value for a read-only picklist. The error - * message for that case is handled separately - */ - invalidCellClassName: '-', - rowHeaders: true, - autoWrapCol: userPreferences.get('workBench', 'editor', 'autoWrapCol'), - autoWrapRow: userPreferences.get('workBench', 'editor', 'autoWrapRow'), - enterBeginsEditing: userPreferences.get( - 'workBench', - 'editor', - 'enterBeginsEditing' - ), - enterMoves: - userPreferences.get('workBench', 'editor', 'enterMoveDirection') === - 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 }, - tabMoves: - userPreferences.get('workBench', 'editor', 'tabMoveDirection') === 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 }, - manualColumnResize: true, - manualColumnMove: true, - outsideClickDeselects: false, - multiColumnSorting: true, - sortIndicator: true, - language: LANGUAGE, - // @ts-expect-error Wrong Handsontable typing? - contextMenu: { - items: ensure< - IR< - | Handsontable.plugins.ContextMenu.MenuItemConfig - | Handsontable.plugins.ContextMenu.PredefinedMenuItemKey - > - >()( - this.isUploaded - ? ({ - // Display uploaded record - upload_results: { - disableSelection: true, - isCommand: false, - renderer: (_hot, wrapper) => { - const { endRow: visualRow, endCol: visualCol } = - getSelectedRegions(hot).at(-1) ?? {}; - const physicalRow = hot.toPhysicalRow(visualRow ?? 0); - const physicalCol = hot.toPhysicalColumn(visualCol ?? 0); - - const createdRecords = - this.validation.uploadResults.newRecords[physicalRow]?.[ - physicalCol - ]; - - if ( - visualRow === undefined || - visualCol === undefined || - createdRecords === undefined || - !this.cells.getCellMeta(physicalRow, physicalCol, 'isNew') - ) { - wrapper.textContent = wbText.noUploadResultsAvailable(); - wrapper.parentElement?.classList.add('htDisabled'); - const span = document.createElement('span'); - span.style.display = 'none'; - return span; - } - - wrapper.setAttribute( - 'class', - `${wrapper.getAttribute('class')} flex flex-col !m-0 - pb-1 wb-uploaded-view-context-menu` - ); - wrapper.innerHTML = createdRecords - .map(([tableName, recordId, label]) => { - const tableLabel = - label === '' - ? strictGetTable(tableName).label - : label; - // REFACTOR: use new table icons - const tableIcon = getIcon(tableName) ?? unknownIcon; - - return ` - - ${tableLabel} - ${legacyNonJsxIcons.link} - `; - }) - .join(''); - - const div = document.createElement('div'); - div.style.display = 'none'; - return div; - }, - }, - } as const) - : ({ - row_above: { - disabled: () => - typeof this.uploadedView === 'function' || - typeof this.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update'), - }, - row_below: { - disabled: () => - typeof this.uploadedView === 'function' || - typeof this.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update'), - }, - remove_row: { - disabled: () => { - if ( - typeof this.uploadedView === 'function' || - typeof this.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update') - ) - return true; - // Or if called on the last row - const selectedRegions = getSelectedRegions(hot); - return ( - selectedRegions.length === 1 && - selectedRegions[0].startRow === this.data.length - 1 && - selectedRegions[0].startRow === selectedRegions[0].endRow - ); - }, - }, - disambiguate: { - name: wbText.disambiguate(), - disabled: (): boolean => - typeof this.uploadedView === 'function' || - typeof this.coordinateConverterView === 'function' || - !this.disambiguation.isAmbiguousCell() || - !hasPermission('/workbench/dataset', 'update'), - callback: () => - this.disambiguation.openDisambiguationDialog(), - }, - separator_1: '---------', - fill_down: this.wbUtils.fillCellsContextMenuItem('down'), - fill_up: this.wbUtils.fillCellsContextMenuItem('up'), - separator_2: '---------', - undo: { - disabled: () => - typeof this.uploadedView === 'function' || - this.hot?.isUndoAvailable() !== true || - !hasPermission('/workbench/dataset', 'update'), - }, - redo: { - disabled: () => - typeof this.uploadedView === 'function' || - this.hot?.isRedoAvailable() !== true || - !hasPermission('/workbench/dataset', 'update'), - }, - } as const) - ), - }, - licenseKey: 'non-commercial-and-evaluation', - stretchH: 'all', - readOnly: - this.isUploaded || !hasPermission('/workbench/dataset', 'update'), - ...Object.fromEntries( - Object.entries(this.hooks).map( - ([name, callback]) => [name, callback.bind(this)] as const - ) - ), - }); - this.hot = hot; - } - - remove(): this { - this.hot?.destroy(); - this.hot = undefined; - this.wbUtils.remove(); - this.dataSetMeta.remove(); - this.uploadedView?.(); - this.actions.status?.(); - this.validation.stopLiveValidation(); - globalThis.removeEventListener('resize', this.handleResize); - document.body.classList.remove('overflow-x-hidden'); - Backbone.View.prototype.remove.call(this); - return this; - } - - // Tools - displayUploadedView(event: MouseEvent): void { - const target = event.target as HTMLElement | null; - if (!this.dataset.rowresults || this.hot === undefined) return; - - if (this.uploadedView !== undefined) { - this.uploadedView(); - return; - } - - if (this.validation.liveValidationStack.length > 0) { - const dialog = this.options.display( - dialog()} - > - {wbText.unavailableWhileValidating()} - - ); - return; - } - - const effects: WritableArray<() => void> = []; - const effectsCleanup: WritableArray<() => void> = []; - - const elementsToDisable = [ - ...[ - 'wb-data-check', - 'wb-replace-value', - 'wb-convert-coordinates', - 'wb-geolocate', - ].map((className) => this.el.getElementsByClassName(className)[0]), - ...Array.from(document.getElementsByClassName('wb-navigation-section')) - .filter((element) => - ['modifiedCells', 'invalidCells'].includes( - element.getAttribute('data-navigation-type') ?? '' - ) - ) - .flatMap((element) => - Array.from(element.getElementsByClassName('wb-cell-navigation')) - ), - ] - .filter((element) => element !== undefined) - .map( - (element) => - [element as HTMLButtonElement, element.getAttribute('title')] as const - ); - - effects.push(() => - elementsToDisable.forEach(([element]) => { - if (element === undefined || element === null) return; - element.disabled = true; - element.setAttribute('title', wbText.unavailableWhileViewingResults()); - }) - ); - effectsCleanup.push(() => - elementsToDisable.forEach(([element, title]) => { - if (element === undefined || element === null) return; - element.disabled = false; - element.setAttribute('title', title ?? ''); - }) - ); - - const isReadOnly = this.hot.getSettings().readOnly; - effects.push(() => this.hot!.updateSettings({ readOnly: true })); - effectsCleanup.push(() => - this.hot?.updateSettings({ readOnly: isReadOnly }) - ); - - const initialHiddenRows = getHotPlugin( - this.hot, - 'hiddenRows' - ).getHiddenRows(); - const initialHiddenCols = getHotPlugin( - this.hot, - 'hiddenColumns' - ).getHiddenColumns(); - const rowsToInclude = new Set(); - const colsToInclude = new Set(); - Object.entries(this.cells.cellMeta).forEach(([physicalRow, rowMeta]) => - rowMeta.forEach((metaArray, physicalCol) => { - if (!this.cells.getCellMetaFromArray(metaArray, 'isNew')) return; - rowsToInclude.add((physicalRow as unknown as number) | 0); - colsToInclude.add(physicalCol); - }) - ); - const rowsToHide = this.data - .map((_, physicalRow) => physicalRow) - .filter( - (physicalRow) => - !rowsToInclude.has(physicalRow) && - !initialHiddenRows.includes(physicalRow) - ) - .map(this.hot.toVisualRow); - const colsToHide = this.dataset.columns - .map((_, physicalCol) => physicalCol) - .filter( - (physicalCol) => - !colsToInclude.has(physicalCol) && - !initialHiddenCols.includes(physicalCol) - ) - .map(this.hot.toVisualColumn); - - effects.push(() => { - if (this.hot === undefined) return; - getHotPlugin(this.hot, 'hiddenRows').hideRows(rowsToHide); - getHotPlugin(this.hot, 'hiddenColumns').hideColumns(colsToHide); - }); - effectsCleanup.push(() => { - if (this.hot === undefined) return; - getHotPlugin(this.hot, 'hiddenRows').showRows( - rowsToHide.filter((visualRow) => !initialHiddenRows.includes(visualRow)) - ); - getHotPlugin(this.hot, 'hiddenColumns').showColumns( - colsToHide.filter((visualCol) => !initialHiddenCols.includes(visualCol)) - ); - }); - - const newCellsAreHidden = this.el.classList.contains('wb-hide-new-cells'); - effects.push(() => this.wbUtils.toggleCellTypes('newCells', 'remove')); - effectsCleanup.push(() => - newCellsAreHidden - ? this.wbUtils.toggleCellTypes('newCells', 'add') - : undefined - ); - - effects.push(() => { - target?.toggleAttribute('aria-pressed', true); - }); - effectsCleanup.push(() => { - target?.toggleAttribute('aria-pressed', false); - }); - - effects.push(() => this.el.classList.add('wb-show-upload-results')); - effectsCleanup.push(() => - this.el.classList.remove('wb-show-upload-results') - ); - - const uploadedViewWrapper = this.el.getElementsByClassName( - 'wb-uploaded-view-wrapper' - )[0]; - effects.push(() => { - uploadedViewWrapper.classList.remove('hidden'); - uploadedViewWrapper.classList.add('contents'); - }); - effectsCleanup.push(() => { - uploadedViewWrapper.classList.remove('contents'); - uploadedViewWrapper.classList.add('hidden'); - }); - - const runEffects = () => - [...effects, this.hot?.render.bind(this.hot) ?? f.void].forEach( - (effect) => effect() - ); - const runCleanup = () => - // If WbView.remove() was called, this.hot would be undefined here - [...effectsCleanup, this.hot?.render.bind(this.hot)].forEach( - (effectCleanup) => effectCleanup?.() - ); - - const container = document.createElement('div'); - this.uploadedView = this.options.display( - this.uploadedView?.()} - />, - container, - () => { - this.uploadedView = undefined; - runCleanup(); - } - ); - uploadedViewWrapper.append(container); - - runEffects(); - } - - protected changeOwner(): void { - this.dataSetMeta.changeOwner(); - } - - protected upload(event: MouseEvent): void { - this.actions.upload(event); - } - - protected unupload(): void { - this.actions.unupload(); - } - - protected toggleDataCheck(event: MouseEvent): void { - this.validation.toggleDataCheck(event); - } - - protected revertChanges(): void { - this.actions.revertChanges(); - } - - protected save(): void { - this.actions.save().catch(raise); - } - - protected export(): void { - const delimiter = userPreferences.get( - 'workBench', - 'editor', - 'exportFileDelimiter' - ); - - downloadDataSet( - this.dataset.name, - this.dataset.rows, - this.dataset.columns, - delimiter - ).catch(raise); - } - - // Helpers - /* - * MappingCol is the index of the lines line corresponding to - * a particular physicalCol. Since there can be unmapped columns, these - * indexes do not line up and need to be converted like this: - */ - public physicalColToMappingCol(physicalCol: number): number | undefined { - return this.mappings?.lines.findIndex( - ({ headerName }) => headerName === this.dataset.columns[physicalCol] - ); - } - - // Check if AJAX failed because Data Set was deleted - public checkDeletedFail(statusCode: number): boolean { - if (statusCode === Http.NOT_FOUND) this.options.onDeleted(); - return statusCode === Http.NOT_FOUND; - } - - // For debugging only. Used in the constructor in the events section - public showPlan(): void { - const dialog = this.options.display( - { - overwriteReadOnly(this.dataset, 'uploadplan', plan); - this.trigger('refresh'); - }} - onClose={(): void => dialog()} - onDeleted={this.options.onDeleted} - /> - ); - } -} - -/* eslint-enable functional/no-this-expression */ diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 10698c514db..67ccd5145ec 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -34,7 +34,7 @@ export function getHotHooks( * */ afterRenderer: ( - td: any, + td: HTMLTableCellElement, visualRow: number, visualCol: number, property: number | string, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 78b9d4dbdaa..9b02a274415 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -52,15 +52,16 @@ export function WorkBenchReact(): JSX.Element { useErrorContext('dataSet', dataSet); const loading = React.useContext(LoadingContext); const [isDeleted, handleDeleted] = useBooleanState(); + // TODO: figure out how handleDeletedConfirmation was being used in Backbone. possibly not used at all const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); const navigate = useNavigate(); - const hotRef = React.useRef(null); + // TODO: consider adding this to workbench variable const spreadsheetContainer = React.useRef(null); if (!dataSet || !treeRanksLoaded) return ; - const triggerRefresh = () => { + const triggerDatasetRefresh = () => { loading(fetchDataSet(dataSet!.id).then(setDataSet)); }; @@ -82,9 +83,8 @@ export function WorkBenchReact(): JSX.Element {
From 015c89456797dd817c774d1f256a94ec8459363d Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:44:44 -0400 Subject: [PATCH 055/153] Add ReadOnlyContext to workbench --- .../lib/components/WbActions/WbResults.tsx | 2 +- .../components/WorkBench/WbSpreadsheet.tsx | 52 ++---- .../lib/components/WorkBench/WbUtils.tsx | 15 +- .../lib/components/WorkBench/WbView.tsx | 150 +++++++++--------- 4 files changed, 99 insertions(+), 120 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx index c710e807925..48d335cd8bd 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx @@ -18,7 +18,7 @@ export function WbResults({ {commonText.results()} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 3b07d28e84e..a49a42c1b77 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -42,6 +42,7 @@ import { mappingPathToString } from '../WbPlanView/mappingHelpers'; import { MappingPath } from '../WbPlanView/Mapper'; import { registerAllModules } from 'handsontable/registry'; import { Workbench } from './WbView'; +import { ReadOnlyContext } from '../Core/Contexts'; registerAllModules(); @@ -104,6 +105,7 @@ function WbSpreadsheetComponent({ readonly checkDeletedFail: any; readonly spreadsheetChanged: any; }): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); const physicalColToMappingCol = (physicalCol: number): number | undefined => mappings?.lines.findIndex( ({ headerName }) => headerName === dataset.columns[physicalCol] @@ -260,28 +262,14 @@ function WbSpreadsheetComponent({ } as const) : ({ row_above: { - disabled: () => - // TODO: Figure out if these commented lines can be removed - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update'), + disabled: () => isReadOnly, }, row_below: { - disabled: () => - // TODO: Figure out if these commented lines can be removed - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update'), + disabled: () => isReadOnly, }, remove_row: { disabled: () => { - if ( - // TODO: Figure out if these commented lines can be removed - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update') - ) - return true; + if (isReadOnly) return true; // Or if called on the last row const selectedRegions = getSelectedRegions(hot!); return ( @@ -294,11 +282,7 @@ function WbSpreadsheetComponent({ disambiguate: { name: wbText.disambiguate(), disabled: (): boolean => - // TODO: Figure out if these commented lines can be removed - // typeof this.uploadedView === 'function' || - // typeof this.coordinateConverterView === 'function' || - !disambiguation.isAmbiguousCell() || - !hasPermission('/workbench/dataset', 'update'), + !disambiguation.isAmbiguousCell() || isReadOnly, callback: () => openDisambiguationDialog(), }, separator_1: '---------', @@ -306,18 +290,10 @@ function WbSpreadsheetComponent({ fill_up: fillCellsContextMenuItem(hot!, 'up'), separator_2: '---------', undo: { - disabled: () => - // TODO: Figure out if these commented lines can be removed - // typeof this.uploadedView === 'function' || - hot?.isUndoAvailable() !== true || - !hasPermission('/workbench/dataset', 'update'), + disabled: () => hot?.isUndoAvailable() !== true || isReadOnly, }, redo: { - disabled: () => - // TODO: Figure out if these commented lines can be removed - // typeof this.uploadedView === 'function' || - hot?.isRedoAvailable() !== true || - !hasPermission('/workbench/dataset', 'update'), + disabled: () => hot?.isRedoAvailable() !== true || isReadOnly, }, } as const) ), @@ -421,9 +397,8 @@ function WbSpreadsheetComponent({ // Hide the disambiguation column columns: [dataset.columns.length], indicators: false, - // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error - // Typing doesn't match for handsontable 12.1.0, fixed in 14 - // copyPasteEnabled: false, + // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 + copyPasteEnabled: false, }; }, []); @@ -431,9 +406,8 @@ function WbSpreadsheetComponent({ return { rows: [], indicators: false, - // TODO: Temporarily disabled as copyPasteEnabled throws an error despite having ts-expect-error - // Typing doesn't match for handsontable 12.1.0, fixed in 14 - // copyPasteEnabled: false, + // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 + copyPasteEnabled: false, }; }, []); @@ -494,7 +468,7 @@ function WbSpreadsheetComponent({ sortIndicator={true} stretchH="all" tabMoves={tabMoves} - readOnly={isUploaded || !hasPermission('/workbench/dataset', 'update')} + readOnly={isReadOnly} contextMenu={contextMenuConfig as Settings} {...hooks} /> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index cb2694edbd1..8f6c7b4937c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -6,7 +6,7 @@ * */ -import React from 'react'; +import React, { KeyboardEventHandler } from 'react'; import _ from 'underscore'; import { wbText } from '../../localization/workbench'; @@ -30,6 +30,7 @@ import { className } from '../Atoms/className'; import { commonText } from '../../localization/common'; import { useBooleanState } from '../../hooks/useBooleanState'; import { icons } from '../Atoms/Icons'; +import { ReadOnlyContext } from '../Core/Contexts'; function Navigation({ name, @@ -44,6 +45,7 @@ function Navigation({ readonly utils: WbUtils; readonly spreadsheetContainer: any; }): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); const [currentPosition, setCurrentPosition] = React.useState(0); const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); const handleTypeToggle = () => { @@ -79,6 +81,7 @@ function Navigation({ data-navigation-direction="previous" variant="bg-inherit text-gray-800 dark:text-gray-100" onClick={handlePrevious} + disabled={name !== 'newCells' && isReadOnly} > {icons.chevronLeft} @@ -94,11 +97,8 @@ function Navigation({ variant="bg-inherit text-gray-800 dark:text-gray-100" onClick={handleTypeToggle} > - {label} ( - - {currentPosition} - - /{totalCount}) + {label} ({currentPosition}/ + {totalCount}) {icons.chevronRight} @@ -124,6 +125,7 @@ export function WbUtilsComponent({ readonly utils: WbUtils; readonly spreadsheetContainer: any; }): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); const searchRef = React.useRef(null); const replaceRef = React.useRef(null); @@ -169,6 +171,7 @@ export function WbUtilsComponent({ placeholder={wbText.replace()} title={wbText.replacementValue()} onKeyDown={handleReplace} + disabled={isReadOnly} />
) : undefined} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 7728cac37c9..c6515cffd49 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -38,6 +38,7 @@ import { DataSetName } from './DataSetMeta'; import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; import { WbToolkit } from '../WbToolkit/WbToolkit'; +import { ReadOnlyContext } from '../Core/Contexts'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -179,7 +180,6 @@ export function WbViewReact({ .map(workbench.hot.toVisualColumn); if (showResults) { - workbench.hot?.updateSettings({ readOnly: true }); getHotPlugin(workbench.hot, 'hiddenRows').hideRows(rowsToHide); getHotPlugin(workbench.hot, 'hiddenColumns').hideColumns(colsToHide); @@ -202,88 +202,90 @@ export function WbViewReact({ return ( <> -
-
- +
+
+ + hot === undefined + ? dataset.rows.length + : hot.countRows() - hot.countEmptyRows(true) + } + /> +
+ + {commonText.tools()} + + + {canUpdate || isMapped ? ( + + {wbPlanText.dataMapper()} + + ) : undefined} + - hot === undefined - ? dataset.rows.length - : hot.countRows() - hot.countEmptyRows(true) - } + hasUnsavedChanges={hasUnsavedChanges} + isUploaded={isUploaded} + onDatasetRefresh={triggerDatasetRefresh} + mappings={mappings!} + checkDeletedFail={checkDeletedFail} + onSpreadsheetUpToDate={spreadsheetUpToDate} + workbench={workbench} + onToggleResults={toggleResults} />
- - {commonText.tools()} - - - {canUpdate || isMapped ? ( - - {wbPlanText.dataMapper()} - - ) : undefined} - -
- {showToolkit ? ( - - ) : undefined} -
-
- } hot={hot} - isUploaded={isUploaded} - data={data} - workbench={workbench} mappings={mappings!} - checkDeletedFail={checkDeletedFail} - spreadsheetChanged={spreadsheetChanged} + data={data} + handleDatasetDelete={handleDatasetDelete} + hasUnsavedChanges={hasUnsavedChanges} + triggerDatasetRefresh={triggerDatasetRefresh} /> -
- {showResults ? ( - - ) : undefined} -
- + + {showResults ? ( + + ) : undefined} +
+ + ); } From 467cdf8d611278e5a1f43321b61809bc7e8d0c3f Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 12 Apr 2024 14:57:21 -0400 Subject: [PATCH 056/153] Use readonly context for fillCellsContextMenuItem --- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index a49a42c1b77..0a324364aa2 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -49,14 +49,13 @@ registerAllModules(); // Context menu item definitions (common for fillUp and fillDown) const fillCellsContextMenuItem = ( hot: Handsontable, - mode: 'down' | 'up' + mode: 'down' | 'up', + isReadOnly: boolean ): Handsontable.plugins.ContextMenu.MenuItemConfig => { return { name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), disabled: () => - // typeof this.wbView.uploadedView === 'function' || - // typeof this.wbView.coordinateConverterView === 'function' || - !hasPermission('/workbench/dataset', 'update') || + isReadOnly || (hot.getSelected()?.every((selection) => selection[0] === selection[2]) ?? false), callback: (_, selections) => @@ -286,8 +285,8 @@ function WbSpreadsheetComponent({ callback: () => openDisambiguationDialog(), }, separator_1: '---------', - fill_down: fillCellsContextMenuItem(hot!, 'down'), - fill_up: fillCellsContextMenuItem(hot!, 'up'), + fill_down: fillCellsContextMenuItem(hot!, 'down', isReadOnly), + fill_up: fillCellsContextMenuItem(hot!, 'up', isReadOnly), separator_2: '---------', undo: { disabled: () => hot?.isUndoAvailable() !== true || isReadOnly, From b0b35f049419a7f75561344f466255b5ff894830 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:26:36 -0400 Subject: [PATCH 057/153] Add improvements - typings - better variable names - minor refactoring --- .../js_src/lib/components/Router/Routes.tsx | 2 +- .../lib/components/WbActions/WbActions.tsx | 12 +- .../lib/components/WbActions/WbSave.tsx | 2 +- .../lib/components/WbActions/WbValidate.tsx | 2 +- .../lib/components/WbToolkit/DevShowPlan.tsx | 29 +-- .../lib/components/WbToolkit/GeoLocate.tsx | 15 +- .../lib/components/WbToolkit/WbLeafletMap.tsx | 4 +- .../lib/components/WbToolkit/WbToolkit.tsx | 24 +-- .../lib/components/WorkBench/CellMeta.ts | 13 +- .../lib/components/WorkBench/DataSetMeta.tsx | 12 +- .../components/WorkBench/WbSpreadsheet.tsx | 30 ++- .../lib/components/WorkBench/WbUtils.tsx | 183 +++++++++--------- .../lib/components/WorkBench/WbValidation.tsx | 6 +- .../lib/components/WorkBench/WbView.tsx | 26 ++- .../js_src/lib/components/WorkBench/hooks.ts | 81 ++++---- .../js_src/lib/components/WorkBench/index.tsx | 15 +- 16 files changed, 221 insertions(+), 235 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx index 00f668560ec..b2ad3765208 100644 --- a/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx +++ b/specifyweb/frontend/js_src/lib/components/Router/Routes.tsx @@ -200,7 +200,7 @@ export const routes: RA = [ { path: ':id', element: () => - import('../WorkBench/index').then(({ WorkBenchReact }) => WorkBenchReact), + import('../WorkBench/index').then(({ WorkBench }) => WorkBench), }, { path: 'import', diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index a4c02894164..2a7e2a6bddb 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -23,6 +23,7 @@ import { WbRollback } from './WbRollback'; import { WbUpload } from './WbUpload'; import { WbRevert } from './WbRevert'; import { WbSave } from './WbSave'; +import { GET } from '../../utils/utils'; export function useWbActions({ datasetId, @@ -118,10 +119,11 @@ export function WbActionsComponent({ workbench, }); - const cells = workbench.cells; + // TODO: put the message rendering logic in a utility function? + const cellCounts = workbench.cellCounts[GET]; const messages = { validate: - cells.cellCounts?.invalidCells === 0 + cellCounts.invalidCells === 0 ? { header: wbText.validationNoErrors(), message: ( @@ -145,7 +147,7 @@ export function WbActionsComponent({ ), }, upload: - cells.cellCounts?.invalidCells === 0 + cellCounts.invalidCells === 0 ? { header: wbText.uploadSuccessful(), message: wbText.uploadSuccessfulDescription(), @@ -237,7 +239,7 @@ export function WbActionsComponent({ /> ) : undefined} - {modeRef.current && showStatus ? ( + {typeof modeRef.current === 'string' && showStatus ? ( - {cells.cellCounts?.invalidCells === 0 && + {cellCounts.invalidCells === 0 && modeRef.current === 'upload' && ( diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx index dfe478b6616..aa11eddfb17 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx @@ -1,3 +1,7 @@ +/** + * Show upload plan as JSON + */ + import React from 'react'; import { commonText } from '../../localization/common'; @@ -16,28 +20,28 @@ import { useBooleanState } from '../../hooks/useBooleanState'; import { wbText } from '../../localization/workbench'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; -export function WbDevPlan({ +export function WbRawPlan({ dataset, - handleDatasetDelete, + onDatasetDeleted: handleDatasetDeleted, triggerDatasetRefresh, }: { readonly dataset: Dataset; - readonly handleDatasetDelete: () => void; + readonly onDatasetDeleted: () => void; readonly triggerDatasetRefresh: () => void; }): JSX.Element { - const [showDevPlan, openDevPlan, closeDevPlan] = useBooleanState(); + const [showRawPlan, openRawPlan, closeRawPlan] = useBooleanState(); return ( <> {wbText.uploadPlan()} - {showDevPlan && ( - )} @@ -54,10 +58,7 @@ export function WbDevPlan({ ); } -/** - * Show upload plan as JSON - */ -function DevShowPlan({ +function RawUploadPlan({ dataSetId, dataSetName: name, uploadPlan: rawPlan, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index 6737bc53465..d056c1a99fa 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -77,14 +77,16 @@ function GeoLocate({ const [localityIndex, setLocalityIndex] = React.useState(0); const selection = React.useMemo( - () => getSelectedLocalities(hot, columns, localityColumns, true), + () => + hot === undefined + ? undefined + : getSelectedLocalities(hot, columns, localityColumns, true), [hot, columns, localityColumns] ); function handleMove(newLocalityIndex: number): void { - if (selection === undefined) return; const { localityColumns, visualRow } = - selection.parseLocalityIndex(newLocalityIndex); + selection!.parseLocalityIndex(newLocalityIndex); setData(getGeoLocateData(hot, columns, { localityColumns, visualRow })); hot.selectRows(visualRow); setLocalityIndex(newLocalityIndex); @@ -96,7 +98,8 @@ function GeoLocate({ ); React.useEffect(() => { - handleMove(0); + if (selection === undefined || hot === undefined) return; + else handleMove(0); }, [hot, columns, localityColumns]); const handleResult = React.useCallback( @@ -155,7 +158,7 @@ function GeoLocate({ // Generate Locality iterator. Able to handle multiple localities in a row export function getSelectedLocalities( - hot: Handsontable | null | undefined, + hot: Handsontable, columns: RA, localityColumns: RA>, // If false, treat single cell selection as entire spreadsheet selection @@ -173,7 +176,7 @@ export function getSelectedLocalities( }; } | undefined { - if (!hot) return undefined; + if (hot === undefined) return undefined; const selectedRegions = getSelectedRegions(hot); const selectedVirtualColumns = f.unique( diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx index 0a54f99cd63..ac9b96e4182 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx @@ -20,7 +20,7 @@ export function WbLeafletMap({ mappings, }: { readonly hasLocality: boolean; - readonly hot: Handsontable | null | undefined; + readonly hot: Handsontable; readonly dataset: Dataset; readonly mappings: WbMapping; }): JSX.Element { @@ -33,7 +33,7 @@ export function WbLeafletMap({ false ); - if (!selection) return undefined; + if (selection === undefined) return undefined; return getLocalitiesDataFromSpreadsheet( mappings.localityColumns, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx index 94316776217..5a60e33170d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx @@ -13,7 +13,7 @@ import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; import { downloadDataSet } from '../WorkBench/helpers'; import type { WbMapping } from '../WorkBench/mapping'; -import { WbDevPlan } from './DevShowPlan'; +import { WbRawPlan } from './DevShowPlan'; import { WbGeoLocate } from './GeoLocate'; import { WbLeafletMap } from './WbLeafletMap'; @@ -22,7 +22,7 @@ export function WbToolkit({ hot, mappings, data, - handleDatasetDelete, + onDatasetDeleted: handleDatasetDeleted, hasUnsavedChanges, triggerDatasetRefresh, }: { @@ -30,11 +30,11 @@ export function WbToolkit({ readonly hot: Handsontable | undefined; readonly mappings: WbMapping; readonly data: RA>; - readonly handleDatasetDelete: () => void; + readonly onDatasetDeleted: () => void; readonly hasUnsavedChanges: boolean; readonly triggerDatasetRefresh: () => void; }): JSX.Element { - const handleExport = React.useCallback((): void => { + const handleExport = (): void => { const delimiter = userPreferences.get( 'workBench', 'editor', @@ -47,9 +47,11 @@ export function WbToolkit({ dataset.columns, delimiter ).catch(raise); - }, [dataset]); + }; const hasLocality = mappings ? mappings.localityColumns.length > 0 : false; + + // TODO: Render certain elements only after hot has loaded return (
- ) : undefined} + diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index 962a876edee..472b77ba7b8 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -1,6 +1,6 @@ import { backEndText } from '../../localization/backEnd'; import type { RA, WritableArray } from '../../utils/types'; -import { throttle } from '../../utils/utils'; +import { SET, throttle } from '../../utils/utils'; import { getHotPlugin } from './handsontable'; import type { Workbench } from './WbView'; @@ -193,7 +193,7 @@ export class WbCellMeta { visualRow: number, visualCol: number ) { - if (!this.workbench.hot) return; + if (this.workbench.hot === undefined) return; if (key === 'isNew') cell?.classList[value === true ? 'add' : 'remove']('wb-no-match-cell'); @@ -251,7 +251,7 @@ export class WbCellMeta { readonly visualCol?: number; } = {} ) { - if (!this.workbench.hot) return; + if (this.workbench.hot === undefined) return; const isValueChanged = this.setCellMeta( physicalRow, physicalCol, @@ -328,7 +328,7 @@ export class WbCellMeta { updateCellInfoStats() { const cellMeta = this.cellMeta.flat(); - const cellCounts = { + this.workbench.cellCounts[SET]({ newCells: cellMeta.reduce( (count, info) => count + (this.getCellMetaFromArray(info, 'isNew') ? 1 : 0), @@ -350,10 +350,7 @@ export class WbCellMeta { count + (this.getCellMetaFromArray(info, 'isModified') ? 1 : 0), 0 ), - }; - - const [_, setCellCounts] = this.workbench.cellCounts; - setCellCounts(cellCounts); + }); } public cellIsType(metaArray: WbMetaArray, type: keyof WbCellCounts): boolean { diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx index 87527bc8703..7aa11bafea1 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx @@ -1,5 +1,6 @@ import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; +import Handsontable from 'handsontable'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useId } from '../../hooks/useId'; @@ -344,14 +345,19 @@ export function DataSetMeta({ export function DataSetName({ dataset, - getRowCount, + hot, }: { readonly dataset: Dataset; - readonly getRowCount: () => number; + readonly hot: Handsontable | undefined; }): JSX.Element { const [showMeta, handleOpen, handleClose] = useBooleanState(); const [name, setName] = React.useState(dataset.name); + const getRowCount = () => + hot === undefined + ? dataset.rows.length + : hot.countRows() - hot.countEmptyRows(true); + useTitle(name); return ( @@ -388,4 +394,4 @@ export function DataSetName({ )} ); -} \ No newline at end of file +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 0a324364aa2..2391c30bea2 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -17,7 +17,6 @@ import { f } from '../../utils/functools'; import { ensure, type IR, type RA } from '../../utils/types'; import { legacyNonJsxIcons } from '../Atoms/Icons'; import { getTable } from '../DataModel/tables'; -import { hasPermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbMapping } from './mapping'; @@ -60,9 +59,9 @@ const fillCellsContextMenuItem = ( false), callback: (_, selections) => selections.forEach((selection) => - Array.from( - new Array(selection.end.col + 1 - selection.start.col).keys() - ).forEach((index) => { + Array.from({ + length: selection.end.col + 1 - selection.start.col, + }).forEach((_, index) => { const startRow = mode === 'up' ? selection.start.row + 1 : selection.start.row; const endRow = selection.end.row; @@ -202,11 +201,11 @@ function WbSpreadsheetComponent({ upload_results: { disableSelection: true, isCommand: false, - renderer: (_hot, wrapper) => { + renderer: (hot, wrapper) => { const { endRow: visualRow, endCol: visualCol } = - getSelectedRegions(_hot).at(-1) ?? {}; - const physicalRow = _hot.toPhysicalRow(visualRow ?? 0); - const physicalCol = _hot.toPhysicalColumn(visualCol ?? 0); + getSelectedRegions(hot).at(-1) ?? {}; + const physicalRow = hot.toPhysicalRow(visualRow ?? 0); + const physicalCol = hot.toPhysicalColumn(visualCol ?? 0); const createdRecords = validation.uploadResults.newRecords[physicalRow]?.[ @@ -427,15 +426,12 @@ function WbSpreadsheetComponent({ return { displayDelay: 100 }; }, []); - const hooks = React.useMemo( - () => - getHotHooks( - workbench, - physicalColToMappingCol, - spreadsheetChanged, - checkDeletedFail - ), - [hot] + const hooks = getHotHooks( + workbench, + physicalColToMappingCol, + spreadsheetChanged, + checkDeletedFail, + isReadOnly ); return ( diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 8f6c7b4937c..45ea75f3105 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -6,7 +6,7 @@ * */ -import React, { KeyboardEventHandler } from 'react'; +import React from 'react'; import _ from 'underscore'; import { wbText } from '../../localization/workbench'; @@ -37,20 +37,20 @@ function Navigation({ label, totalCount, utils, - spreadsheetContainer, + spreadsheetContainerRef, }: { readonly name: keyof WbCellCounts; readonly label: LocalizedString; readonly totalCount: number; readonly utils: WbUtils; - readonly spreadsheetContainer: any; + readonly spreadsheetContainerRef: React.RefObject; }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); const [currentPosition, setCurrentPosition] = React.useState(0); const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); const handleTypeToggle = () => { togglePress(); - utils.toggleCellTypes(name, 'toggle', spreadsheetContainer.current); + utils.toggleCellTypes(name, 'toggle', spreadsheetContainerRef.current); }; const handlePrevious = () => { const [_, position] = utils.navigateCells({ @@ -118,12 +118,12 @@ export function WbUtilsComponent({ isUploaded, cellCounts, utils, - spreadsheetContainer, + spreadsheetContainerRef, }: { readonly isUploaded: boolean; readonly cellCounts: WbCellCounts; readonly utils: WbUtils; - readonly spreadsheetContainer: any; + readonly spreadsheetContainerRef: React.RefObject; }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); const searchRef = React.useRef(null); @@ -134,102 +134,100 @@ export function WbUtilsComponent({ utils.searchCells( event, searchRef.current as HTMLInputElement, - spreadsheetContainer.current + spreadsheetContainerRef?.current ); }; const handleReplace = (event: KeyboardEvent) => { - utils.replaceCells(event, replaceRef.current as HTMLInputElement); + utils.replaceCells(event, replaceRef.current!); }; return ( - <> -
- +
+ +
+ +
+ {!isUploaded && hasPermission('/workbench/dataset', 'update') ? (
-
- {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( -
- -
- ) : undefined} - - { - if ( - newSearchPreferences.navigation.direction !== - utils.searchPreferences.navigation.direction - ) { - // TODO: add workbench or cells to parent component - // cells.flushIndexedCellData = true; - } - utils.searchPreferences = newSearchPreferences; - // TODO: figure out what searchCells with SettingsChange does - // if (utils.searchPreferences.search.liveUpdate) - // utils.searchCells({ - // key: 'SettingsChange', - // }).catch(softFail); - }} - /> - + ) : undefined} + + { + if ( + newSearchPreferences.navigation.direction !== + utils.searchPreferences.navigation.direction + ) { + // TODO: add workbench or cells to parent component + // cells.flushIndexedCellData = true; + } + utils.searchPreferences = newSearchPreferences; + // TODO: figure out what searchCells with SettingsChange does + // if (utils.searchPreferences.search.liveUpdate) + // utils.searchCells({ + // key: 'SettingsChange', + // }).catch(softFail); + }} + /> +
+ + {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( - {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( - - ) : undefined} + ) : undefined} + + {!isUploaded && ( - {!isUploaded && ( - - )} -
- + )} +
); } @@ -292,7 +290,7 @@ export class WbUtils { : visualCol; const [currentRow, currentCol] = - currentCell ?? getSelectedLast(this.workbench.hot); + currentCell ?? getSelectedLast(this.workbench.hot!); const [currentTransposedRow, currentTransposedCol] = [ resolveIndex(currentRow, currentCol, true), @@ -376,7 +374,10 @@ export class WbUtils { if (matchedCell === undefined) return [undefined, finalCellPosition]; - this.workbench.hot.selectCell(matchedCell.visualRow, matchedCell.visualCol); + this.workbench.hot?.selectCell( + matchedCell.visualRow, + matchedCell.visualCol + ); // Turn on the respective cell type if it was hidden // TODO: figure out if this is needed @@ -388,9 +389,9 @@ export class WbUtils { searchCells( event: KeyboardEvent | { readonly key: 'SettingsChange' }, searchQueryElement: HTMLInputElement, - spreadsheetContainer: any + spreadsheetContainer: HTMLElement | null ) { - if (!this.workbench.hot) return; + if (this.workbench.hot === undefined) return; /* * Don't rerun search on live search if search query did not change * (e.x, if Ctrl/Cmd+A is clicked in the search box) @@ -412,7 +413,6 @@ export class WbUtils { } this.toggleCellTypes('searchResults', 'remove', spreadsheetContainer); - let resultsCount = 0; const data = this.workbench.dataset.rows; const firstVisibleRow = getHotPlugin(this.workbench.hot, 'autoRowSize').getFirstVisibleRow() - 3; @@ -473,14 +473,11 @@ export class WbUtils { visualCol, } ); - if (isSearchResult) resultsCount += 1; } } this.workbench.cells.updateCellInfoStats(); - // navigationTotalElement.textContent = resultsCount.toString(); - // Navigate to the first search result when hitting Enter if (event.key === 'Enter') this.navigateCells({ @@ -536,7 +533,7 @@ export class WbUtils { toggleCellTypes( navigationType: keyof WbCellCounts, action: 'add' | 'remove' | 'toggle' = 'toggle', - spreadsheetContainer?: any + spreadsheetContainer?: HTMLElement | null ): void { const groupName = camelToKebab(navigationType); const cssClassName = `wb-hide-${groupName}`; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index cf47625d328..f640d341fe2 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -17,7 +17,7 @@ import { resolveValidationMessage } from './resultsParser'; import type { Workbench } from './WbView'; type UploadResults = { - ambiguousMatches: WritableArray< + readonly ambiguousMatches: WritableArray< WritableArray<{ physicalCols: RA; mappingPath: MappingPath; @@ -25,8 +25,8 @@ type UploadResults = { key: string; }> >; - recordCounts: Partial, number>>; - newRecords: Partial< + readonly recordCounts: Partial, number>>; + readonly newRecords: Partial< WritableArray< WritableArray< WritableArray< diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index c6515cffd49..82c99aa055c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -55,16 +55,16 @@ export type Workbench = { cellCounts: GetSet; }; -export function WbViewReact({ +export function WbView({ dataset, - handleDatasetDelete, + onDatasetDeleted: handleDatasetDeleted, triggerDatasetRefresh, - spreadsheetContainer, + spreadsheetContainerRef, }: { readonly dataset: Dataset; - readonly handleDatasetDelete: () => void; + readonly onDatasetDeleted: () => void; readonly triggerDatasetRefresh: () => void; - readonly spreadsheetContainer: any; + readonly spreadsheetContainerRef: React.RefObject; }): JSX.Element { const data = React.useMemo>>( () => @@ -76,7 +76,7 @@ export function WbViewReact({ const [hotTable, setHotTable] = React.useState(); const hot = React.useMemo( - () => (hotTable?.hotInstance ? hotTable?.hotInstance : undefined), + () => (hotTable?.hotInstance ?? undefined), [hotTable] ); @@ -123,7 +123,7 @@ export function WbViewReact({ }, [dataset, hot]); const checkDeletedFail = React.useCallback((statusCode: number): boolean => { - if (statusCode === Http.NOT_FOUND) handleDatasetDelete(); + if (statusCode === Http.NOT_FOUND) handleDatasetDeleted(); return statusCode === Http.NOT_FOUND; }, []); @@ -186,7 +186,7 @@ export function WbViewReact({ workbench.utils.toggleCellTypes( 'newCells', 'remove', - spreadsheetContainer.current + spreadsheetContainerRef?.current ); } else { getHotPlugin(workbench.hot, 'hiddenRows').showRows( @@ -210,11 +210,7 @@ export function WbViewReact({
- hot === undefined - ? dataset.rows.length - : hot.countRows() - hot.countEmptyRows(true) - } + hot={hot} />
@@ -283,7 +279,7 @@ export function WbViewReact({ isUploaded={isUploaded} cellCounts={cellCounts} utils={workbench.utils} - spreadsheetContainer={spreadsheetContainer} + spreadsheetContainerRef={spreadsheetContainerRef} /> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 67ccd5145ec..dc2b0cbf3d4 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -12,7 +12,6 @@ import type { RA } from '../../utils/types'; import { ensure, overwriteReadOnly } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { schema } from '../DataModel/schema'; -import { hasPermission } from '../Permissions/helpers'; import { getHotPlugin } from './handsontable'; import { Workbench } from './WbView'; @@ -20,7 +19,8 @@ export function getHotHooks( workbench: Workbench, physicalColToMappingCol: (physicalCol: number) => number | undefined, spreadsheetChanged: () => void, - checkDeletedFail: (statusCode: number) => boolean + checkDeletedFail: (statusCode: number) => boolean, + isReadOnly: boolean ): Partial { let sortConfigIsSet: boolean = false; @@ -33,13 +33,7 @@ export function getHotHooks( * cases * */ - afterRenderer: ( - td: HTMLTableCellElement, - visualRow: number, - visualCol: number, - property: number | string, - _value: string - ) => { + afterRenderer: (td, visualRow, visualCol, property, _value) => { if (workbench.hot === undefined) { td.classList.add('text-gray-500'); return; @@ -50,8 +44,8 @@ export function getHotHooks( ? property : workbench.hot.toPhysicalColumn(visualCol); if (physicalCol >= workbench.dataset.columns.length) return; - const metaArray = workbench.cells?.cellMeta?.[physicalRow]?.[physicalCol]; - if (workbench.cells?.getCellMetaFromArray(metaArray!, 'isModified')) + const metaArray = workbench.cells.cellMeta?.[physicalRow]?.[physicalCol]; + if (workbench.cells.getCellMetaFromArray(metaArray!, 'isModified')) workbench.cells.runMetaUpdateEffects( td, 'isModified', @@ -59,40 +53,36 @@ export function getHotHooks( visualRow, visualCol ); - if (workbench.cells?.getCellMetaFromArray(metaArray!, 'isNew')) - workbench.cells?.runMetaUpdateEffects( + if (workbench.cells.getCellMetaFromArray(metaArray!, 'isNew')) + workbench.cells.runMetaUpdateEffects( td, 'isNew', true, visualRow, visualCol ); - if (workbench.cells!.getCellMetaFromArray(metaArray!, 'isSearchResult')) - workbench.cells?.runMetaUpdateEffects( + if (workbench.cells.getCellMetaFromArray(metaArray!, 'isSearchResult')) + workbench.cells.runMetaUpdateEffects( td, 'isSearchResult', true, visualRow, visualCol ); - if (workbench.mappings?.mappedHeaders?.[physicalCol] === undefined) + if (workbench.mappings.mappedHeaders?.[physicalCol] === undefined) td.classList.add('text-gray-500'); - if (workbench.mappings?.coordinateColumns?.[physicalCol] !== undefined) + if (workbench.mappings.coordinateColumns?.[physicalCol] !== undefined) td.classList.add('wb-coordinate-cell'); }, // Make HOT use defaultValues for validation if cell is empty - beforeValidate: ( - value: any, - _visualRow: number, - property: number | string - ) => { + beforeValidate: (value, _visualRow, property) => { if (Boolean(value) || workbench.hot === undefined) return value; const visualCol = workbench.hot.propToCol(property); const physicalCol = workbench.hot.toPhysicalColumn(visualCol); - return workbench.mappings?.defaultValues[physicalCol] ?? value; + return workbench.mappings.defaultValues[physicalCol] ?? value; }, afterValidate: ( @@ -106,7 +96,7 @@ export function getHotHooks( const physicalRow = workbench.hot.toPhysicalRow(visualRow); const physicalCol = workbench.hot.toPhysicalColumn(visualCol); - const issues = workbench.cells?.getCellMeta( + const issues = workbench.cells.getCellMeta( physicalRow, physicalCol, 'issues' @@ -137,7 +127,7 @@ export function getHotHooks( ), ]); if (JSON.stringify(issues) !== JSON.stringify(newIssues)) - workbench.cells!.updateCellMeta( + workbench.cells.updateCellMeta( physicalRow, physicalCol, 'issues', @@ -149,10 +139,7 @@ export function getHotHooks( afterRedo: (data) => afterUndoRedo(workbench, 'redo', data), - beforePaste: () => - // !wbView.uploadedView && - // !wbView.isUploaded && - hasPermission('/workbench/dataset', 'update'), + beforePaste: () => !isReadOnly, /* * If copying values from a 1x3 area and pasting into the last cell, HOT @@ -261,19 +248,19 @@ export function getHotHooks( newValue, }) => { if ( - workbench.cells?.getCellMeta( + workbench.cells.getCellMeta( physicalRow, physicalCol, 'originalValue' ) === undefined ) - workbench.cells?.setCellMeta( + workbench.cells.setCellMeta( physicalRow, physicalCol, 'originalValue', oldValue ); - workbench.cells?.recalculateIsModifiedState( + workbench.cells.recalculateIsModifiedState( physicalRow, physicalCol, { @@ -282,25 +269,25 @@ export function getHotHooks( } ); if ( - workbench.utils?.searchPreferences.search.liveUpdate && - workbench.utils?.searchQuery !== undefined + workbench.utils.searchPreferences.search.liveUpdate && + workbench.utils.searchQuery !== undefined ) - workbench.cells?.updateCellMeta( + workbench.cells.updateCellMeta( physicalRow, physicalCol, 'isSearchResult', - workbench.utils?.searchFunction(newValue), + workbench.utils.searchFunction(newValue), { visualRow, visualCol } ); } ); spreadsheetChanged(); - workbench.cells?.updateCellInfoStats(); + workbench.cells.updateCellInfoStats(); if (workbench.dataset.uploadplan) changedRows.forEach((physicalRow) => - workbench.validation?.startValidateRow(physicalRow) + workbench.validation.startValidateRow(physicalRow) ); }, @@ -330,7 +317,7 @@ export function getHotHooks( // REFACTOR: use sortFunction here ).sort(); - workbench.cells!.flushIndexedCellData = true; + workbench.cells.flushIndexedCellData = true; addedRows .filter((physicalRow) => physicalRow < workbench.cells!.cellMeta.length) .forEach((physicalRow) => @@ -353,15 +340,15 @@ export function getHotHooks( .reverse(); removedRows.forEach((physicalRow) => { - workbench.cells!.cellMeta.splice(physicalRow, 1); - workbench.validation!.liveValidationStack.splice(physicalRow, 1); + workbench.cells.cellMeta.splice(physicalRow, 1); + workbench.validation.liveValidationStack.splice(physicalRow, 1); }); - workbench.cells!.flushIndexedCellData = true; + workbench.cells.flushIndexedCellData = true; if (workbench.hot && source !== 'auto') { spreadsheetChanged(); - workbench.cells!.updateCellInfoStats(); + workbench.cells.updateCellInfoStats(); } return true; @@ -373,7 +360,7 @@ export function getHotHooks( * and sorting them in the same direction */ beforeColumnSort: (currentSortConfig, newSortConfig) => { - workbench.cells!.flushIndexedCellData = true; + workbench.cells.flushIndexedCellData = true; // if (wbView.coordinateConverterView) return false; @@ -585,7 +572,7 @@ function afterUndoRedo( // wbView.undoRedoIsHandled = true; workbench.hot?.undo(); // wbView.undoRedoIsHandled = false; - workbench.disambiguation!.afterChangeDisambiguation(physicalRow); + workbench.disambiguation.afterChangeDisambiguation(physicalRow); }, 0); - else workbench.disambiguation!.afterChangeDisambiguation(physicalRow); -} \ No newline at end of file + else workbench.disambiguation.afterChangeDisambiguation(physicalRow); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 9b02a274415..fd83b786ee9 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { HotTable } from '@handsontable/react'; import { useAsyncState } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; @@ -18,7 +17,7 @@ import { treeRanksPromise } from '../InitialContext/treeRanks'; import { Dialog } from '../Molecules/Dialog'; import { NotFoundView } from '../Router/NotFoundView'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { WbViewReact } from './WbView'; +import { WbView } from './WbView'; const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); @@ -41,7 +40,7 @@ function useDataSet( ); } -export function WorkBenchReact(): JSX.Element { +export function WorkBench(): JSX.Element { useMenuItem('workBench'); const [treeRanksLoaded = false] = useAsyncState(fetchTreeRanks, true); @@ -57,7 +56,7 @@ export function WorkBenchReact(): JSX.Element { const navigate = useNavigate(); // TODO: consider adding this to workbench variable - const spreadsheetContainer = React.useRef(null); + const spreadsheetContainerRef = React.useRef(null); if (!dataSet || !treeRanksLoaded) return ; @@ -80,12 +79,12 @@ export function WorkBenchReact(): JSX.Element { ) : ( <>
-
- +
From ceeca5bf2bedfe3f18c8658f27dee325112a9a8d Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 12 Apr 2024 17:30:18 -0400 Subject: [PATCH 058/153] Run prettier --- .../lib/components/WbActions/WbActions.tsx | 25 +++++++++---------- .../lib/components/WorkBench/WbView.tsx | 7 ++---- .../js_src/lib/components/WorkBench/hooks.ts | 12 +++------ .../js_src/lib/components/WorkBench/index.tsx | 5 +++- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index 2a7e2a6bddb..d1d2941b2f7 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -272,19 +272,18 @@ export function WbActionsComponent({ - {cellCounts.invalidCells === 0 && - modeRef.current === 'upload' && ( - { - modeRef.current = undefined; - refreshInitiatorAborted.current = false; - closeOperationCompleted(); - }} - /> - )} + {cellCounts.invalidCells === 0 && modeRef.current === 'upload' && ( + { + modeRef.current = undefined; + refreshInitiatorAborted.current = false; + closeOperationCompleted(); + }} + /> + )} {commonText.close()} } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 82c99aa055c..0c551866546 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -76,7 +76,7 @@ export function WbView({ const [hotTable, setHotTable] = React.useState(); const hot = React.useMemo( - () => (hotTable?.hotInstance ?? undefined), + () => hotTable?.hotInstance ?? undefined, [hotTable] ); @@ -208,10 +208,7 @@ export function WbView({ role="toolbar" >
- +
-
+
Date: Fri, 12 Apr 2024 17:33:17 -0400 Subject: [PATCH 059/153] Suppress some type errors temporarily --- specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx | 2 ++ specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx | 1 + 2 files changed, 3 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 45ea75f3105..a5e200f8756 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -158,6 +158,7 @@ export function WbUtilsComponent({ spellCheck title={commonText.searchQuery()} type="search" + // @ts-expect-error KeyboardEvent type does not match? onKeyDown={handleSearch} />
@@ -169,6 +170,7 @@ export function WbUtilsComponent({ autoComplete="on" placeholder={wbText.replace()} title={wbText.replacementValue()} + // @ts-expect-error KeyboardEvent type does not match? onKeyDown={handleReplace} disabled={isReadOnly} /> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 74bd7f15304..2dd5d73a1b0 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -52,6 +52,7 @@ export function WorkBench(): JSX.Element { const loading = React.useContext(LoadingContext); const [isDeleted, handleDeleted] = useBooleanState(); // TODO: figure out how handleDeletedConfirmation was being used in Backbone. possibly not used at all + // @ts-ignore const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); const navigate = useNavigate(); From f570559cdddb06a4eec1ca6a03823c444f4ed426 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 15 Apr 2024 16:51:25 -0400 Subject: [PATCH 060/153] Add suggested improvements --- .../lib/components/WbActions/WbActions.tsx | 102 +++++--- .../lib/components/WbActions/WbResults.tsx | 24 +- .../lib/components/WbActions/WbRevert.tsx | 46 ++-- .../lib/components/WbActions/WbRollback.tsx | 30 +-- .../lib/components/WbActions/WbSave.tsx | 67 ++--- .../lib/components/WbActions/WbUpload.tsx | 61 +++-- .../lib/components/WbActions/WbValidate.tsx | 46 ++-- .../lib/components/WbToolkit/ChangeOwner.tsx | 27 +- .../WbToolkit/CoordinateConverter.tsx | 39 ++- .../lib/components/WbToolkit/DevShowPlan.tsx | 43 ++- .../lib/components/WbToolkit/GeoLocate.tsx | 49 ++-- .../lib/components/WbToolkit/WbLeafletMap.tsx | 53 ++-- .../lib/components/WbToolkit/WbToolkit.tsx | 69 ++--- .../lib/components/WorkBench/DataSetMeta.tsx | 4 +- .../components/WorkBench/Disambiguation.tsx | 6 +- .../components/WorkBench/WbSpreadsheet.tsx | 245 ++++++++---------- .../lib/components/WorkBench/WbUtils.tsx | 44 ++-- .../lib/components/WorkBench/WbView.tsx | 152 +++++------ .../js_src/lib/components/WorkBench/hooks.ts | 8 +- .../js_src/lib/components/WorkBench/index.tsx | 28 +- 20 files changed, 551 insertions(+), 592 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index d1d2941b2f7..b1afa2d1f6b 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -24,6 +24,8 @@ import { WbUpload } from './WbUpload'; import { WbRevert } from './WbRevert'; import { WbSave } from './WbSave'; import { GET } from '../../utils/utils'; +import { ErrorBoundary } from '../Errors/ErrorBoundary'; +import { LoadingContext } from '../Core/Contexts'; export function useWbActions({ datasetId, @@ -40,20 +42,23 @@ export function useWbActions({ }) { const modeRef = React.useRef(undefined); const refreshInitiatorAborted = React.useRef(false); + const loading = React.useContext(LoadingContext); const startUpload = (newMode: WbStatus): void => { workbench.validation.stopLiveValidation(); // TODO: figure out what to do in updateValidationButton(); workbench.validation.updateValidationButton(); - ping(`/api/workbench/${newMode}/${datasetId}/`, { - method: 'POST', - expectedErrors: [Http.CONFLICT], - }) - .then((statusCode): void => { - checkDeletedFail(statusCode); - checkConflictFail(statusCode); + loading( + ping(`/api/workbench/${newMode}/${datasetId}/`, { + method: 'POST', + expectedErrors: [Http.CONFLICT], }) - .then(() => triggerStatusComponent(newMode)); + .then((statusCode): void => { + checkDeletedFail(statusCode); + checkConflictFail(statusCode); + }) + .then(() => triggerStatusComponent(newMode)) + ); }; const triggerStatusComponent = (newMode: WbStatus): void => { @@ -79,10 +84,11 @@ export function useWbActions({ }; } -export function WbActionsComponent({ +export function WbActions({ dataset, hasUnsavedChanges, isUploaded, + isResultsOpen, workbench, mappings, checkDeletedFail, @@ -93,8 +99,9 @@ export function WbActionsComponent({ readonly dataset: Dataset; readonly hasUnsavedChanges: boolean; readonly isUploaded: boolean; + readonly isResultsOpen: boolean; readonly workbench: Workbench; - readonly mappings: WbMapping; + readonly mappings: WbMapping | undefined; readonly checkDeletedFail: (statusCode: number) => void; readonly onDatasetRefresh: () => void; readonly onSpreadsheetUpToDate: () => void; @@ -198,45 +205,58 @@ export function WbActionsComponent({
) : undefined} {!isUploaded && hasPermission('/workbench/dataset', 'validate') ? ( - + + + ) : undefined} + + - ) : undefined} - + {isUploaded && hasPermission('/workbench/dataset', 'unupload') ? ( - + + + ) : undefined} {!isUploaded && hasPermission('/workbench/dataset', 'upload') ? ( - + + + ) : undefined} {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( <> - - + + + + + + ) : undefined} {typeof modeRef.current === 'string' && showStatus ? ( diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx index 48d335cd8bd..9d27b579c19 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx @@ -3,27 +3,25 @@ import React from 'react'; import { Button } from '../Atoms/Button'; import { wbText } from '../../localization/workbench'; import { commonText } from '../../localization/common'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbResults({ hasUnsavedChanges, + isResultsOpen, onToggleResults: handleToggleResults, }: { readonly hasUnsavedChanges: boolean; + readonly isResultsOpen: boolean; readonly onToggleResults: () => void; }): JSX.Element { return ( - <> - - - {commonText.results()} - - - + + {commonText.results()} + ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx index fdc66e65d2a..d00bb460169 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx @@ -26,31 +26,29 @@ export function WbRevert({ return ( <> - - + {wbText.revert()} + + {showRevert && ( + + {commonText.cancel()} + + {wbText.revert()} + + + } + header={wbText.revertChanges()} + onClose={closeRevert} > - {wbText.revert()} -
- {showRevert && ( - - {commonText.cancel()} - - {wbText.revert()} - - - } - header={wbText.revertChanges()} - onClose={closeRevert} - > - {wbText.revertChangesDescription()} - - )} - + {wbText.revertChangesDescription()} + + )} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx index cde88da25f1..a6a751f8257 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx @@ -20,22 +20,20 @@ export function WbRollback({ return ( <> - - - {wbText.rollback()} - - {rollback ? ( - - ) : undefined} - + + {wbText.rollback()} + + {rollback ? ( + + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index a58360cbef9..499fb1b027a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -11,7 +11,7 @@ import { Workbench } from '../WorkBench/WbView'; import { overwriteReadOnly } from '../../utils/types'; import { ping } from '../../utils/ajax/ping'; import { Http } from '../../utils/ajax/definitions'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; +import { LoadingContext } from '../Core/Contexts'; export function WbSave({ workbench, @@ -26,6 +26,7 @@ export function WbSave({ }): JSX.Element { const [showProgressBar, openProgressBar, closeProgressBar] = useBooleanState(); + const loading = React.useContext(LoadingContext); const handleSave = () => { // Clear validation @@ -38,43 +39,43 @@ export function WbSave({ openProgressBar(); // Send data - ping(`/api/workbench/rows/${workbench.dataset.id}/`, { - method: 'PUT', - body: workbench.data, - expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], - }) - .then((status) => checkDeletedFail(status)) - .then(() => { - handleSpreadsheetUpToDate(); - workbench.cells.cellMeta = []; - // TODO: Figure out how to rework searchCells for SettingsChange as input - // workbench.utils?.searchCells({ key: 'SettingsChange' }); - workbench.hot?.render(); - closeProgressBar(); - }); + loading( + ping(`/api/workbench/rows/${workbench.dataset.id}/`, { + method: 'PUT', + body: workbench.data, + expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], + }) + .then((status) => checkDeletedFail(status)) + .then(() => { + handleSpreadsheetUpToDate(); + workbench.cells.cellMeta = []; + // TODO: Figure out how to rework searchCells for SettingsChange as input + // workbench.utils?.searchCells({ key: 'SettingsChange' }); + workbench.hot?.render(); + closeProgressBar(); + }) + ); }; return ( <> - - + {commonText.save()} + + {showProgressBar ? ( + - {commonText.save()} -
- {showProgressBar ? ( - - {loadingBar} - - ) : undefined} - + {loadingBar} + + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx index 134cf8f38e8..5662c977f98 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx @@ -8,7 +8,6 @@ import { Dialog } from '../Molecules/Dialog'; import { wbText } from '../../localization/workbench'; import { commonText } from '../../localization/common'; import { WbCellCounts } from '../WorkBench/CellMeta'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbUpload({ hasUnsavedChanges, @@ -18,7 +17,7 @@ export function WbUpload({ cellCounts, }: { readonly hasUnsavedChanges: boolean; - readonly mappings: WbMapping; + readonly mappings: WbMapping | undefined; readonly openNoUploadPlan: () => void; readonly startUpload: (mode: WbStatus) => void; readonly cellCounts: WbCellCounts; @@ -40,38 +39,36 @@ export function WbUpload({ return ( <> - - 0} - title={ - hasUnsavedChanges - ? wbText.unavailableWhileEditing() - : cellCounts.invalidCells > 0 - ? wbText.uploadUnavailableWhileHasErrors() - : '' + 0} + title={ + hasUnsavedChanges + ? wbText.unavailableWhileEditing() + : cellCounts.invalidCells > 0 + ? wbText.uploadUnavailableWhileHasErrors() + : '' + } + > + {wbText.upload()} + + {showUpload ? ( + + {commonText.cancel()} + + {wbText.upload()} + + } + header={wbText.startUpload()} + onClose={closeUpload} > - {wbText.upload()} - - {showUpload ? ( - - {commonText.cancel()} - - {wbText.upload()} - - - } - header={wbText.startUpload()} - onClose={closeUpload} - > - {wbText.startUploadDescription()} - - ) : undefined} - + {wbText.startUploadDescription()} + + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index 222623902f8..3ecca26f954 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -23,30 +23,28 @@ export function WbValidate({ return ( <> - - - {validation.validationMode === 'live' - ? validation.liveValidationStack.length > 0 - ? commonText.countLine({ - resource: wbText.dataCheckOn(), - count: validation.liveValidationStack.length, - }) - : wbText.dataCheckOn() - : wbText.dataCheck()} - - - {wbText.validate()} - - + + {validation.validationMode === 'live' + ? validation.liveValidationStack.length > 0 + ? commonText.countLine({ + resource: wbText.dataCheckOn(), + count: validation.liveValidationStack.length, + }) + : wbText.dataCheckOn() + : wbText.dataCheck()} + + + {wbText.validate()} + ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx index 26bf5c24e24..d7fa8595c0e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx @@ -19,7 +19,6 @@ import { unsafeNavigate } from '../Router/Router'; import { Button } from '../Atoms/Button'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbChangeOwner({ hasUnsavedChanges, @@ -32,20 +31,18 @@ export function WbChangeOwner({ useBooleanState(); return ( <> - - - {wbText.changeOwner()} - - {showChangeOwner && ( - - )} - + + {wbText.changeOwner()} + + {showChangeOwner && ( + + )} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 68fd5d85f08..4359a06c41d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -20,7 +20,6 @@ import { import { useBooleanState } from '../../hooks/useBooleanState'; import type { Dataset } from '../WbPlanView/Wrapped'; import type { WbMapping } from '../WorkBench/mapping'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbConvertCoordinates({ hasLocality, @@ -39,26 +38,24 @@ export function WbConvertCoordinates({ useBooleanState(); return ( <> - - - {wbText.convertCoordinates()} - - {showConvertCoords && ( - - )} - + + {wbText.convertCoordinates()} + + {showConvertCoords && ( + + )} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx index aa11eddfb17..667729b3209 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx @@ -18,7 +18,6 @@ import { overwriteReadOnly } from '../../utils/types'; import type { Dataset } from '../WbPlanView/Wrapped'; import { useBooleanState } from '../../hooks/useBooleanState'; import { wbText } from '../../localization/workbench'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbRawPlan({ dataset, @@ -32,28 +31,26 @@ export function WbRawPlan({ const [showRawPlan, openRawPlan, closeRawPlan] = useBooleanState(); return ( <> - - - {wbText.uploadPlan()} - - {showRawPlan && ( - { - overwriteReadOnly(dataset, 'uploadplan', plan); - triggerDatasetRefresh(); - }} - onClose={closeRawPlan} - onDeleted={handleDatasetDeleted} - /> - )} - + + {wbText.uploadPlan()} + + {showRawPlan && ( + { + overwriteReadOnly(dataset, 'uploadplan', plan); + triggerDatasetRefresh(); + }} + onClose={closeRawPlan} + onDeleted={handleDatasetDeleted} + /> + )} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index d056c1a99fa..3fc208aa121 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -23,7 +23,6 @@ import type { Dataset } from '../WbPlanView/Wrapped'; import type { WbMapping } from '../WorkBench/mapping'; import { wbText } from '../../localization/workbench'; import { localityText } from '../../localization/locality'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbGeoLocate({ hasLocality, @@ -39,25 +38,23 @@ export function WbGeoLocate({ const [showGeoLocate, openGeoLocate, closeGeoLocate] = useBooleanState(); return ( <> - - - {localityText.geoLocate()} - - {showGeoLocate && mappings && ( - - )} - + + {localityText.geoLocate()} + + {showGeoLocate && mappings && ( + + )} ); } @@ -77,11 +74,8 @@ function GeoLocate({ const [localityIndex, setLocalityIndex] = React.useState(0); const selection = React.useMemo( - () => - hot === undefined - ? undefined - : getSelectedLocalities(hot, columns, localityColumns, true), - [hot, columns, localityColumns] + () => getSelectedLocalities(hot, columns, localityColumns, true), + [columns, localityColumns] ); function handleMove(newLocalityIndex: number): void { @@ -98,9 +92,9 @@ function GeoLocate({ ); React.useEffect(() => { - if (selection === undefined || hot === undefined) return; + if (selection === undefined) return; else handleMove(0); - }, [hot, columns, localityColumns]); + }, [columns, localityColumns]); const handleResult = React.useCallback( ({ latitude, longitude, uncertainty }: GeoLocatePayload) => { @@ -176,7 +170,6 @@ export function getSelectedLocalities( }; } | undefined { - if (hot === undefined) return undefined; const selectedRegions = getSelectedRegions(hot); const selectedVirtualColumns = f.unique( diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx index ac9b96e4182..a3854eea08b 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx @@ -11,7 +11,6 @@ import type { WbMapping } from '../WorkBench/mapping'; import { Button } from '../Atoms/Button'; import { localityText } from '../../localization/locality'; import { wbText } from '../../localization/workbench'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbLeafletMap({ hasLocality, @@ -45,33 +44,31 @@ export function WbLeafletMap({ return ( <> - - - {localityText.geoMap()} - - {showLeafletMap && ( - { - if (localityPoints === undefined) return; - const rowNumber = localityPoints[localityPoint].rowNumber.value; - if (typeof rowNumber !== 'number') - throw new Error('rowNumber must be a number'); - const [_currentRow, currentCol] = getSelectedLast(hot!); - hot?.scrollViewportTo(rowNumber, currentCol); - // Select entire row - hot?.selectRows(rowNumber); - }} - /> - )} - + + {localityText.geoMap()} + + {showLeafletMap && ( + { + if (localityPoints === undefined) return; + const rowNumber = localityPoints[localityPoint].rowNumber.value; + if (typeof rowNumber !== 'number') + throw new Error('rowNumber must be a number'); + const [_currentRow, currentCol] = getSelectedLast(hot!); + hot?.scrollViewportTo(rowNumber, currentCol); + // Select entire row + hot?.selectRows(rowNumber); + }} + /> + )} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx index 5a60e33170d..f4db20efa8c 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx @@ -16,6 +16,7 @@ import type { WbMapping } from '../WorkBench/mapping'; import { WbRawPlan } from './DevShowPlan'; import { WbGeoLocate } from './GeoLocate'; import { WbLeafletMap } from './WbLeafletMap'; +import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbToolkit({ dataset, @@ -28,7 +29,7 @@ export function WbToolkit({ }: { readonly dataset: Dataset; readonly hot: Handsontable | undefined; - readonly mappings: WbMapping; + readonly mappings: WbMapping | undefined; readonly data: RA>; readonly onDatasetDeleted: () => void; readonly hasUnsavedChanges: boolean; @@ -61,17 +62,21 @@ export function WbToolkit({ {hasPermission('/workbench/dataset', 'transfer') && hasTablePermission('SpecifyUser', 'read') ? ( <> - + + + ) : undefined} - + + + {hasPermission('/workbench/dataset', 'update') && ( <> - - + + + + + + )} - + + +
); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx index 7aa11bafea1..55d4138b819 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx @@ -361,7 +361,7 @@ export function DataSetName({ useTitle(name); return ( - <> +
{' '}

{dataset.uploadplan !== null && ( @@ -392,6 +392,6 @@ export function DataSetName({ onDeleted={() => unsafeNavigate('/specify/', { replace: true })} /> )} - +

); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx index ee47da643e5..c45c019b21d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx @@ -13,15 +13,18 @@ import { isTreeTable } from '../InitialContext/treeRanks'; import { Dialog } from '../Molecules/Dialog'; import { FormattedResource } from '../Molecules/FormattedResource'; import { hasTablePermission } from '../Permissions/helpers'; +import { WritableArray } from '../../utils/types'; export function DisambiguationDialog({ matches, + liveValidationStack, defaultResource, onSelected: handleSelected, onSelectedAll: handleSelectedAll, onClose: handleClose, }: { readonly matches: RA>; + readonly liveValidationStack: WritableArray; readonly defaultResource?: SpecifyResource; readonly onSelected: (resource: SpecifyResource) => void; readonly onSelectedAll: (resource: SpecifyResource) => void; @@ -46,7 +49,8 @@ export function DisambiguationDialog({ {commonText.apply()} { handleSelectedAll(selected!); handleClose(); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 2391c30bea2..fae88a4185d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -9,12 +9,12 @@ import { HotTable } from '@handsontable/react'; import Handsontable from 'handsontable'; import React from 'react'; -import type { Settings } from 'handsontable/plugins/contextMenu'; +import type { DetailedSettings } from 'handsontable/plugins/contextMenu'; import { LANGUAGE } from '../../localization/utils/config'; import { wbPlanText } from '../../localization/wbPlan'; import { f } from '../../utils/functools'; -import { ensure, type IR, type RA } from '../../utils/types'; +import { type RA } from '../../utils/types'; import { legacyNonJsxIcons } from '../Atoms/Icons'; import { getTable } from '../DataModel/tables'; import { userPreferences } from '../Preferences/userPreferences'; @@ -94,14 +94,14 @@ function WbSpreadsheetComponent({ spreadsheetChanged, }: { readonly dataset: Dataset; - readonly setHotTable: React.Ref; + readonly setHotTable: React.RefObject; readonly hot: Handsontable | undefined; readonly isUploaded: boolean; readonly data: RA>; readonly workbench: Workbench; readonly mappings: WbMapping; - readonly checkDeletedFail: any; - readonly spreadsheetChanged: any; + readonly checkDeletedFail: (_: number) => boolean; + readonly spreadsheetChanged: () => void; }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); const physicalColToMappingCol = (physicalCol: number): number | undefined => @@ -155,32 +155,6 @@ function WbSpreadsheetComponent({ openNoDisambiguationDialog(); return; } - - // Re-enable this once live validation is available again: - /* - * Disable "Apply All" if validation is still in progress. - * This is because we don't know all matches until validation is done - */ - /* - *Let applyAllAvailable = true; - *const applyAllButton = content.find('#applyAllButton'); - * - *const updateIt = () => { - * const newState = this.liveValidationStack.length === 0; - * if (newState !== applyAllAvailable) { - * applyAllAvailable = newState; - * applyAllButton.disabled = !newState; - * applyAllButton[newState ? 'removeAttribute' : 'setAttribute']( - * 'title', - * wbText.applyAllUnavailable() - * ); - * } - *}; - * - *const interval = globalThis.setInterval(updateIt, 100); - * // onClose: globalThis.clearInterval(interval); - */ - setDisambiguationMatches(matches); setResource(resources); setPhysicalRow(physicalRow); @@ -188,56 +162,55 @@ function WbSpreadsheetComponent({ }); }; - const contextMenuConfig = { - items: ensure< - IR< - | Handsontable.plugins.ContextMenu.MenuItemConfig - | Handsontable.plugins.ContextMenu.PredefinedMenuItemKey - > - >()( - isUploaded - ? ({ - // Display uploaded record - upload_results: { - disableSelection: true, - isCommand: false, - renderer: (hot, wrapper) => { - const { endRow: visualRow, endCol: visualCol } = - getSelectedRegions(hot).at(-1) ?? {}; - const physicalRow = hot.toPhysicalRow(visualRow ?? 0); - const physicalCol = hot.toPhysicalColumn(visualCol ?? 0); + const contextMenuConfig: DetailedSettings | undefined = + hot === undefined + ? undefined + : { + items: isUploaded + ? ({ + // Display uploaded record + upload_results: { + disableSelection: true, + isCommand: false, + renderer: (hot, wrapper) => { + const { endRow: visualRow, endCol: visualCol } = + getSelectedRegions(hot).at(-1) ?? {}; + const physicalRow = hot.toPhysicalRow(visualRow ?? 0); + const physicalCol = hot.toPhysicalColumn(visualCol ?? 0); - const createdRecords = - validation.uploadResults.newRecords[physicalRow]?.[ - physicalCol - ]; + const createdRecords = + validation.uploadResults.newRecords[physicalRow]?.[ + physicalCol + ]; - if ( - visualRow === undefined || - visualCol === undefined || - createdRecords === undefined || - !cells.getCellMeta(physicalRow, physicalCol, 'isNew') - ) { - wrapper.textContent = wbText.noUploadResultsAvailable(); - wrapper.parentElement?.classList.add('htDisabled'); - const span = document.createElement('span'); - span.style.display = 'none'; - return span; - } + if ( + visualRow === undefined || + visualCol === undefined || + createdRecords === undefined || + !cells.getCellMeta(physicalRow, physicalCol, 'isNew') + ) { + wrapper.textContent = wbText.noUploadResultsAvailable(); + wrapper.parentElement?.classList.add('htDisabled'); + const span = document.createElement('span'); + span.style.display = 'none'; + return span; + } - wrapper.setAttribute( - 'class', - `${wrapper.getAttribute('class')} flex flex-col !m-0 + wrapper.setAttribute( + 'class', + `${wrapper.getAttribute('class')} flex flex-col !m-0 pb-1` - ); - wrapper.innerHTML = createdRecords - .map(([tableName, recordId, label]) => { - const tableLabel = - label === '' ? strictGetTable(tableName).label : label; - // REFACTOR: use new table icons - const tableIcon = getIcon(tableName) ?? unknownIcon; + ); + wrapper.innerHTML = createdRecords + .map(([tableName, recordId, label]) => { + const tableLabel = + label === '' + ? strictGetTable(tableName).label + : label; + // REFACTOR: use new table icons + const tableIcon = getIcon(tableName) ?? unknownIcon; - return `${legacyNonJsxIcons.link} `; - }) - .join(''); + }) + .join(''); - const div = document.createElement('div'); - div.style.display = 'none'; - return div; - }, - }, - } as const) - : ({ - row_above: { - disabled: () => isReadOnly, - }, - row_below: { - disabled: () => isReadOnly, - }, - remove_row: { - disabled: () => { - if (isReadOnly) return true; - // Or if called on the last row - const selectedRegions = getSelectedRegions(hot!); - return ( - selectedRegions.length === 1 && - selectedRegions[0].startRow === data.length - 1 && - selectedRegions[0].startRow === selectedRegions[0].endRow - ); - }, - }, - disambiguate: { - name: wbText.disambiguate(), - disabled: (): boolean => - !disambiguation.isAmbiguousCell() || isReadOnly, - callback: () => openDisambiguationDialog(), - }, - separator_1: '---------', - fill_down: fillCellsContextMenuItem(hot!, 'down', isReadOnly), - fill_up: fillCellsContextMenuItem(hot!, 'up', isReadOnly), - separator_2: '---------', - undo: { - disabled: () => hot?.isUndoAvailable() !== true || isReadOnly, - }, - redo: { - disabled: () => hot?.isRedoAvailable() !== true || isReadOnly, - }, - } as const) - ), - }; + const div = document.createElement('div'); + div.style.display = 'none'; + return div; + }, + }, + } as const) + : ({ + row_above: { + disabled: () => isReadOnly, + }, + row_below: { + disabled: () => isReadOnly, + }, + remove_row: { + disabled: () => { + if (isReadOnly) return true; + // Or if called on the last row + const selectedRegions = getSelectedRegions(hot!); + return ( + selectedRegions.length === 1 && + selectedRegions[0].startRow === data.length - 1 && + selectedRegions[0].startRow === selectedRegions[0].endRow + ); + }, + }, + disambiguate: { + name: wbText.disambiguate(), + disabled: (): boolean => + !disambiguation.isAmbiguousCell() || isReadOnly, + callback: () => openDisambiguationDialog(), + }, + separator_1: '---------', + fill_down: fillCellsContextMenuItem(hot!, 'down', isReadOnly), + fill_up: fillCellsContextMenuItem(hot!, 'up', isReadOnly), + separator_2: '---------', + undo: { + disabled: () => hot?.isUndoAvailable() !== true || isReadOnly, + }, + redo: { + disabled: () => hot?.isRedoAvailable() !== true || isReadOnly, + }, + } as const), + }; React.useEffect(() => { - const getPickLists = async () => - mappings === undefined - ? {} - : await fetchWbPickLists( - dataset.columns, - mappings.tableNames, - mappings.lines - ); - if (hot) { - const configureHotAndPickLists = async () => { - const pickLists = await getPickLists(); - configureHandsontable(hot, mappings, dataset, pickLists); - }; - configureHotAndPickLists(); - } + if (hot === undefined) return; + (mappings === undefined + ? Promise.resolve({}) + : fetchWbPickLists(dataset.columns, mappings.tableNames, mappings.lines) + ).then((pickLists) => + configureHandsontable(hot, mappings, dataset, pickLists) + ); }, [hot]); // Highlight validation cells @@ -435,7 +399,7 @@ function WbSpreadsheetComponent({ ); return ( - <> +
{noDisambiguationDialog && ( @@ -479,6 +443,7 @@ function WbSpreadsheetComponent({ {disambiguationDialog && ( { disambiguation.setDisambiguation( @@ -521,7 +486,7 @@ function WbSpreadsheetComponent({ } /> )} - +
); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index a5e200f8756..754aeec1398 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -133,7 +133,7 @@ export function WbUtilsComponent({ // TODO: check if debounce is needed here utils.searchCells( event, - searchRef.current as HTMLInputElement, + searchRef.current!, spreadsheetContainerRef?.current ); }; @@ -164,38 +164,37 @@ export function WbUtilsComponent({
{!isUploaded && hasPermission('/workbench/dataset', 'update') ? (
-
) : undefined} - - { - if ( - newSearchPreferences.navigation.direction !== - utils.searchPreferences.navigation.direction - ) { - // TODO: add workbench or cells to parent component - // cells.flushIndexedCellData = true; - } - utils.searchPreferences = newSearchPreferences; - // TODO: figure out what searchCells with SettingsChange does - // if (utils.searchPreferences.search.liveUpdate) - // utils.searchCells({ - // key: 'SettingsChange', - // }).catch(softFail); - }} - /> - + { + if ( + newSearchPreferences.navigation.direction !== + utils.searchPreferences.navigation.direction + ) { + // TODO: add workbench or cells to parent component + // cells.flushIndexedCellData = true; + } + utils.searchPreferences = newSearchPreferences; + // TODO: figure out what searchCells with SettingsChange does + // if (utils.searchPreferences.search.liveUpdate) + // utils.searchCells({ + // key: 'SettingsChange', + // }).catch(softFail); + }} + /> (); - const hot = React.useMemo( - () => hotTable?.hotInstance ?? undefined, - [hotTable] - ); + const hot = hotTable?.hotInstance ?? undefined; const isUploaded = React.useMemo( () => dataset.uploadresult !== null && dataset.uploadresult.success, @@ -201,84 +198,79 @@ export function WbView({ }, [showResults]); return ( - <> - -
+
+ + -
- -
- - {commonText.tools()} - - - {canUpdate || isMapped ? ( - - {wbPlanText.dataMapper()} - - ) : undefined} - -
- {showToolkit ? ( - + {commonText.tools()} + + + {canUpdate || isMapped ? ( + + {wbPlanText.dataMapper()} + ) : undefined} -
-
- } - hot={hot} - isUploaded={isUploaded} - data={data} - workbench={workbench} - mappings={mappings!} - checkDeletedFail={checkDeletedFail} - spreadsheetChanged={spreadsheetChanged} - /> -
- {showResults ? ( - - ) : undefined} -
- - - +
+ {showToolkit && typeof hot === 'object' ? ( + + ) : undefined} +
+ + {showResults ? ( + + ) : undefined} +
+ +
); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index d0ae5068a45..f7199cdc4d7 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -9,7 +9,7 @@ import { ping } from '../../utils/ajax/ping'; import { setCache } from '../../utils/cache'; import { f } from '../../utils/functools'; import type { RA } from '../../utils/types'; -import { ensure, overwriteReadOnly } from '../../utils/types'; +import { overwriteReadOnly } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { schema } from '../DataModel/schema'; import { getHotPlugin } from './handsontable'; @@ -24,7 +24,7 @@ export function getHotHooks( ): Partial { let sortConfigIsSet: boolean = false; - return ensure>()({ + return { /* * After cell is rendered, we need to reApply metaData classes * NOTE: @@ -474,7 +474,7 @@ export function getHotHooks( // An ugly fix for jQuery's dialogs conflicting with HOT if (dropIndex === undefined || !workbench.hot) return; - workbench.cells!.flushIndexedCellData = true; + workbench.cells.flushIndexedCellData = true; const columnOrder = workbench.dataset.columns.map((_, visualCol) => workbench.hot!.toPhysicalColumn(visualCol) @@ -520,7 +520,7 @@ export function getHotHooks( workbench.hot?.selectCells(newSelection); } }, - }); + }; } /** diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 2dd5d73a1b0..50e00a89ec7 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -78,20 +78,18 @@ export function WorkBench(): JSX.Element { {wbText.dataSetDeletedDescription()} ) : ( - <> -
-
- -
-
- +
+
+ +
+
); } From bf0f54cec94d1b5b18f8bd39a8c1e56037511d4d Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 16 Apr 2024 13:39:34 -0400 Subject: [PATCH 061/153] Add minor improvements - Typing fixes - Load components only after hot loads - Initialize some variables only after hot loads --- .../components/WbToolkit/CoordinateConverter.tsx | 6 +++--- .../js_src/lib/components/WbToolkit/GeoLocate.tsx | 6 +++--- .../lib/components/WbToolkit/WbLeafletMap.tsx | 15 ++++++++------- .../js_src/lib/components/WbToolkit/WbToolkit.tsx | 10 +++++----- .../lib/components/WorkBench/WbSpreadsheet.tsx | 13 +++++++------ .../js_src/lib/components/WorkBench/WbUtils.tsx | 6 +----- .../js_src/lib/components/WorkBench/WbView.tsx | 4 ++-- .../js_src/lib/components/WorkBench/hooks.ts | 4 ++-- 8 files changed, 31 insertions(+), 33 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 4359a06c41d..bf20f994327 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -31,7 +31,7 @@ export function WbConvertCoordinates({ readonly hasLocality: boolean; readonly dataset: Dataset; readonly data: RA>; - readonly mappings: WbMapping; + readonly mappings: WbMapping | undefined; readonly hot: Handsontable; }): JSX.Element { const [showConvertCoords, openConvertCoords, closeConvertCoords] = @@ -47,7 +47,7 @@ export function WbConvertCoordinates({ > {wbText.convertCoordinates()} - {showConvertCoords && ( + {showConvertCoords && mappings !== undefined ? ( - )} + ): undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index 3fc208aa121..3ab978e90c4 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -33,7 +33,7 @@ export function WbGeoLocate({ readonly hasLocality: boolean; readonly hot: Handsontable; readonly dataset: Dataset; - readonly mappings: WbMapping; + readonly mappings: WbMapping | undefined; }): JSX.Element { const [showGeoLocate, openGeoLocate, closeGeoLocate] = useBooleanState(); return ( @@ -47,14 +47,14 @@ export function WbGeoLocate({ > {localityText.geoLocate()} - {showGeoLocate && mappings && ( + {showGeoLocate && mappings !== undefined ? ( - )} + ) : undefined} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx index a3854eea08b..b1b81128347 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx @@ -21,10 +21,11 @@ export function WbLeafletMap({ readonly hasLocality: boolean; readonly hot: Handsontable; readonly dataset: Dataset; - readonly mappings: WbMapping; + readonly mappings: WbMapping | undefined; }): JSX.Element { const [showLeafletMap, openLeafletMap, closeLeafletMap] = useBooleanState(); const localityPoints = React.useMemo(() => { + if (mappings === undefined) return undefined; const selection = getSelectedLocalities( hot, dataset.columns, @@ -36,11 +37,11 @@ export function WbLeafletMap({ return getLocalitiesDataFromSpreadsheet( mappings.localityColumns, - selection.visualRows.map((visualRow) => hot!.getDataAtRow(visualRow)), - getVisualHeaders(hot!, dataset.columns), + selection.visualRows.map((visualRow) => hot.getDataAtRow(visualRow)), + getVisualHeaders(hot, dataset.columns), selection.visualRows ); - }, [mappings.localityColumns]); + }, [mappings?.localityColumns]); return ( <> @@ -62,10 +63,10 @@ export function WbLeafletMap({ const rowNumber = localityPoints[localityPoint].rowNumber.value; if (typeof rowNumber !== 'number') throw new Error('rowNumber must be a number'); - const [_currentRow, currentCol] = getSelectedLast(hot!); - hot?.scrollViewportTo(rowNumber, currentCol); + const [_currentRow, currentCol] = getSelectedLast(hot); + hot.scrollViewportTo(rowNumber, currentCol); // Select entire row - hot?.selectRows(rowNumber); + hot.selectRows(rowNumber); }} /> )} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx index f4db20efa8c..4645d1c9196 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx @@ -28,7 +28,7 @@ export function WbToolkit({ triggerDatasetRefresh, }: { readonly dataset: Dataset; - readonly hot: Handsontable | undefined; + readonly hot: Handsontable; readonly mappings: WbMapping | undefined; readonly data: RA>; readonly onDatasetDeleted: () => void; @@ -50,7 +50,7 @@ export function WbToolkit({ ).catch(raise); }; - const hasLocality = mappings ? mappings.localityColumns.length > 0 : false; + const hasLocality = mappings !== undefined ? mappings.localityColumns.length > 0 : false; // TODO: Render certain elements only after hot has loaded return ( @@ -92,14 +92,14 @@ export function WbToolkit({ dataset={dataset} data={data} mappings={mappings} - hot={hot!} + hot={hot} hasLocality={hasLocality} /> @@ -109,7 +109,7 @@ export function WbToolkit({ diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index fae88a4185d..08ff8427659 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -94,12 +94,12 @@ function WbSpreadsheetComponent({ spreadsheetChanged, }: { readonly dataset: Dataset; - readonly setHotTable: React.RefObject; + readonly setHotTable: React.RefCallback; readonly hot: Handsontable | undefined; readonly isUploaded: boolean; readonly data: RA>; readonly workbench: Workbench; - readonly mappings: WbMapping; + readonly mappings: WbMapping | undefined; readonly checkDeletedFail: (_: number) => boolean; readonly spreadsheetChanged: () => void; }): JSX.Element { @@ -162,6 +162,7 @@ function WbSpreadsheetComponent({ }); }; + // @ts-expect-error Typing error for separators as DetailedSettings only allows one separator const contextMenuConfig: DetailedSettings | undefined = hot === undefined ? undefined @@ -257,14 +258,14 @@ function WbSpreadsheetComponent({ callback: () => openDisambiguationDialog(), }, separator_1: '---------', - fill_down: fillCellsContextMenuItem(hot!, 'down', isReadOnly), - fill_up: fillCellsContextMenuItem(hot!, 'up', isReadOnly), + fill_down: fillCellsContextMenuItem(hot, 'down', isReadOnly), + fill_up: fillCellsContextMenuItem(hot, 'up', isReadOnly), separator_2: '---------', undo: { - disabled: () => hot?.isUndoAvailable() !== true || isReadOnly, + disabled: () => hot.isUndoAvailable() !== true || isReadOnly, }, redo: { - disabled: () => hot?.isRedoAvailable() !== true || isReadOnly, + disabled: () => hot.isRedoAvailable() !== true || isReadOnly, }, } as const), }; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 754aeec1398..efde893df4d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -138,10 +138,6 @@ export function WbUtilsComponent({ ); }; - const handleReplace = (event: KeyboardEvent) => { - utils.replaceCells(event, replaceRef.current!); - }; - return (
utils.replaceCells(event, replaceRef.current!)} disabled={isReadOnly} />
diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index d141eebf660..4d808f15377 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -74,7 +74,7 @@ export function WbView({ [dataset] ); - const [hotTable, setHotTable] = React.useState(); + const [hotTable, setHotTable] = React.useState(null); const hot = hotTable?.hotInstance ?? undefined; const isUploaded = React.useMemo( @@ -249,7 +249,7 @@ export function WbView({ isUploaded={isUploaded} data={data} workbench={workbench} - mappings={mappings!} + mappings={mappings} checkDeletedFail={checkDeletedFail} spreadsheetChanged={spreadsheetChanged} /> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index f7199cdc4d7..265a9cd7420 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -69,9 +69,9 @@ export function getHotHooks( visualRow, visualCol ); - if (workbench.mappings.mappedHeaders?.[physicalCol] === undefined) + if (workbench.mappings?.mappedHeaders?.[physicalCol] === undefined) td.classList.add('text-gray-500'); - if (workbench.mappings.coordinateColumns?.[physicalCol] !== undefined) + if (workbench.mappings?.coordinateColumns?.[physicalCol] !== undefined) td.classList.add('wb-coordinate-cell'); }, From 811698872ec313c6e00cd2b4c9cf9925b13a81e9 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:01:52 -0400 Subject: [PATCH 062/153] Move hot props into a different file - Also changes column headers to JSX --- .../components/WorkBench/WbSpreadsheet.tsx | 119 ++------------ .../lib/components/WorkBench/hotProps.tsx | 150 ++++++++++++++++++ 2 files changed, 164 insertions(+), 105 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 08ff8427659..a714abe8b0c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -12,12 +12,8 @@ import React from 'react'; import type { DetailedSettings } from 'handsontable/plugins/contextMenu'; import { LANGUAGE } from '../../localization/utils/config'; -import { wbPlanText } from '../../localization/wbPlan'; -import { f } from '../../utils/functools'; import { type RA } from '../../utils/types'; import { legacyNonJsxIcons } from '../Atoms/Icons'; -import { getTable } from '../DataModel/tables'; -import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbMapping } from './mapping'; import { getSelectedRegions } from './hotHelpers'; @@ -42,6 +38,7 @@ import { MappingPath } from '../WbPlanView/Mapper'; import { registerAllModules } from 'handsontable/registry'; import { Workbench } from './WbView'; import { ReadOnlyContext } from '../Core/Contexts'; +import { getHotProps } from './hotProps'; registerAllModules(); @@ -289,107 +286,19 @@ function WbSpreadsheetComponent({ } }, [dataset.rowresults, hot]); - // TODO: Move hottable props to a different file? - const autoWrapCol = React.useMemo( - () => userPreferences.get('workBench', 'editor', 'autoWrapCol'), - [] - ); - const autoWrapRow = React.useMemo( - () => userPreferences.get('workBench', 'editor', 'autoWrapRow'), - [] - ); - const columns = React.useMemo( - () => - Array.from( - // Last column is invisible and contains disambiguation metadata - { length: dataset.columns.length + 1 }, - (_, physicalCol) => ({ - // Get data from nth column for nth column - data: physicalCol, - }) - ), - [] - ); - const enterMoves = React.useMemo( - () => - userPreferences.get('workBench', 'editor', 'enterMoveDirection') === 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 }, - [] - ); - - const colHeaders = React.useCallback((physicalCol: number) => { - const tableIcon = mappings?.mappedHeaders?.[physicalCol]; - const isMapped = tableIcon !== undefined; - const mappingCol = physicalColToMappingCol(physicalCol); - const tableName = - (typeof mappingCol === 'number' - ? mappings?.tableNames[mappingCol] - : undefined) ?? tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; - const tableLabel = isMapped - ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' - : ''; - // REFACTOR: use new table icons - return `
- ${ - isMapped - ? `${tableLabel}` - : `${legacyNonJsxIcons.ban}` - } - - ${dataset.columns[physicalCol]} - -
`; - }, []); - - const enterBeginsEditing = React.useMemo( - () => userPreferences.get('workBench', 'editor', 'enterBeginsEditing'), - [] - ); - - const hiddenColumns = React.useMemo(() => { - return { - // Hide the disambiguation column - columns: [dataset.columns.length], - indicators: false, - // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 - copyPasteEnabled: false, - }; - }, []); - - const hiddenRows = React.useMemo(() => { - return { - rows: [], - indicators: false, - // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 - copyPasteEnabled: false, - }; - }, []); - - const minSpareRows = React.useMemo( - () => userPreferences.get('workBench', 'editor', 'minSpareRows'), - [] - ); - - const tabMoves = React.useMemo( - () => - userPreferences.get('workBench', 'editor', 'tabMoveDirection') === 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 }, - [] - ); - - const comments = React.useMemo(() => { - return { displayDelay: 100 }; - }, []); + const { + autoWrapCol, + autoWrapRow, + columns, + enterMoves, + colHeaders, + enterBeginsEditing, + hiddenRows, + hiddenColumns, + minSpareRows, + tabMoves, + comments, + } = getHotProps({ dataset, mappings, physicalColToMappingCol }); const hooks = getHotHooks( workbench, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx new file mode 100644 index 00000000000..2d4f8dca6a2 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import ReactDOMServer from 'react-dom/server'; + +import { userPreferences } from '../Preferences/userPreferences'; +import { Dataset } from '../WbPlanView/Wrapped'; +import { WbMapping } from './mapping'; +import { f } from '../../utils/functools'; +import { getTable } from '../DataModel/tables'; +import { wbPlanText } from '../../localization/wbPlan'; +import { icons } from '../Atoms/Icons'; + +const comments = { displayDelay: 100 }; + +const hiddenRows = { + rows: [], + indicators: false, + // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 + copyPasteEnabled: false, +}; + +function ColumnHeader({ + isMapped, + tableLabel, + tableIcon, + columnName, +}: { + readonly isMapped: boolean; + readonly tableLabel: string; + readonly tableIcon: string | undefined; + readonly columnName: string; +}): JSX.Element { + return ( +
+ {isMapped ? ( + {tableLabel} + ) : ( + + {icons.ban} + + )} + {columnName} +
+ ); +} + +export function getHotProps({ + dataset, + mappings, + physicalColToMappingCol, +}: { + readonly dataset: Dataset; + readonly mappings: WbMapping | undefined; + readonly physicalColToMappingCol: (physicalCol: number) => number | undefined; +}) { + const autoWrapCol = userPreferences.get('workBench', 'editor', 'autoWrapCol'); + + const autoWrapRow = userPreferences.get('workBench', 'editor', 'autoWrapRow'); + + const columns = React.useMemo( + () => + Array.from( + // Last column is invisible and contains disambiguation metadata + { length: dataset.columns.length + 1 }, + (_, physicalCol) => ({ + // Get data from nth column for nth column + data: physicalCol, + }) + ), + [dataset.columns.length] + ); + + const enterMoves = + userPreferences.get('workBench', 'editor', 'enterMoveDirection') === 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 }; + + const colHeaders = React.useCallback( + (physicalCol: number) => { + const tableIcon = mappings?.mappedHeaders?.[physicalCol]; + const isMapped = tableIcon !== undefined; + const mappingCol = physicalColToMappingCol(physicalCol); + const tableName = + (typeof mappingCol === 'number' + ? mappings?.tableNames[mappingCol] + : undefined) ?? tableIcon?.split('/').slice(-1)?.[0]?.split('.')?.[0]; + const tableLabel = isMapped + ? f.maybe(tableName, getTable)?.label ?? tableName ?? '' + : ''; + // REFACTOR: use new table icons + return ReactDOMServer.renderToString( + + ); + }, + [mappings] + ); + + const enterBeginsEditing = userPreferences.get( + 'workBench', + 'editor', + 'enterBeginsEditing' + ); + + const hiddenColumns = React.useMemo(() => { + return { + // Hide the disambiguation column + columns: [dataset.columns.length], + indicators: false, + // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 + copyPasteEnabled: false, + }; + }, []); + + const minSpareRows = userPreferences.get( + 'workBench', + 'editor', + 'minSpareRows' + ); + + const tabMoves = + userPreferences.get('workBench', 'editor', 'tabMoveDirection') === 'col' + ? { col: 1, row: 0 } + : { col: 0, row: 1 }; + + return { + autoWrapCol, + autoWrapRow, + columns, + enterMoves, + colHeaders, + enterBeginsEditing, + hiddenRows, + hiddenColumns, + minSpareRows, + tabMoves, + comments, + }; +} From a18cfea1cb26bd072114ebafa08ccd3393d47770 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 16 Apr 2024 16:21:17 -0400 Subject: [PATCH 063/153] Move show results logic into a hook --- .../lib/components/WbActions/useResults.ts | 84 +++++++++++++++++++ .../lib/components/WorkBench/WbView.tsx | 73 ++-------------- 2 files changed, 90 insertions(+), 67 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts new file mode 100644 index 00000000000..0188c6f9ee1 --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -0,0 +1,84 @@ +import React from "react"; +import Handsontable from "handsontable"; + +import { Workbench } from "../WorkBench/WbView"; +import { getHotPlugin } from "../WorkBench/handsontable"; +import { useBooleanState } from "../../hooks/useBooleanState"; + +export function useResults({ + hot, + workbench, + spreadsheetContainerRef +}: { + readonly hot: Handsontable | undefined; + readonly workbench: Workbench; + readonly spreadsheetContainerRef: React.RefObject; +}) { + const [showResults, _, closeResults, toggleResults] = useBooleanState(); + + const initialHiddenRows = React.useMemo( + () => + hot === undefined ? [] : getHotPlugin(hot, 'hiddenRows').getHiddenRows(), + [hot] + ); + const initialHiddenCols = React.useMemo( + () => + hot === undefined + ? [] + : getHotPlugin(hot, 'hiddenColumns').getHiddenColumns(), + [hot] + ); + + // Makes the hot changes required for upload view results + React.useEffect(() => { + if (hot === undefined) return; + + const rowsToInclude = new Set(); + const colsToInclude = new Set(); + Object.entries(workbench.cells.cellMeta).forEach(([physicalRow, rowMeta]) => + rowMeta.forEach((metaArray, physicalCol) => { + if (!workbench.cells.getCellMetaFromArray(metaArray, 'isNew')) return; + rowsToInclude.add((physicalRow as unknown as number) | 0); + colsToInclude.add(physicalCol); + }) + ); + const rowsToHide = workbench.data + .map((_, physicalRow) => physicalRow) + .filter( + (physicalRow) => + !rowsToInclude.has(physicalRow) && + !initialHiddenRows.includes(physicalRow) + ) + .map(hot.toVisualRow); + const colsToHide = workbench.dataset.columns + .map((_, physicalCol) => physicalCol) + .filter( + (physicalCol) => + !colsToInclude.has(physicalCol) && + !initialHiddenCols.includes(physicalCol) + ) + .map(hot.toVisualColumn); + + if (showResults) { + getHotPlugin(hot, 'hiddenRows').hideRows(rowsToHide); + getHotPlugin(hot, 'hiddenColumns').hideColumns(colsToHide); + + workbench.utils.toggleCellTypes( + 'newCells', + 'remove', + spreadsheetContainerRef?.current + ); + } else { + getHotPlugin(hot, 'hiddenRows').showRows( + rowsToHide.filter((visualRow) => !initialHiddenRows.includes(visualRow)) + ); + getHotPlugin(hot, 'hiddenColumns').showColumns( + colsToHide.filter((visualCol) => !initialHiddenCols.includes(visualCol)) + ); + } + + hot.render(); + }, [showResults]); + + return { showResults, closeResults, toggleResults } +} \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 4d808f15377..65b6e5c4f0c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -27,7 +27,6 @@ import { hasPermission } from '../Permissions/helpers'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbCellCounts, WbCellMeta } from './CellMeta'; import { Disambiguation } from './DisambiguationLogic'; -import { getHotPlugin } from './handsontable'; import type { WbMapping } from './mapping'; import { parseWbMappings } from './mapping'; import { WbUploaded } from './Results'; @@ -39,6 +38,7 @@ import { WbSpreadsheet } from './WbSpreadsheet'; import { useBooleanState } from '../../hooks/useBooleanState'; import { WbToolkit } from '../WbToolkit/WbToolkit'; import { ReadOnlyContext } from '../Core/Contexts'; +import { useResults } from '../WbActions/useResults'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -130,72 +130,11 @@ export function WbView({ const [showToolkit, _openToolkit, _closeToolkit, toggleToolkit] = useBooleanState(); - // TODO: move all states and logic for Results into a hook - const [showResults, _, closeResults, toggleResults] = useBooleanState(); - - const initialHiddenRows = React.useMemo( - () => - hot === undefined ? [] : getHotPlugin(hot, 'hiddenRows').getHiddenRows(), - [hot] - ); - const initialHiddenCols = React.useMemo( - () => - hot === undefined - ? [] - : getHotPlugin(hot, 'hiddenColumns').getHiddenColumns(), - [hot] - ); - - // Makes the hot changes required for upload view results - React.useEffect(() => { - if (!workbench.hot) return; - - const rowsToInclude = new Set(); - const colsToInclude = new Set(); - Object.entries(workbench.cells.cellMeta).forEach(([physicalRow, rowMeta]) => - rowMeta.forEach((metaArray, physicalCol) => { - if (!workbench.cells.getCellMetaFromArray(metaArray, 'isNew')) return; - rowsToInclude.add((physicalRow as unknown as number) | 0); - colsToInclude.add(physicalCol); - }) - ); - const rowsToHide = workbench.data - .map((_, physicalRow) => physicalRow) - .filter( - (physicalRow) => - !rowsToInclude.has(physicalRow) && - !initialHiddenRows.includes(physicalRow) - ) - .map(workbench.hot.toVisualRow); - const colsToHide = workbench.dataset.columns - .map((_, physicalCol) => physicalCol) - .filter( - (physicalCol) => - !colsToInclude.has(physicalCol) && - !initialHiddenCols.includes(physicalCol) - ) - .map(workbench.hot.toVisualColumn); - - if (showResults) { - getHotPlugin(workbench.hot, 'hiddenRows').hideRows(rowsToHide); - getHotPlugin(workbench.hot, 'hiddenColumns').hideColumns(colsToHide); - - workbench.utils.toggleCellTypes( - 'newCells', - 'remove', - spreadsheetContainerRef?.current - ); - } else { - getHotPlugin(workbench.hot, 'hiddenRows').showRows( - rowsToHide.filter((visualRow) => !initialHiddenRows.includes(visualRow)) - ); - getHotPlugin(workbench.hot, 'hiddenColumns').showColumns( - colsToHide.filter((visualCol) => !initialHiddenCols.includes(visualCol)) - ); - } - - workbench.hot.render(); - }, [showResults]); + const { showResults, closeResults, toggleResults } = useResults({ + hot, + workbench, + spreadsheetContainerRef, + }); return ( From 73f854920b582aba947a51568f4919ad3e3cfaac Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:21:18 -0400 Subject: [PATCH 064/153] Complete all TODOs - moves some components to diferent files as well --- .../lib/components/WbActions/WbActions.tsx | 138 ++++++++--------- .../components/WbActions/WbNoUploadPlan.tsx | 56 +++++++ .../lib/components/WbActions/WbSave.tsx | 2 - .../lib/components/WbActions/useResults.ts | 3 +- .../lib/components/WbToolkit/WbToolkit.tsx | 1 - .../WorkBench/DisambiguationLogic.tsx | 3 +- .../lib/components/WorkBench/Navigation.tsx | 91 +++++++++++ .../components/WorkBench/WbSpreadsheet.tsx | 5 +- .../lib/components/WorkBench/WbUtils.tsx | 146 ++++-------------- .../lib/components/WorkBench/WbValidation.tsx | 14 +- .../lib/components/WorkBench/WbView.tsx | 9 +- .../js_src/lib/components/WorkBench/hooks.ts | 17 +- .../js_src/lib/components/WorkBench/index.tsx | 1 - 13 files changed, 260 insertions(+), 226 deletions(-) create mode 100644 specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx create mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index b1afa2d1f6b..4f3fb60036a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { commonText } from '../../localization/common'; -import { wbPlanText } from '../../localization/wbPlan'; import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; import { ping } from '../../utils/ajax/ping'; import { Button } from '../Atoms/Button'; -import { Link } from '../Atoms/Link'; import { Dialog } from '../Molecules/Dialog'; import type { Status } from '../WbPlanView/Wrapped'; import { CreateRecordSetButton } from '../WorkBench/RecordSet'; @@ -26,6 +24,8 @@ import { WbSave } from './WbSave'; import { GET } from '../../utils/utils'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { LoadingContext } from '../Core/Contexts'; +import { WbNoUploadPlan } from './WbNoUploadPlan'; +import { WbCellCounts } from '../WorkBench/CellMeta'; export function useWbActions({ datasetId, @@ -46,8 +46,6 @@ export function useWbActions({ const startUpload = (newMode: WbStatus): void => { workbench.validation.stopLiveValidation(); - // TODO: figure out what to do in updateValidationButton(); - workbench.validation.updateValidationButton(); loading( ping(`/api/workbench/${newMode}/${datasetId}/`, { method: 'POST', @@ -84,50 +82,7 @@ export function useWbActions({ }; } -export function WbActions({ - dataset, - hasUnsavedChanges, - isUploaded, - isResultsOpen, - workbench, - mappings, - checkDeletedFail, - onDatasetRefresh: handleRefresh, - onSpreadsheetUpToDate: handleSpreadsheetUpToDate, - onToggleResults, -}: { - readonly dataset: Dataset; - readonly hasUnsavedChanges: boolean; - readonly isUploaded: boolean; - readonly isResultsOpen: boolean; - readonly workbench: Workbench; - readonly mappings: WbMapping | undefined; - readonly checkDeletedFail: (statusCode: number) => void; - readonly onDatasetRefresh: () => void; - readonly onSpreadsheetUpToDate: () => void; - readonly onToggleResults: () => void; -}): JSX.Element { - const [canLiveValidate] = userPreferences.use( - 'workBench', - 'general', - 'liveValidation' - ); - const [noUploadPlan, openNoUploadPlan, closeNoUploadPlan] = useBooleanState(); - const [showStatus, openStatus, closeStatus] = useBooleanState(); - const [operationAborted, openAbortedMessage, closeAbortedMessage] = - useBooleanState(); - const [operationCompleted, openOperationCompleted, closeOperationCompleted] = - useBooleanState(); - const { modeRef, refreshInitiatorAborted, ...actions } = useWbActions({ - datasetId: dataset.id, - onRefresh: handleRefresh, - checkDeletedFail, - onOpenStatus: openStatus, - workbench, - }); - - // TODO: put the message rendering logic in a utility function? - const cellCounts = workbench.cellCounts[GET]; +function getMessage(cellCounts: WbCellCounts, mode: WbStatus) { const messages = { validate: cellCounts.invalidCells === 0 @@ -176,34 +131,67 @@ export function WbActions({ }, }; - React.useEffect(() => { - if ( - !isUploaded && - (mappings?.lines ?? []).length === 0 && - hasPermission('/workbench/dataset', 'upload') - ) { - openNoUploadPlan(); - } - }, []); + return messages[mode]; +} + +export function WbActions({ + dataset, + hasUnsavedChanges, + isUploaded, + isResultsOpen, + workbench, + mappings, + checkDeletedFail, + onDatasetRefresh: handleRefresh, + onSpreadsheetUpToDate: handleSpreadsheetUpToDate, + onToggleResults, +}: { + readonly dataset: Dataset; + readonly hasUnsavedChanges: boolean; + readonly isUploaded: boolean; + readonly isResultsOpen: boolean; + readonly workbench: Workbench; + readonly mappings: WbMapping | undefined; + readonly checkDeletedFail: (statusCode: number) => void; + readonly onDatasetRefresh: () => void; + readonly onSpreadsheetUpToDate: () => void; + readonly onToggleResults: () => void; +}): JSX.Element { + const [canLiveValidate] = userPreferences.use( + 'workBench', + 'general', + 'liveValidation' + ); + const [noUploadPlan, openNoUploadPlan, closeNoUploadPlan] = useBooleanState(); + const [showStatus, openStatus, closeStatus] = useBooleanState(); + const [operationAborted, openAbortedMessage, closeAbortedMessage] = + useBooleanState(); + const [operationCompleted, openOperationCompleted, closeOperationCompleted] = + useBooleanState(); + const { modeRef, refreshInitiatorAborted, ...actions } = useWbActions({ + datasetId: dataset.id, + onRefresh: handleRefresh, + checkDeletedFail, + onOpenStatus: openStatus, + workbench, + }); + + const cellCounts = workbench.cellCounts[GET]; + const message = + modeRef.current === undefined + ? undefined + : getMessage(cellCounts, modeRef.current); return ( <> - {noUploadPlan ? ( - - {commonText.close()} - - {commonText.create()} - - - } - header={wbPlanText.noUploadPlan()} - onClose={closeNoUploadPlan} - > - {wbPlanText.noUploadPlanDescription()} - - ) : undefined} + {!isUploaded && hasPermission('/workbench/dataset', 'validate') ? ( {commonText.close()} } - header={messages[modeRef.current!].header} + header={message!.header} onClose={closeOperationCompleted} > - {messages[modeRef.current!].message} + {message?.message} ) : undefined} {operationAborted ? ( diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx new file mode 100644 index 00000000000..f2c213acb1c --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Dialog } from '../Molecules/Dialog'; +import { Button } from '../Atoms/Button'; +import { Link } from '../Atoms/Link'; +import { commonText } from '../../localization/common'; +import { wbPlanText } from '../../localization/wbPlan'; +import { hasPermission } from '../Permissions/helpers'; +import { WbMapping } from '../WorkBench/mapping'; + +export function WbNoUploadPlan({ + isUploaded, + mappings, + datasetId, + noUploadPlan, + onOpenNoUploadPlan: handleOpenNoUploadPlan, + onCloseNoUploadPlan: handleCloseNoUploadPlan +} : { + readonly isUploaded: boolean; + readonly mappings: WbMapping | undefined; + readonly datasetId: number; + readonly noUploadPlan: boolean; + readonly onOpenNoUploadPlan: () => void; + readonly onCloseNoUploadPlan: () => void; +}): JSX.Element { + React.useEffect(() => { + if ( + !isUploaded && + (mappings?.lines ?? []).length === 0 && + hasPermission('/workbench/dataset', 'upload') + ) { + handleOpenNoUploadPlan(); + } + }, []); + + return ( + <> + {noUploadPlan ? ( + + {commonText.close()} + + {commonText.create()} + + + } + header={wbPlanText.noUploadPlan()} + onClose={handleCloseNoUploadPlan} + > + {wbPlanText.noUploadPlanDescription()} + + ) : undefined} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index 499fb1b027a..e2fa38f22f2 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -32,8 +32,6 @@ export function WbSave({ // Clear validation overwriteReadOnly(workbench.dataset, 'rowresults', null); workbench.validation.stopLiveValidation(); - // TODO: figure out what to do in updateValidationButton - workbench.validation.updateValidationButton(); // Show saving progress bar openProgressBar(); diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts index 0188c6f9ee1..4d2d9b527f5 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -65,8 +65,7 @@ export function useResults({ workbench.utils.toggleCellTypes( 'newCells', - 'remove', - spreadsheetContainerRef?.current + 'remove' ); } else { getHotPlugin(hot, 'hiddenRows').showRows( diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx index 4645d1c9196..48a97fe095d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx @@ -52,7 +52,6 @@ export function WbToolkit({ const hasLocality = mappings !== undefined ? mappings.localityColumns.length > 0 : false; - // TODO: Render certain elements only after hot has loaded return (
(0); + const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); + const handleTypeToggle = () => { + togglePress(); + utils.toggleCellTypes(name, 'toggle'); + }; + const handlePrevious = () => { + const [_, position] = utils.navigateCells({ + type: name, + direction: 'previous', + currentCellPosition: currentPosition, + totalCount, + }); + setCurrentPosition(position); + }; + const handleNext = () => { + const [_, position] = utils.navigateCells({ + type: name, + direction: 'next', + currentCellPosition: currentPosition, + totalCount, + }); + setCurrentPosition(position); + }; + return ( + + + {icons.chevronLeft} + + + {label} ({currentPosition}/ + {totalCount}) + + + {icons.chevronRight} + + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index a714abe8b0c..94a4d1937ed 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -279,9 +279,10 @@ function WbSpreadsheetComponent({ // Highlight validation cells React.useEffect(() => { - // TODO: Verify if previous code does anything else after validation - if (dataset.rowresults && hot) { + if (dataset.rowresults !== null && hot !== undefined) { validation.getValidationResults(); + if (validation.validationMode === 'static' && !isUploaded) + workbench.utils.toggleCellTypes('invalidCells', 'remove'); workbench.cells.flushIndexedCellData = true; } }, [dataset.rowresults, hot]); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index efde893df4d..f8f743ec7b9 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -19,125 +19,32 @@ import { getInitialSearchPreferences, WbAdvancedSearch, } from './AdvancedSearch'; -import type { WbCellCounts } from './CellMeta'; +import type { WbCellCounts, WbCellMeta } from './CellMeta'; import { getHotPlugin } from './handsontable'; import { getSelectedLast } from './hotHelpers'; import type { Workbench } from './WbView'; import { Input } from '../Atoms/Form'; -import { LocalizedString } from 'typesafe-i18n'; -import { Button } from '../Atoms/Button'; -import { className } from '../Atoms/className'; import { commonText } from '../../localization/common'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { icons } from '../Atoms/Icons'; import { ReadOnlyContext } from '../Core/Contexts'; - -function Navigation({ - name, - label, - totalCount, - utils, - spreadsheetContainerRef, -}: { - readonly name: keyof WbCellCounts; - readonly label: LocalizedString; - readonly totalCount: number; - readonly utils: WbUtils; - readonly spreadsheetContainerRef: React.RefObject; -}): JSX.Element { - const isReadOnly = React.useContext(ReadOnlyContext); - const [currentPosition, setCurrentPosition] = React.useState(0); - const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); - const handleTypeToggle = () => { - togglePress(); - utils.toggleCellTypes(name, 'toggle', spreadsheetContainerRef.current); - }; - const handlePrevious = () => { - const [_, position] = utils.navigateCells({ - type: name, - direction: 'previous', - currentCellPosition: currentPosition, - totalCount, - }); - setCurrentPosition(position); - }; - const handleNext = () => { - const [_, position] = utils.navigateCells({ - type: name, - direction: 'next', - currentCellPosition: currentPosition, - totalCount, - }); - setCurrentPosition(position); - }; - return ( - - - {icons.chevronLeft} - - - {label} ({currentPosition}/ - {totalCount}) - - - {icons.chevronRight} - - - ); -} +import { Navigation } from './Navigation'; export function WbUtilsComponent({ isUploaded, cellCounts, utils, - spreadsheetContainerRef, + cells, + debounceRate }: { readonly isUploaded: boolean; readonly cellCounts: WbCellCounts; readonly utils: WbUtils; - readonly spreadsheetContainerRef: React.RefObject; + readonly cells: WbCellMeta; + readonly debounceRate: number }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); const searchRef = React.useRef(null); const replaceRef = React.useRef(null); - const handleSearch = (event: KeyboardEvent) => { - // TODO: check if debounce is needed here - utils.searchCells( - event, - searchRef.current!, - spreadsheetContainerRef?.current - ); - }; - return (
) => + utils.searchCells( + event, + searchRef.current! + ), + debounceRate, + true + )} />
{!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( @@ -167,8 +81,9 @@ export function WbUtilsComponent({ placeholder={wbText.replace()} title={wbText.replacementValue()} type="search" - // @ts-expect-error KeyboardEvent type does not match? - onKeyDown={(event) => utils.replaceCells(event, replaceRef.current!)} + onKeyDown={(event) => + utils.replaceCells(event, replaceRef.current!) + } disabled={isReadOnly} />
@@ -180,8 +95,7 @@ export function WbUtilsComponent({ newSearchPreferences.navigation.direction !== utils.searchPreferences.navigation.direction ) { - // TODO: add workbench or cells to parent component - // cells.flushIndexedCellData = true; + cells.flushIndexedCellData = true; } utils.searchPreferences = newSearchPreferences; // TODO: figure out what searchCells with SettingsChange does @@ -197,7 +111,6 @@ export function WbUtilsComponent({ name="searchResults" totalCount={cellCounts.searchResults} utils={utils} - spreadsheetContainerRef={spreadsheetContainerRef} /> {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( ) : undefined} {!isUploaded && ( )}
@@ -377,16 +287,14 @@ export class WbUtils { ); // Turn on the respective cell type if it was hidden - // TODO: figure out if this is needed - // this.toggleCellTypes(event, 'remove'); + this.toggleCellTypes(type, 'remove'); return [matchedCell, finalCellPosition]; } searchCells( - event: KeyboardEvent | { readonly key: 'SettingsChange' }, - searchQueryElement: HTMLInputElement, - spreadsheetContainer: HTMLElement | null + event: React.KeyboardEvent | { readonly key: 'SettingsChange' }, + searchQueryElement: HTMLInputElement ) { if (this.workbench.hot === undefined) return; /* @@ -404,10 +312,10 @@ export class WbUtils { return; if (this.parseSearchQuery(searchQueryElement) === undefined) { - this.toggleCellTypes('searchResults', 'add', spreadsheetContainer); + this.toggleCellTypes('searchResults', 'add'); return; } - this.toggleCellTypes('searchResults', 'remove', spreadsheetContainer); + this.toggleCellTypes('searchResults', 'remove'); const data = this.workbench.dataset.rows; const firstVisibleRow = @@ -443,7 +351,7 @@ export class WbUtils { let render = false; /* - * Calling this.wbView.hot.getCell only if cell is within the render + * Calling hot.getCell only if cell is within the render * bounds. * While hot.getCell is supposed to check for this too, doing it this * way makes search about 25% faster @@ -529,10 +437,10 @@ export class WbUtils { toggleCellTypes( navigationType: keyof WbCellCounts, action: 'add' | 'remove' | 'toggle' = 'toggle', - spreadsheetContainer?: HTMLElement | null ): void { const groupName = camelToKebab(navigationType); const cssClassName = `wb-hide-${groupName}`; + const { current: spreadsheetContainer } = this.workbench.spreadsheetContainerRef; if (spreadsheetContainer) spreadsheetContainer.classList[action](cssClassName); } @@ -554,7 +462,7 @@ export class WbUtils { } replaceCells( - event: KeyboardEvent, + event: React.KeyboardEvent, replacementValueElement: HTMLInputElement ) { if ( diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index f640d341fe2..1f2d795befd 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -89,9 +89,8 @@ export class WbValidation { (_, visualRow) => this.workbench.hot!.toPhysicalRow(visualRow) ).reverse(); this.triggerLiveValidation(); - // TODO: toggleCells needs refactor - // this.workbench.utils?.toggleCellTypes('newCells', 'remove'); - // this.workbench.utils?.toggleCellTypes('invalidCells', 'remove'); + this.workbench.utils?.toggleCellTypes('newCells', 'remove'); + this.workbench.utils?.toggleCellTypes('invalidCells', 'remove'); break; } case 'off': { @@ -102,7 +101,6 @@ export class WbValidation { } this.workbench.hot.render(); - this.updateValidationButton(); } public startValidateRow(physicalRow: number): void { @@ -115,7 +113,6 @@ export class WbValidation { triggerLiveValidation() { const pumpValidation = (): void => { - this.updateValidationButton(); if (this.liveValidationStack.length === 0) { this.liveValidationActive = false; return; @@ -144,12 +141,6 @@ export class WbValidation { } } - updateValidationButton(): void { - // TODO: figure out this function - // if (!this.setCount) return; - // this.setCount(this.liveValidationStack.length); - } - gotRowValidationResult(physicalRow: number, result: UploadResult): void { if ( this.validationMode !== 'live' || @@ -377,7 +368,6 @@ export class WbValidation { if (this.workbench.dataset.rowresults === null) { this.validationMode = 'off'; - this.updateValidationButton(); return; } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 65b6e5c4f0c..ce471773ecf 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -53,6 +53,9 @@ export type Workbench = { mappings: WbMapping; utils: WbUtils; cellCounts: GetSet; + spreadsheetContainerRef: React.RefObject; + undoRedoIsHandled: boolean; + spreadsheetChanged: () => void; }; export function WbView({ @@ -111,6 +114,9 @@ export function WbView({ validation: undefined!, utils: undefined!, cellCounts: [cellCounts, setCellCounts], + spreadsheetContainerRef, + undoRedoIsHandled: false, + spreadsheetChanged }; workbench.cells = new WbCellMeta(workbench); workbench.disambiguation = new Disambiguation(workbench); @@ -208,7 +214,8 @@ export function WbView({ isUploaded={isUploaded} cellCounts={cellCounts} utils={workbench.utils} - spreadsheetContainerRef={spreadsheetContainerRef} + cells={workbench.cells} + debounceRate={Math.ceil(clamp(10, data.length / 20, 200))} /> ); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 265a9cd7420..6723e137bac 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -230,13 +230,13 @@ export function getHotHooks( /* * Don't clear disambiguation when afterChange is triggered by - * wbView.hot.undo() from inside of wbView.afterUndoRedo() + * hot.undo() from inside of afterUndoRedo() * FEATURE: consider not clearing disambiguation at all */ - // if (!wbView.undoRedoIsHandled) - // changedRows.forEach((physicalRow) => - // workbench.disambiguation?.clearDisambiguation(physicalRow) - // ); + if (!workbench.undoRedoIsHandled) + changedRows.forEach((physicalRow) => + workbench.disambiguation.clearDisambiguation(physicalRow) + ); changes.forEach( ({ @@ -536,8 +536,7 @@ function afterUndoRedo( data: Action ): void { if ( - // TODO: add this variable to workbench? - // workbench.undoRedoIsHandled || + workbench.undoRedoIsHandled || data.actionType !== 'change' || data.changes.length !== 1 || workbench.hot === undefined @@ -565,9 +564,9 @@ function afterUndoRedo( ) // HOT doesn't seem to like calling undo from inside of afterUndo globalThis.setTimeout(() => { - // wbView.undoRedoIsHandled = true; + workbench.undoRedoIsHandled = true; workbench.hot?.undo(); - // wbView.undoRedoIsHandled = false; + workbench.undoRedoIsHandled = false; workbench.disambiguation.afterChangeDisambiguation(physicalRow); }, 0); else workbench.disambiguation.afterChangeDisambiguation(physicalRow); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 50e00a89ec7..d1c0fde8a83 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -56,7 +56,6 @@ export function WorkBench(): JSX.Element { const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); const navigate = useNavigate(); - // TODO: consider adding this to workbench variable const spreadsheetContainerRef = React.useRef(null); if (!dataSet || !treeRanksLoaded) return ; From 87b8bdd634fe7770393a4ca609487f5b8f384f04 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 16 Apr 2024 18:27:35 -0400 Subject: [PATCH 065/153] Move disambiguate dialogs to WbView --- .../components/WorkBench/WbSpreadsheet.tsx | 120 +----------------- .../lib/components/WorkBench/WbView.tsx | 120 +++++++++++++++++- 2 files changed, 122 insertions(+), 118 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 94a4d1937ed..bf13e4ab583 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -25,16 +25,6 @@ import { strictGetTable } from '../DataModel/tables'; import { getIcon, unknownIcon } from '../InitialContext/icons'; import { commonText } from '../../localization/common'; import { iconClassName } from '../Atoms/Icons'; -import { getSelectedLast } from './hotHelpers'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { getTableFromMappingPath } from '../WbPlanView/navigator'; -import { hasTablePermission } from '../Permissions/helpers'; -import type { Collection } from '../DataModel/specifyTable'; -import { AnySchema } from '../DataModel/helperTypes'; -import { Dialog } from '../Molecules/Dialog'; -import { DisambiguationDialog } from './Disambiguation'; -import { mappingPathToString } from '../WbPlanView/mappingHelpers'; -import { MappingPath } from '../WbPlanView/Mapper'; import { registerAllModules } from 'handsontable/registry'; import { Workbench } from './WbView'; import { ReadOnlyContext } from '../Core/Contexts'; @@ -89,6 +79,7 @@ function WbSpreadsheetComponent({ mappings, checkDeletedFail, spreadsheetChanged, + onClickDisambiguate: handleClickDisambiguate }: { readonly dataset: Dataset; readonly setHotTable: React.RefCallback; @@ -99,6 +90,7 @@ function WbSpreadsheetComponent({ readonly mappings: WbMapping | undefined; readonly checkDeletedFail: (_: number) => boolean; readonly spreadsheetChanged: () => void; + readonly onClickDisambiguate: () => void; }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); const physicalColToMappingCol = (physicalCol: number): number | undefined => @@ -107,57 +99,6 @@ function WbSpreadsheetComponent({ ); const { validation, cells, disambiguation } = workbench; - const [disambiguationMatches, setDisambiguationMatches] = React.useState<{ - readonly physicalCols: RA; - readonly mappingPath: MappingPath; - readonly ids: RA; - readonly key: string; - }>(); - const [disambiguationPhysicalRow, setPhysicalRow] = React.useState(); - const [disambiguationResource, setResource] = - React.useState>(); - const [ - noDisambiguationDialog, - openNoDisambiguationDialog, - closeNoDisambiguationDialog, - ] = useBooleanState(); - const [disambiguationDialog, openDisambiguation, closeDisambiguation] = - useBooleanState(); - - const openDisambiguationDialog = () => { - if (mappings === undefined || hot === undefined) return; - - const [visualRow, visualCol] = getSelectedLast(hot); - const physicalRow = hot.toPhysicalRow(visualRow); - const physicalCol = hot.toPhysicalColumn(visualCol); - - const matches = validation?.uploadResults.ambiguousMatches[ - physicalRow - ].find(({ physicalCols }) => physicalCols.includes(physicalCol)); - if (matches === undefined) return; - const tableName = getTableFromMappingPath( - mappings.baseTable.name, - matches.mappingPath - ); - const table = strictGetTable(tableName); - const resources = new table.LazyCollection({ - filters: { id__in: matches.ids.join(',') }, - }) as Collection; - - (hasTablePermission(table.name, 'read') - ? resources.fetch({ limit: 0 }) - : Promise.resolve(resources) - ).then(({ models }) => { - if (models.length === 0) { - openNoDisambiguationDialog(); - return; - } - setDisambiguationMatches(matches); - setResource(resources); - setPhysicalRow(physicalRow); - openDisambiguation(); - }); - }; // @ts-expect-error Typing error for separators as DetailedSettings only allows one separator const contextMenuConfig: DetailedSettings | undefined = @@ -252,7 +193,7 @@ function WbSpreadsheetComponent({ name: wbText.disambiguate(), disabled: (): boolean => !disambiguation.isAmbiguousCell() || isReadOnly, - callback: () => openDisambiguationDialog(), + callback: () => handleClickDisambiguate(), }, separator_1: '---------', fill_down: fillCellsContextMenuItem(hot, 'down', isReadOnly), @@ -342,61 +283,6 @@ function WbSpreadsheetComponent({ contextMenu={contextMenuConfig} {...hooks} /> - {noDisambiguationDialog && ( - - {wbText.noDisambiguationResultsDescription()} - - )} - {disambiguationDialog && ( - { - disambiguation.setDisambiguation( - disambiguationPhysicalRow!, - disambiguationMatches!.mappingPath, - selected.id - ); - validation?.startValidateRow(disambiguationPhysicalRow!); - hot?.render(); - }} - onSelectedAll={(selected): void => - // Loop backwards so the live validation will go from top to bottom - hot?.batch(() => { - for ( - let visualRow = data.length - 1; - visualRow >= 0; - visualRow-- - ) { - const physicalRow = hot?.toPhysicalRow(visualRow); - if ( - !validation?.uploadResults.ambiguousMatches[ - physicalRow - ]?.find( - ({ key, mappingPath }) => - key === disambiguationMatches!.key && - typeof disambiguation.getDisambiguation(physicalRow)[ - mappingPathToString(mappingPath) - ] !== 'number' - ) - ) - continue; - disambiguation.setDisambiguation( - physicalRow, - disambiguationMatches!.mappingPath, - selected.id - ); - validation?.startValidateRow(physicalRow); - } - }) - } - /> - )} ); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index ce471773ecf..bebf2af4bc8 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -39,6 +39,16 @@ import { useBooleanState } from '../../hooks/useBooleanState'; import { WbToolkit } from '../WbToolkit/WbToolkit'; import { ReadOnlyContext } from '../Core/Contexts'; import { useResults } from '../WbActions/useResults'; +import { getSelectedLast } from './hotHelpers'; +import { getTableFromMappingPath } from '../WbPlanView/navigator'; +import { hasTablePermission } from '../Permissions/helpers'; +import type { Collection } from '../DataModel/specifyTable'; +import { AnySchema } from '../DataModel/helperTypes'; +import { Dialog } from '../Molecules/Dialog'; +import { DisambiguationDialog } from './Disambiguation'; +import { mappingPathToString } from '../WbPlanView/mappingHelpers'; +import { MappingPath } from '../WbPlanView/Mapper'; +import { strictGetTable } from '../DataModel/tables'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -116,7 +126,7 @@ export function WbView({ cellCounts: [cellCounts, setCellCounts], spreadsheetContainerRef, undoRedoIsHandled: false, - spreadsheetChanged + spreadsheetChanged, }; workbench.cells = new WbCellMeta(workbench); workbench.disambiguation = new Disambiguation(workbench); @@ -142,6 +152,58 @@ export function WbView({ spreadsheetContainerRef, }); + const [disambiguationMatches, setDisambiguationMatches] = React.useState<{ + readonly physicalCols: RA; + readonly mappingPath: MappingPath; + readonly ids: RA; + readonly key: string; + }>(); + const [disambiguationPhysicalRow, setPhysicalRow] = React.useState(); + const [disambiguationResource, setResource] = + React.useState>(); + const [ + noDisambiguationDialog, + openNoDisambiguationDialog, + closeNoDisambiguationDialog, + ] = useBooleanState(); + const [disambiguationDialog, openDisambiguation, closeDisambiguation] = + useBooleanState(); + + const openDisambiguationDialog = React.useCallback(() => { + if (mappings === undefined || hot === undefined) return; + + const [visualRow, visualCol] = getSelectedLast(hot); + const physicalRow = hot.toPhysicalRow(visualRow); + const physicalCol = hot.toPhysicalColumn(visualCol); + + const matches = workbench.validation.uploadResults.ambiguousMatches[ + physicalRow + ].find(({ physicalCols }) => physicalCols.includes(physicalCol)); + if (matches === undefined) return; + const tableName = getTableFromMappingPath( + mappings.baseTable.name, + matches.mappingPath + ); + const table = strictGetTable(tableName); + const resources = new table.LazyCollection({ + filters: { id__in: matches.ids.join(',') }, + }) as Collection; + + (hasTablePermission(table.name, 'read') + ? resources.fetch({ limit: 0 }) + : Promise.resolve(resources) + ).then(({ models }) => { + if (models.length === 0) { + openNoDisambiguationDialog(); + return; + } + setDisambiguationMatches(matches); + setResource(resources); + setPhysicalRow(physicalRow); + openDisambiguation(); + }); + }, [mappings, hot]); + return (
{showResults ? ( ) : undefined}
+ {noDisambiguationDialog && ( + + {wbText.noDisambiguationResultsDescription()} + + )} + {disambiguationDialog && ( + { + workbench.disambiguation.setDisambiguation( + disambiguationPhysicalRow!, + disambiguationMatches!.mappingPath, + selected.id + ); + workbench.validation.startValidateRow(disambiguationPhysicalRow!); + hot?.render(); + }} + onSelectedAll={(selected): void => + // Loop backwards so the live validation will go from top to bottom + hot?.batch(() => { + for ( + let visualRow = data.length - 1; + visualRow >= 0; + visualRow-- + ) { + const physicalRow = hot?.toPhysicalRow(visualRow); + if ( + !workbench.validation.uploadResults.ambiguousMatches[ + physicalRow + ]?.find( + ({ key, mappingPath }) => + key === disambiguationMatches!.key && + typeof workbench.disambiguation.getDisambiguation(physicalRow)[ + mappingPathToString(mappingPath) + ] !== 'number' + ) + ) + continue; + workbench.disambiguation.setDisambiguation( + physicalRow, + disambiguationMatches!.mappingPath, + selected.id + ); + workbench.validation.startValidateRow(physicalRow); + } + }) + } + /> + )} Date: Tue, 16 Apr 2024 18:43:08 -0400 Subject: [PATCH 066/153] Rename dataSet to dataset in workbench folders --- .../lib/components/WbActions/WbActions.tsx | 4 +-- .../lib/components/WbActions/WbRollback.tsx | 2 +- .../lib/components/WbToolkit/DevShowPlan.tsx | 14 +++++----- .../lib/components/WorkBench/Components.tsx | 6 ++--- .../lib/components/WorkBench/RecordSet.tsx | 26 +++++++++---------- .../lib/components/WorkBench/Results.tsx | 12 ++++----- .../components/WorkBench/WbSpreadsheet.tsx | 2 +- .../lib/components/WorkBench/WbView.tsx | 4 +-- .../js_src/lib/components/WorkBench/index.tsx | 24 ++++++++--------- 9 files changed, 47 insertions(+), 47 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx index 4f3fb60036a..2cdb0ee3887 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx @@ -282,8 +282,8 @@ export function WbActions({ <> {cellCounts.invalidCells === 0 && modeRef.current === 'upload' && ( { modeRef.current = undefined; diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx index a6a751f8257..c3053db47c8 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx @@ -29,7 +29,7 @@ export function WbRollback({ {rollback ? ( diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx index 667729b3209..607e1182332 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx @@ -40,8 +40,8 @@ export function WbRawPlan({ {showRawPlan && ( { overwriteReadOnly(dataset, 'uploadplan', plan); @@ -56,15 +56,15 @@ export function WbRawPlan({ } function RawUploadPlan({ - dataSetId, - dataSetName: name, + datasetId, + datasetName: name, uploadPlan: rawPlan, onClose: handleClose, onChanged: handleChanged, onDeleted: handleDeleted, }: { - readonly dataSetId: number; - readonly dataSetName: string; + readonly datasetId: number; + readonly datasetName: string; readonly uploadPlan: UploadPlan; readonly onClose: () => void; readonly onChanged: (plan: UploadPlan) => void; @@ -90,7 +90,7 @@ function RawUploadPlan({ const plan = uploadPlan.length === 0 ? null : JSON.parse(uploadPlan); loading( - ping(`/api/workbench/dataset/${dataSetId}/`, { + ping(`/api/workbench/dataset/${datasetId}/`, { method: 'PUT', body: { uploadplan: plan }, expectedErrors: [Http.NOT_FOUND], diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx index b8146093640..20fff9c1fff 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx @@ -8,11 +8,11 @@ import { LoadingContext } from '../Core/Contexts'; import { Dialog, dialogClassNames } from '../Molecules/Dialog'; export function RollbackConfirmation({ - dataSetId, + datasetId, onClose: handleClose, onRollback: handleRollback, }: { - readonly dataSetId: number; + readonly datasetId: number; readonly onClose: () => void; readonly onRollback: () => void; }): JSX.Element { @@ -25,7 +25,7 @@ export function RollbackConfirmation({ loading( - ping(`/api/workbench/unupload/${dataSetId}/`, { + ping(`/api/workbench/unupload/${datasetId}/`, { method: 'POST', }) .then(handleRollback) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx index a1e56b2c98d..f6d5f4653be 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx @@ -16,13 +16,13 @@ import { unsafeNavigate } from '../Router/Router'; import { EditRecordSet } from '../Toolbar/RecordSetEdit'; export function CreateRecordSetButton({ - dataSetId, - dataSetName, + datasetId, + datasetName, onClose: handleClosed, small, }: { - readonly dataSetId: number; - readonly dataSetName: string; + readonly datasetId: number; + readonly datasetName: string; readonly onClose: () => void; readonly small: boolean; }): JSX.Element { @@ -38,8 +38,8 @@ export function CreateRecordSetButton({ {isOpen && ( { handleClose(); handleClosed(); @@ -52,20 +52,20 @@ export function CreateRecordSetButton({ } function CreateRecordSetDialog({ - dataSetId, - dataSetName, + datasetId, + datasetName, onClose: handleClose, }: { - readonly dataSetId: number; - readonly dataSetName: string; + readonly datasetId: number; + readonly datasetName: string; readonly onClose: () => void; }): JSX.Element { const recordSet = React.useMemo( () => new tables.RecordSet.Resource({ - name: wbText.recordSetName({ dataSet: dataSetName }), + name: wbText.recordSetName({ dataSet: datasetName }), }), - [dataSetId] + [datasetId] ); const loading = React.useContext(LoadingContext); @@ -76,7 +76,7 @@ function CreateRecordSetDialog({ onSaving={(unsetUnloadProtect): false => { unsetUnloadProtect(); loading( - ajax(`/api/workbench/create_recordset/${dataSetId}/`, { + ajax(`/api/workbench/create_recordset/${datasetId}/`, { method: 'POST', headers: { Accept: 'application/json' }, body: formData({ name: recordSet.get('name') }), diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx index f606f690321..373b80cdaf1 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx @@ -21,14 +21,14 @@ import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbUploaded({ recordCounts, - dataSetId, - dataSetName, + datasetId, + datasetName, isUploaded, onClose: handleClose, }: { readonly recordCounts: Partial, number>>; - readonly dataSetId: number; - readonly dataSetName: string; + readonly datasetId: number; + readonly datasetName: string; readonly isUploaded: boolean; readonly onClose: () => void; }): JSX.Element { @@ -65,8 +65,8 @@ export function WbUploaded({
{isUploaded && ( diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index bf13e4ab583..1a13b1aebbf 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -193,7 +193,7 @@ function WbSpreadsheetComponent({ name: wbText.disambiguate(), disabled: (): boolean => !disambiguation.isAmbiguousCell() || isReadOnly, - callback: () => handleClickDisambiguate(), + callback: handleClickDisambiguate, }, separator_1: '---------', fill_down: fillCellsContextMenuItem(hot, 'down', isReadOnly), diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index bebf2af4bc8..39e5132a781 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -264,8 +264,8 @@ export function WbView({ {showResults ? ( ) : undefined}
- {noDisambiguationDialog && ( - - {wbText.noDisambiguationResultsDescription()} - - )} - {disambiguationDialog && ( - { - workbench.disambiguation.setDisambiguation( - disambiguationPhysicalRow!, - disambiguationMatches!.mappingPath, - selected.id - ); - workbench.validation.startValidateRow(disambiguationPhysicalRow!); - hot?.render(); - }} - onSelectedAll={(selected): void => - // Loop backwards so the live validation will go from top to bottom - hot?.batch(() => { - for ( - let visualRow = data.length - 1; - visualRow >= 0; - visualRow-- - ) { - const physicalRow = hot?.toPhysicalRow(visualRow); - if ( - !workbench.validation.uploadResults.ambiguousMatches[ - physicalRow - ]?.find( - ({ key, mappingPath }) => - key === disambiguationMatches!.key && - typeof workbench.disambiguation.getDisambiguation( - physicalRow - )[mappingPathToString(mappingPath)] !== 'number' - ) - ) - continue; - workbench.disambiguation.setDisambiguation( - physicalRow, - disambiguationMatches!.mappingPath, - selected.id - ); - workbench.validation.startValidateRow(physicalRow); - } - }) - } - /> - )} + {disambiguationDialogs}
); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx new file mode 100644 index 00000000000..d0a0427ae9a --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx @@ -0,0 +1,155 @@ +import React from 'react'; +import Handsontable from 'handsontable'; + +import { type RA } from '../../utils/types'; +import { MappingPath } from '../WbPlanView/Mapper'; +import type { Collection } from '../DataModel/specifyTable'; +import { AnySchema } from '../DataModel/helperTypes'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { getSelectedLast } from './hotHelpers'; +import { getTableFromMappingPath } from '../WbPlanView/navigator'; +import { hasTablePermission } from '../Permissions/helpers'; +import { strictGetTable } from '../DataModel/tables'; +import { WbMapping } from './mapping'; +import { WbValidation } from './WbValidation'; +import { Dialog } from '../Molecules/Dialog'; +import { wbText } from '../../localization/workbench'; +import { commonText } from '../../localization/common'; +import { DisambiguationDialog } from './Disambiguation'; +import { Disambiguation } from './DisambiguationLogic'; +import { mappingPathToString } from '../WbPlanView/mappingHelpers'; +import { LoadingContext } from '../Core/Contexts'; + +type DisambiguationMatches = { + readonly physicalCols: RA; + readonly mappingPath: MappingPath; + readonly ids: RA; + readonly key: string; +}; + +export function useDisambiguationDialog({ + hot, + data, + disambiguation, + mappings, + validation, +}: { + readonly data: RA>; + readonly validation: WbValidation; + readonly disambiguation: Disambiguation; + readonly mappings: WbMapping | undefined; + readonly hot: Handsontable | undefined; +}) { + const [disambiguationMatches, setDisambiguationMatches] = + React.useState(); + const [disambiguationPhysicalRow, setPhysicalRow] = React.useState(); + const [disambiguationResource, setResource] = + React.useState>(); + const [ + noDisambiguationDialog, + openNoDisambiguationDialog, + closeNoDisambiguationDialog, + ] = useBooleanState(); + + const [disambiguationDialog, openDisambiguation, closeDisambiguation] = + useBooleanState(); + + const loading = React.useContext(LoadingContext); + + const openDisambiguationDialog = React.useCallback(() => { + if (mappings === undefined || hot === undefined) return; + + const [visualRow, visualCol] = getSelectedLast(hot); + const physicalRow = hot.toPhysicalRow(visualRow); + const physicalCol = hot.toPhysicalColumn(visualCol); + + const matches = validation.uploadResults.ambiguousMatches[physicalRow].find( + ({ physicalCols }) => physicalCols.includes(physicalCol) + ); + if (matches === undefined) return; + const tableName = getTableFromMappingPath( + mappings.baseTable.name, + matches.mappingPath + ); + const table = strictGetTable(tableName); + const resources = new table.LazyCollection({ + filters: { id__in: matches.ids.join(',') }, + }) as Collection; + + loading( + (hasTablePermission(table.name, 'read') + ? resources.fetch({ limit: 0 }) + : Promise.resolve(resources) + ).then(({ models }) => { + if (models.length === 0) { + openNoDisambiguationDialog(); + return; + } + setDisambiguationMatches(matches); + setResource(resources); + setPhysicalRow(physicalRow); + openDisambiguation(); + }) + ); + }, [mappings, hot]); + + const disambiguationDialogs = ( + <> + {noDisambiguationDialog && ( + + {wbText.noDisambiguationResultsDescription()} + + )} + {disambiguationDialog && ( + { + disambiguation.setDisambiguation( + disambiguationPhysicalRow!, + disambiguationMatches!.mappingPath, + selected.id + ); + validation.startValidateRow(disambiguationPhysicalRow!); + hot?.render(); + }} + onSelectedAll={(selected): void => + // Loop backwards so the live validation will go from top to bottom + hot?.batch(() => { + for ( + let visualRow = data.length - 1; + visualRow >= 0; + visualRow-- + ) { + const physicalRow = hot.toPhysicalRow(visualRow); + if ( + !validation.uploadResults.ambiguousMatches[physicalRow]?.find( + ({ key, mappingPath }) => + key === disambiguationMatches!.key && + typeof disambiguation.getDisambiguation(physicalRow)[ + mappingPathToString(mappingPath) + ] !== 'number' + ) + ) + continue; + disambiguation.setDisambiguation( + physicalRow, + disambiguationMatches!.mappingPath, + selected.id + ); + validation.startValidateRow(physicalRow); + } + }) + } + /> + )} + + ); + + return { openDisambiguationDialog, disambiguationDialogs }; +} From e501576771ce3618204870022297ba584c08dd3a Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:40:02 -0400 Subject: [PATCH 074/153] Add suggestions - also lint + prettier --- .../components/WbActions/WbNoUploadPlan.tsx | 8 +- .../lib/components/WbActions/WbResults.tsx | 27 -- .../lib/components/WbActions/WbRevert.tsx | 6 +- .../lib/components/WbActions/WbRollback.tsx | 50 +++- .../lib/components/WbActions/WbSave.tsx | 22 +- .../lib/components/WbActions/WbUpload.tsx | 12 +- .../lib/components/WbActions/WbValidate.tsx | 45 +-- .../WbActions/{WbActions.tsx => index.tsx} | 270 +++++++++--------- .../lib/components/WbActions/useResults.ts | 6 +- .../lib/components/WbToolkit/ChangeOwner.tsx | 22 +- .../WbToolkit/CoordinateConverter.tsx | 6 +- .../lib/components/WbToolkit/DevShowPlan.tsx | 6 +- .../lib/components/WbToolkit/GeoLocate.tsx | 10 +- .../lib/components/WbToolkit/WbLeafletMap.tsx | 18 +- .../lib/components/WbToolkit/WbToolkit.tsx | 32 +-- .../lib/components/WorkBench/Components.tsx | 49 ---- .../lib/components/WorkBench/DataSetMeta.tsx | 3 +- .../components/WorkBench/Disambiguation.tsx | 8 +- .../lib/components/WorkBench/Navigation.tsx | 26 +- .../lib/components/WorkBench/Results.tsx | 2 +- .../components/WorkBench/WbSpreadsheet.tsx | 54 ++-- .../lib/components/WorkBench/WbUtils.tsx | 52 ++-- .../lib/components/WorkBench/WbValidation.tsx | 12 +- .../lib/components/WorkBench/WbView.tsx | 79 +++-- .../js_src/lib/components/WorkBench/hooks.ts | 18 +- .../lib/components/WorkBench/hotProps.tsx | 24 +- .../js_src/lib/components/WorkBench/index.tsx | 56 ++-- .../WorkBench/useDisambiguationDialog.tsx | 30 +- 28 files changed, 457 insertions(+), 496 deletions(-) delete mode 100644 specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx rename specifyweb/frontend/js_src/lib/components/WbActions/{WbActions.tsx => index.tsx} (94%) delete mode 100644 specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx index c593c8b1a6b..7cbf7c099e9 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { Dialog } from '../Molecules/Dialog'; -import { Button } from '../Atoms/Button'; -import { Link } from '../Atoms/Link'; import { commonText } from '../../localization/common'; import { wbPlanText } from '../../localization/wbPlan'; +import { Button } from '../Atoms/Button'; +import { Link } from '../Atoms/Link'; +import { Dialog } from '../Molecules/Dialog'; import { hasPermission } from '../Permissions/helpers'; -import { WbMapping } from '../WorkBench/mapping'; +import type { WbMapping } from '../WorkBench/mapping'; export function WbNoUploadPlan({ isUploaded, diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx deleted file mode 100644 index 9d27b579c19..00000000000 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbResults.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; - -import { Button } from '../Atoms/Button'; -import { wbText } from '../../localization/workbench'; -import { commonText } from '../../localization/common'; - -export function WbResults({ - hasUnsavedChanges, - isResultsOpen, - onToggleResults: handleToggleResults, -}: { - readonly hasUnsavedChanges: boolean; - readonly isResultsOpen: boolean; - readonly onToggleResults: () => void; -}): JSX.Element { - return ( - - {commonText.results()} - - ); -} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx index b5d4e8a91f1..d78f7124621 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRevert.tsx @@ -1,9 +1,9 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { Button } from '../Atoms/Button'; -import { wbText } from '../../localization/workbench'; import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; +import { Button } from '../Atoms/Button'; import { Dialog } from '../Molecules/Dialog'; export function WbRevert({ @@ -27,8 +27,8 @@ export function WbRevert({ <> {wbText.revert()} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx index f44fde1842a..fb7d4016a3e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx @@ -1,10 +1,13 @@ import React from 'react'; -import type { WbStatus } from '../WorkBench/WbView'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { Button } from '../Atoms/Button'; +import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; -import { RollbackConfirmation } from '../WorkBench/Components'; +import { ping } from '../../utils/ajax/ping'; +import { Button } from '../Atoms/Button'; +import { LoadingContext } from '../Core/Contexts'; +import { Dialog, dialogClassNames } from '../Molecules/Dialog'; +import type { WbStatus } from '../WorkBench/WbView'; export function WbRollback({ datasetId, @@ -36,3 +39,44 @@ export function WbRollback({ ); } + +function RollbackConfirmation({ + datasetId, + onClose: handleClose, + onRollback: handleRollback, +}: { + readonly datasetId: number; + readonly onClose: () => void; + readonly onRollback: () => void; +}): JSX.Element { + const loading = React.useContext(LoadingContext); + return ( + + {commonText.cancel()} + + loading( + ping(`/api/workbench/unupload/${datasetId}/`, { + method: 'POST', + }) + .then(handleRollback) + .finally(handleClose) + ) + } + > + {wbText.rollback()} + + + } + className={{ + container: dialogClassNames.narrowContainer, + }} + header={wbText.beginRollback()} + onClose={handleClose} + > + {wbText.beginRollbackDescription()} + + ); +} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index e2fa38f22f2..1618176bdb9 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -1,17 +1,17 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { Button } from '../Atoms/Button'; -import { className } from '../Atoms/className'; -import { Dialog } from '../Molecules/Dialog'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; -import { loadingBar } from '../Molecules'; -import { Workbench } from '../WorkBench/WbView'; -import { overwriteReadOnly } from '../../utils/types'; -import { ping } from '../../utils/ajax/ping'; import { Http } from '../../utils/ajax/definitions'; +import { ping } from '../../utils/ajax/ping'; +import { overwriteReadOnly } from '../../utils/types'; +import { Button } from '../Atoms/Button'; +import { className } from '../Atoms/className'; import { LoadingContext } from '../Core/Contexts'; +import { loadingBar } from '../Molecules'; +import { Dialog } from '../Molecules/Dialog'; +import type { Workbench } from '../WorkBench/WbView'; export function WbSave({ workbench, @@ -47,8 +47,10 @@ export function WbSave({ .then(() => { handleSpreadsheetUpToDate(); workbench.cells.cellMeta = []; - // TODO: Figure out how to rework searchCells for SettingsChange as input - // workbench.utils?.searchCells({ key: 'SettingsChange' }); + /* + * TODO: Figure out how to rework searchCells for SettingsChange as input + * workbench.utils?.searchCells({ key: 'SettingsChange' }); + */ workbench.hot?.render(); closeProgressBar(); }) @@ -59,9 +61,9 @@ export function WbSave({ <> {commonText.save()} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx index 5662c977f98..4af08318c90 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx @@ -1,13 +1,13 @@ import React from 'react'; -import type { WbMapping } from '../WorkBench/mapping'; -import type { WbStatus } from '../WorkBench/WbView'; import { useBooleanState } from '../../hooks/useBooleanState'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; import { Button } from '../Atoms/Button'; import { Dialog } from '../Molecules/Dialog'; -import { wbText } from '../../localization/workbench'; -import { commonText } from '../../localization/common'; -import { WbCellCounts } from '../WorkBench/CellMeta'; +import type { WbCellCounts } from '../WorkBench/CellMeta'; +import type { WbMapping } from '../WorkBench/mapping'; +import type { WbStatus } from '../WorkBench/WbView'; export function WbUpload({ hasUnsavedChanges, @@ -41,7 +41,6 @@ export function WbUpload({ <> 0} title={ hasUnsavedChanges @@ -50,6 +49,7 @@ export function WbUpload({ ? wbText.uploadUnavailableWhileHasErrors() : '' } + onClick={handleUpload} > {wbText.upload()} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index e8fddaba34c..64eeaed1ae1 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -1,46 +1,51 @@ import React from 'react'; -import type { WbStatus } from '../WorkBench/WbView'; -import { WbValidation } from '../WorkBench/WbValidation'; -import { Button } from '../Atoms/Button'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; +import { Button } from '../Atoms/Button'; +import { userPreferences } from '../Preferences/userPreferences'; +import type { WbValidation } from '../WorkBench/WbValidation'; +import type { WbStatus } from '../WorkBench/WbView'; export function WbValidate({ - canLiveValidate, hasUnsavedChanges, startUpload, validation, }: { - readonly canLiveValidate: boolean; readonly hasUnsavedChanges: boolean; readonly startUpload: (mode: WbStatus) => void; readonly validation: WbValidation; }): JSX.Element { + const [canLiveValidate] = userPreferences.use( + 'workBench', + 'general', + 'liveValidation' + ); const handleValidate = () => startUpload('validate'); const handleToggleDataCheck = () => validation.toggleDataCheck(); return ( <> - - {validation.validationMode === 'live' - ? validation.liveValidationStack.length > 0 - ? commonText.countLine({ - resource: wbText.dataCheckOn(), - count: validation.liveValidationStack.length, - }) - : wbText.dataCheckOn() - : wbText.dataCheck()} - + {canLiveValidate ? ( + + {validation.validationMode === 'live' + ? validation.liveValidationStack.length > 0 + ? commonText.countLine({ + resource: wbText.dataCheckOn(), + count: validation.liveValidationStack.length, + }) + : wbText.dataCheckOn() + : wbText.dataCheck()} + + ) : undefined} {wbText.validate()} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx similarity index 94% rename from specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx rename to specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 2cdb0ee3887..55cd257294e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbActions.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -1,138 +1,29 @@ import React from 'react'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; import { ping } from '../../utils/ajax/ping'; +import { GET } from '../../utils/utils'; import { Button } from '../Atoms/Button'; +import { LoadingContext } from '../Core/Contexts'; +import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { Dialog } from '../Molecules/Dialog'; +import { hasPermission } from '../Permissions/helpers'; import type { Status } from '../WbPlanView/Wrapped'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { WbCellCounts } from '../WorkBench/CellMeta'; +import type { WbMapping } from '../WorkBench/mapping'; import { CreateRecordSetButton } from '../WorkBench/RecordSet'; import { WbStatus as WbStatusComponent } from '../WorkBench/Status'; import type { WbStatus, Workbench } from '../WorkBench/WbView'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import type { WbMapping } from '../WorkBench/mapping'; -import { hasPermission } from '../Permissions/helpers'; -import { userPreferences } from '../Preferences/userPreferences'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { WbResults } from './WbResults'; -import { WbValidate } from './WbValidate'; -import { WbRollback } from './WbRollback'; -import { WbUpload } from './WbUpload'; +import { WbNoUploadPlan } from './WbNoUploadPlan'; import { WbRevert } from './WbRevert'; +import { WbRollback } from './WbRollback'; import { WbSave } from './WbSave'; -import { GET } from '../../utils/utils'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; -import { LoadingContext } from '../Core/Contexts'; -import { WbNoUploadPlan } from './WbNoUploadPlan'; -import { WbCellCounts } from '../WorkBench/CellMeta'; - -export function useWbActions({ - datasetId, - workbench, - checkDeletedFail, - onRefresh: handleRefresh, - onOpenStatus: handleOpenStatus, -}: { - readonly datasetId: number; - readonly workbench: Workbench; - readonly checkDeletedFail: (statusCode: number) => void; - readonly onRefresh: () => void; - readonly onOpenStatus: () => void; -}) { - const modeRef = React.useRef(undefined); - const refreshInitiatorAborted = React.useRef(false); - const loading = React.useContext(LoadingContext); - - const startUpload = (newMode: WbStatus): void => { - workbench.validation.stopLiveValidation(); - loading( - ping(`/api/workbench/${newMode}/${datasetId}/`, { - method: 'POST', - expectedErrors: [Http.CONFLICT], - }) - .then((statusCode): void => { - checkDeletedFail(statusCode); - checkConflictFail(statusCode); - }) - .then(() => triggerStatusComponent(newMode)) - ); - }; - - const triggerStatusComponent = (newMode: WbStatus): void => { - modeRef.current = newMode; - handleOpenStatus(); - }; - - const checkConflictFail = (statusCode: number): boolean => { - if (statusCode === Http.CONFLICT) - /* - * Upload/Validation/Un-Upload has been initialized by another session - * Need to reload the page to display the new state - */ - handleRefresh(); - return statusCode === Http.CONFLICT; - }; - - return { - modeRef, - refreshInitiatorAborted, - startUpload, - triggerStatusComponent, - }; -} - -function getMessage(cellCounts: WbCellCounts, mode: WbStatus) { - const messages = { - validate: - cellCounts.invalidCells === 0 - ? { - header: wbText.validationNoErrors(), - message: ( - <> - {wbText.validationNoErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - } - : { - header: wbText.validationErrors(), - message: ( - <> - {wbText.validationErrorsDescription()} -
-
- {wbText.validationReEditWarning()} - - ), - }, - upload: - cellCounts.invalidCells === 0 - ? { - header: wbText.uploadSuccessful(), - message: wbText.uploadSuccessfulDescription(), - } - : { - header: wbText.uploadErrors(), - message: ( - <> - {wbText.uploadErrorsDescription()} -
-
- {wbText.uploadErrorsSecondDescription()} - - ), - }, - unupload: { - header: wbText.dataSetRollback(), - message: wbText.dataSetRollbackDescription(), - }, - }; - - return messages[mode]; -} +import { WbUpload } from './WbUpload'; +import { WbValidate } from './WbValidate'; export function WbActions({ dataset, @@ -144,7 +35,7 @@ export function WbActions({ checkDeletedFail, onDatasetRefresh: handleRefresh, onSpreadsheetUpToDate: handleSpreadsheetUpToDate, - onToggleResults, + onToggleResults: handleToggleResults, }: { readonly dataset: Dataset; readonly hasUnsavedChanges: boolean; @@ -157,11 +48,6 @@ export function WbActions({ readonly onSpreadsheetUpToDate: () => void; readonly onToggleResults: () => void; }): JSX.Element { - const [canLiveValidate] = userPreferences.use( - 'workBench', - 'general', - 'liveValidation' - ); const [noUploadPlan, openNoUploadPlan, closeNoUploadPlan] = useBooleanState(); const [showStatus, openStatus, closeStatus] = useBooleanState(); const [operationAborted, openAbortedMessage, closeAbortedMessage] = @@ -185,9 +71,9 @@ export function WbActions({ return ( <> ) : undefined} - + + {commonText.results()} + {isUploaded && hasPermission('/workbench/dataset', 'unupload') ? ( @@ -220,11 +109,11 @@ export function WbActions({ {!isUploaded && hasPermission('/workbench/dataset', 'upload') ? ( ) : undefined} @@ -239,10 +128,10 @@ export function WbActions({ @@ -323,3 +212,110 @@ export function WbActions({ ); } + +function useWbActions({ + datasetId, + workbench, + checkDeletedFail, + onRefresh: handleRefresh, + onOpenStatus: handleOpenStatus, +}: { + readonly datasetId: number; + readonly workbench: Workbench; + readonly checkDeletedFail: (statusCode: number) => void; + readonly onRefresh: () => void; + readonly onOpenStatus: () => void; +}) { + const modeRef = React.useRef(undefined); + const refreshInitiatorAborted = React.useRef(false); + const loading = React.useContext(LoadingContext); + + const startUpload = (newMode: WbStatus): void => { + workbench.validation.stopLiveValidation(); + loading( + ping(`/api/workbench/${newMode}/${datasetId}/`, { + method: 'POST', + expectedErrors: [Http.CONFLICT], + }) + .then((statusCode): void => { + checkDeletedFail(statusCode); + checkConflictFail(statusCode); + }) + .then(() => triggerStatusComponent(newMode)) + ); + }; + + const triggerStatusComponent = (newMode: WbStatus): void => { + modeRef.current = newMode; + handleOpenStatus(); + }; + + const checkConflictFail = (statusCode: number): boolean => { + if (statusCode === Http.CONFLICT) + /* + * Upload/Validation/Un-Upload has been initialized by another session + * Need to reload the page to display the new state + */ + handleRefresh(); + return statusCode === Http.CONFLICT; + }; + + return { + modeRef, + refreshInitiatorAborted, + startUpload, + triggerStatusComponent, + }; +} + +function getMessage(cellCounts: WbCellCounts, mode: WbStatus) { + const messages = { + validate: + cellCounts.invalidCells === 0 + ? { + header: wbText.validationNoErrors(), + message: ( + <> + {wbText.validationNoErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + } + : { + header: wbText.validationErrors(), + message: ( + <> + {wbText.validationErrorsDescription()} +
+
+ {wbText.validationReEditWarning()} + + ), + }, + upload: + cellCounts.invalidCells === 0 + ? { + header: wbText.uploadSuccessful(), + message: wbText.uploadSuccessfulDescription(), + } + : { + header: wbText.uploadErrors(), + message: ( + <> + {wbText.uploadErrorsDescription()} +
+
+ {wbText.uploadErrorsSecondDescription()} + + ), + }, + unupload: { + header: wbText.dataSetRollback(), + message: wbText.dataSetRollbackDescription(), + }, + }; + + return messages[mode]; +} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts index 0213db32d38..646ef42ef0e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -1,9 +1,9 @@ +import type Handsontable from 'handsontable'; import React from 'react'; -import Handsontable from 'handsontable'; -import { Workbench } from '../WorkBench/WbView'; -import { getHotPlugin } from '../WorkBench/handsontable'; import { useBooleanState } from '../../hooks/useBooleanState'; +import { getHotPlugin } from '../WorkBench/handsontable'; +import type { Workbench } from '../WorkBench/WbView'; export function useResults({ hot, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx index d7fa8595c0e..453068aa112 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx @@ -1,24 +1,24 @@ import React from 'react'; import { useAsyncState } from '../../hooks/useAsyncState'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { useId } from '../../hooks/useId'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; import { formData } from '../../utils/ajax/helpers'; +import { ping } from '../../utils/ajax/ping'; import type { RA } from '../../utils/types'; +import { Button } from '../Atoms/Button'; import { Form, Label, Select } from '../Atoms/Form'; +import { Submit } from '../Atoms/Submit'; +import { LoadingContext } from '../Core/Contexts'; import { fetchCollection } from '../DataModel/collection'; import type { SerializedResource } from '../DataModel/helperTypes'; -import { userInformation } from '../InitialContext/userInformation'; import type { SpecifyUser } from '../DataModel/types'; -import { useId } from '../../hooks/useId'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { ping } from '../../utils/ajax/ping'; -import { Submit } from '../Atoms/Submit'; -import { LoadingContext } from '../Core/Contexts'; +import { userInformation } from '../InitialContext/userInformation'; import { Dialog } from '../Molecules/Dialog'; import { unsafeNavigate } from '../Router/Router'; -import { Button } from '../Atoms/Button'; -import { commonText } from '../../localization/common'; -import { wbText } from '../../localization/workbench'; +import type { Dataset } from '../WbPlanView/Wrapped'; export function WbChangeOwner({ hasUnsavedChanges, @@ -34,9 +34,9 @@ export function WbChangeOwner({ {wbText.changeOwner()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index ed238e89ebc..4fe52feff18 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -1,6 +1,7 @@ import type Handsontable from 'handsontable'; import React from 'react'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { useCachedState } from '../../hooks/useCachedState'; import { commonText } from '../../localization/common'; import { localityText } from '../../localization/locality'; @@ -12,13 +13,12 @@ import { Ul } from '../Atoms'; import { Button } from '../Atoms/Button'; import { Input, Label } from '../Atoms/Form'; import { Dialog } from '../Molecules/Dialog'; +import type { Dataset } from '../WbPlanView/Wrapped'; import { getSelectedCells, getSelectedLast, setHotData, } from '../WorkBench/hotHelpers'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import type { Dataset } from '../WbPlanView/Wrapped'; import type { WbMapping } from '../WorkBench/mapping'; export function WbConvertCoordinates({ @@ -41,9 +41,9 @@ export function WbConvertCoordinates({ {wbText.convertCoordinates()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx index 607e1182332..6c6a7b1f97f 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/DevShowPlan.tsx @@ -4,20 +4,20 @@ import React from 'react'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { wbPlanText } from '../../localization/wbPlan'; +import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; import { ping } from '../../utils/ajax/ping'; +import { overwriteReadOnly } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { LoadingContext } from '../Core/Contexts'; import { AutoGrowTextArea } from '../Molecules/AutoGrowTextArea'; import { Dialog } from '../Molecules/Dialog'; import { downloadFile } from '../Molecules/FilePicker'; import type { UploadPlan } from '../WbPlanView/uploadPlanParser'; -import { overwriteReadOnly } from '../../utils/types'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { wbText } from '../../localization/workbench'; export function WbRawPlan({ dataset, diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index 3ab978e90c4..9b3fbbbdc29 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -1,7 +1,10 @@ import type Handsontable from 'handsontable'; import React from 'react'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; +import { localityText } from '../../localization/locality'; +import { wbText } from '../../localization/workbench'; import { f } from '../../utils/functools'; import type { IR, RA } from '../../utils/types'; import { filterArray } from '../../utils/types'; @@ -13,16 +16,13 @@ import { } from '../Leaflet/wbLocalityDataExtractor'; import type { GeoLocatePayload } from '../Molecules/GeoLocate'; import { GenericGeoLocate } from '../Molecules/GeoLocate'; +import type { Dataset } from '../WbPlanView/Wrapped'; import { getSelectedRegions, getVisualHeaders, setHotData, } from '../WorkBench/hotHelpers'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import type { Dataset } from '../WbPlanView/Wrapped'; import type { WbMapping } from '../WorkBench/mapping'; -import { wbText } from '../../localization/workbench'; -import { localityText } from '../../localization/locality'; export function WbGeoLocate({ hasLocality, @@ -41,9 +41,9 @@ export function WbGeoLocate({ {localityText.geoLocate()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx index b1b81128347..f316a7c4c69 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbLeafletMap.tsx @@ -1,16 +1,16 @@ +import type Handsontable from 'handsontable'; import React from 'react'; -import Handsontable from 'handsontable'; -import { getSelectedLocalities } from './GeoLocate'; -import { getLocalitiesDataFromSpreadsheet } from '../Leaflet/wbLocalityDataExtractor'; -import { getSelectedLast, getVisualHeaders } from '../WorkBench/hotHelpers'; -import { LeafletMap } from '../Leaflet/Map'; import { useBooleanState } from '../../hooks/useBooleanState'; -import type { Dataset } from '../WbPlanView/Wrapped'; -import type { WbMapping } from '../WorkBench/mapping'; -import { Button } from '../Atoms/Button'; import { localityText } from '../../localization/locality'; import { wbText } from '../../localization/workbench'; +import { Button } from '../Atoms/Button'; +import { LeafletMap } from '../Leaflet/Map'; +import { getLocalitiesDataFromSpreadsheet } from '../Leaflet/wbLocalityDataExtractor'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import { getSelectedLast, getVisualHeaders } from '../WorkBench/hotHelpers'; +import type { WbMapping } from '../WorkBench/mapping'; +import { getSelectedLocalities } from './GeoLocate'; export function WbLeafletMap({ hasLocality, @@ -48,9 +48,9 @@ export function WbLeafletMap({ {localityText.geoMap()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx index 97e0c8dc628..040c819cda5 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx @@ -1,22 +1,22 @@ +import type Handsontable from 'handsontable'; import React from 'react'; -import Handsontable from 'handsontable'; -import { hasPermission, hasTablePermission } from '../Permissions/helpers'; -import { WbChangeOwner } from './ChangeOwner'; -import { WbConvertCoordinates } from './CoordinateConverter'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import type { RA } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { raise } from '../Errors/Crash'; +import { ErrorBoundary } from '../Errors/ErrorBoundary'; +import { hasPermission, hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; import { downloadDataSet } from '../WorkBench/helpers'; import type { WbMapping } from '../WorkBench/mapping'; +import { WbChangeOwner } from './ChangeOwner'; +import { WbConvertCoordinates } from './CoordinateConverter'; import { WbRawPlan } from './DevShowPlan'; import { WbGeoLocate } from './GeoLocate'; import { WbLeafletMap } from './WbLeafletMap'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbToolkit({ dataset, @@ -51,7 +51,7 @@ export function WbToolkit({ }; const hasLocality = - mappings !== undefined ? mappings.localityColumns.length > 0 : false; + mappings === undefined ? false : mappings.localityColumns.length > 0; return (
{hasPermission('/workbench/dataset', 'transfer') && hasTablePermission('SpecifyUser', 'read') ? ( - <> - + - ) : undefined} {commonText.export()} @@ -89,18 +87,18 @@ export function WbToolkit({ <> @@ -108,9 +106,9 @@ export function WbToolkit({ )} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx deleted file mode 100644 index 20fff9c1fff..00000000000 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Components.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; - -import { commonText } from '../../localization/common'; -import { wbText } from '../../localization/workbench'; -import { ping } from '../../utils/ajax/ping'; -import { Button } from '../Atoms/Button'; -import { LoadingContext } from '../Core/Contexts'; -import { Dialog, dialogClassNames } from '../Molecules/Dialog'; - -export function RollbackConfirmation({ - datasetId, - onClose: handleClose, - onRollback: handleRollback, -}: { - readonly datasetId: number; - readonly onClose: () => void; - readonly onRollback: () => void; -}): JSX.Element { - const loading = React.useContext(LoadingContext); - return ( - - {commonText.cancel()} - - loading( - ping(`/api/workbench/unupload/${datasetId}/`, { - method: 'POST', - }) - .then(handleRollback) - .finally(handleClose) - ) - } - > - {wbText.rollback()} - - - } - className={{ - container: dialogClassNames.narrowContainer, - }} - header={wbText.beginRollback()} - onClose={handleClose} - > - {wbText.beginRollbackDescription()} - - ); -} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx index 55d4138b819..d9be877f831 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx @@ -1,6 +1,6 @@ +import type Handsontable from 'handsontable'; import React from 'react'; import type { LocalizedString } from 'typesafe-i18n'; -import Handsontable from 'handsontable'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useId } from '../../hooks/useId'; @@ -362,7 +362,6 @@ export function DataSetName({ return (
- {' '}

{dataset.uploadplan !== null && ( diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx index b1832cb48bf..1a28adf1767 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx @@ -4,6 +4,7 @@ import { useAsyncState } from '../../hooks/useAsyncState'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import type { RA } from '../../utils/types'; +import type { WritableArray } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { Input, Label } from '../Atoms/Form'; import type { AnySchema } from '../DataModel/helperTypes'; @@ -13,7 +14,6 @@ import { isTreeTable } from '../InitialContext/treeRanks'; import { Dialog } from '../Molecules/Dialog'; import { FormattedResource } from '../Molecules/FormattedResource'; import { hasTablePermission } from '../Permissions/helpers'; -import { WritableArray } from '../../utils/types'; export function DisambiguationDialog({ matches, @@ -53,9 +53,9 @@ export function DisambiguationDialog({ selected === undefined || liveValidationStack?.length !== 0 } title={ - liveValidationStack?.length !== 0 - ? wbText.applyAllUnavailable() - : undefined + liveValidationStack?.length === 0 + ? undefined + : wbText.applyAllUnavailable() } onClick={(): void => { handleSelectedAll(selected!); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx index 26128e35fb7..58c5405880a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx @@ -1,14 +1,14 @@ import React from 'react'; +import type { LocalizedString } from 'typesafe-i18n'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { icons } from '../Atoms/Icons'; -import { LocalizedString } from 'typesafe-i18n'; +import { wbText } from '../../localization/workbench'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; -import { WbCellCounts } from './CellMeta'; -import { WbUtils } from './WbUtils'; +import { icons } from '../Atoms/Icons'; import { ReadOnlyContext } from '../Core/Contexts'; -import { wbText } from '../../localization/workbench'; +import type { WbCellCounts } from './CellMeta'; +import type { WbUtils } from './WbUtils'; export function Navigation({ name, @@ -55,20 +55,20 @@ export function Navigation({ {icons.chevronLeft} {icons.chevronRight} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx index 373b80cdaf1..ea86c70b285 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Results.tsx @@ -15,9 +15,9 @@ import { Button } from '../Atoms/Button'; import { formatNumber } from '../Atoms/Internationalization'; import { strictGetTable } from '../DataModel/tables'; import type { Tables } from '../DataModel/types'; +import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { TableIcon } from '../Molecules/TableIcon'; import { CreateRecordSetButton } from './RecordSet'; -import { ErrorBoundary } from '../Errors/ErrorBoundary'; export function WbUploaded({ recordCounts, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 54d29602cb8..686cb3ff186 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -7,28 +7,28 @@ */ import { HotTable } from '@handsontable/react'; -import Handsontable from 'handsontable'; -import React from 'react'; +import type Handsontable from 'handsontable'; import type { DetailedSettings } from 'handsontable/plugins/contextMenu'; +import { registerAllModules } from 'handsontable/registry'; +import React from 'react'; +import { commonText } from '../../localization/common'; import { LANGUAGE } from '../../localization/utils/config'; +import { wbText } from '../../localization/workbench'; import { type RA } from '../../utils/types'; import { legacyNonJsxIcons } from '../Atoms/Icons'; +import { iconClassName } from '../Atoms/Icons'; +import { ReadOnlyContext } from '../Core/Contexts'; +import { strictGetTable } from '../DataModel/tables'; +import { getIcon, unknownIcon } from '../InitialContext/icons'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { WbMapping } from './mapping'; -import { getSelectedRegions } from './hotHelpers'; import { configureHandsontable } from './handsontable'; -import { fetchWbPickLists } from './pickLists'; import { getHotHooks } from './hooks'; -import { wbText } from '../../localization/workbench'; -import { strictGetTable } from '../DataModel/tables'; -import { getIcon, unknownIcon } from '../InitialContext/icons'; -import { commonText } from '../../localization/common'; -import { iconClassName } from '../Atoms/Icons'; -import { registerAllModules } from 'handsontable/registry'; -import { Workbench } from './WbView'; -import { ReadOnlyContext } from '../Core/Contexts'; +import { getSelectedRegions } from './hotHelpers'; import { getHotProps } from './hotProps'; +import type { WbMapping } from './mapping'; +import { fetchWbPickLists } from './pickLists'; +import type { Workbench } from './WbView'; registerAllModules(); @@ -37,8 +37,7 @@ const fillCellsContextMenuItem = ( hot: Handsontable, mode: 'down' | 'up', isReadOnly: boolean -): Handsontable.plugins.ContextMenu.MenuItemConfig => { - return { +): Handsontable.plugins.ContextMenu.MenuItemConfig => ({ name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), disabled: () => isReadOnly || @@ -66,8 +65,7 @@ const fillCellsContextMenuItem = ( ); }) ), - }; -}; + }); function WbSpreadsheetComponent({ dataset, @@ -88,7 +86,7 @@ function WbSpreadsheetComponent({ readonly data: RA>; readonly workbench: Workbench; readonly mappings: WbMapping | undefined; - readonly checkDeletedFail: (_: number) => boolean; + readonly checkDeletedFail: (statusCode: number) => boolean; readonly spreadsheetChanged: () => void; readonly onClickDisambiguate: () => void; }): JSX.Element { @@ -180,7 +178,7 @@ function WbSpreadsheetComponent({ disabled: () => { if (isReadOnly) return true; // Or if called on the last row - const selectedRegions = getSelectedRegions(hot!); + const selectedRegions = getSelectedRegions(hot); return ( selectedRegions.length === 1 && selectedRegions[0].startRow === data.length - 1 && @@ -199,10 +197,10 @@ function WbSpreadsheetComponent({ fill_up: fillCellsContextMenuItem(hot, 'up', isReadOnly), ['separator_2' as 'redo']: '---------', undo: { - disabled: () => hot.isUndoAvailable() !== true || isReadOnly, + disabled: () => !hot.isUndoAvailable() || isReadOnly, }, redo: { - disabled: () => hot.isRedoAvailable() !== true || isReadOnly, + disabled: () => !hot.isRedoAvailable() || isReadOnly, }, } as const), }; @@ -258,6 +256,7 @@ function WbSpreadsheetComponent({ columns={columns} commentedCellClassName="htCommentCell" comments={comments} + contextMenu={contextMenuConfig} data={data as (string | null)[][]} enterBeginsEditing={enterBeginsEditing} enterMoves={enterMoves} @@ -266,20 +265,17 @@ function WbSpreadsheetComponent({ invalidCellClassName="-" language={LANGUAGE} licenseKey="non-commercial-and-evaluation" - manualColumnMove={true} - manualColumnResize={true} + manualColumnMove + manualColumnResize minSpareRows={minSpareRows} - multiColumnSorting={true} + multiColumnSorting outsideClickDeselects={false} placeholderCellClassName="htPlaceholder" + readOnly={isReadOnly} ref={setHotTable} - rowHeaders={true} - // @ts-expect-error typing error, possibly fixed in v14 - sortIndicator={true} + rowHeaders stretchH="all" tabMoves={tabMoves} - readOnly={isReadOnly} - contextMenu={contextMenuConfig} {...hooks} /> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx index 6d5f74f61bc..4091c7652e7 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx @@ -9,10 +9,13 @@ import React from 'react'; import _ from 'underscore'; +import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { f } from '../../utils/functools'; import type { RA, WritableArray } from '../../utils/types'; import { camelToKebab } from '../../utils/utils'; +import { Input } from '../Atoms/Form'; +import { ReadOnlyContext } from '../Core/Contexts'; import { hasPermission } from '../Permissions/helpers'; import type { WbSearchPreferences } from './AdvancedSearch'; import { @@ -22,11 +25,8 @@ import { import type { WbCellCounts, WbCellMeta } from './CellMeta'; import { getHotPlugin } from './handsontable'; import { getSelectedLast } from './hotHelpers'; -import type { Workbench } from './WbView'; -import { Input } from '../Atoms/Form'; -import { commonText } from '../../localization/common'; -import { ReadOnlyContext } from '../Core/Contexts'; import { Navigation } from './Navigation'; +import type { Workbench } from './WbView'; export function WbUtilsComponent({ isUploaded, @@ -54,9 +54,9 @@ export function WbUtilsComponent({
utils.replaceCells(event, replaceRef.current!) } - disabled={isReadOnly} />
) : undefined} @@ -95,11 +95,13 @@ export function WbUtilsComponent({ cells.indexedCellMeta = undefined; } utils.searchPreferences = newSearchPreferences; - // TODO: figure out what searchCells with SettingsChange does - // if (utils.searchPreferences.search.liveUpdate) - // utils.searchCells({ - // key: 'SettingsChange', - // }).catch(softFail); + /* + * TODO: figure out what searchCells with SettingsChange does + * if (utils.searchPreferences.search.liveUpdate) + * utils.searchCells({ + * key: 'SettingsChange', + * }).catch(softFail); + */ }} />
@@ -156,27 +158,27 @@ export class WbUtils { matchCurrentCell = false, currentCell = undefined, }: { - type: keyof WbCellCounts; - direction: 'previous' | 'next'; - currentCellPosition?: number; - totalCount?: number; + readonly type: keyof WbCellCounts; + readonly direction: 'next' | 'previous'; + readonly currentCellPosition?: number; + readonly totalCount?: number; // If true and current cell is of correct type, don't navigate away - matchCurrentCell?: boolean; + readonly matchCurrentCell?: boolean; /* * Overwrite what is considered to be a current cell * Setting to [0,0] and matchCurrentCell=true allows navigation to the first * cell of type (used on hitting "Enter" in the Search Box) */ - currentCell?: readonly [number, number] | undefined; + readonly currentCell?: readonly [number, number] | undefined; }): - | [ + | readonly [ { readonly visualRow: number; readonly visualCol: number; }, number ] - | [undefined, number] { + | readonly [undefined, number] { const cellMetaObject = this.workbench.cells.getCellMetaObject(); /* * The cellMetaObject is transposed if navigation direction is "Column @@ -365,7 +367,7 @@ export class WbUtils { render = Boolean(cell); } - this.workbench.cells![render ? 'updateCellMeta' : 'setCellMeta']( + this.workbench.cells[render ? 'updateCellMeta' : 'setCellMeta']( physicalRow, physicalCol, 'isSearchResult', @@ -493,11 +495,11 @@ export class WbUtils { if (this.searchPreferences.replace.replaceMode === 'replaceAll') { // eslint-disable-next-line functional/prefer-readonly-type const modifications: WritableArray<[number, number, string]> = []; - Object.entries(this.workbench.cells!.cellMeta).forEach( + Object.entries(this.workbench.cells.cellMeta).forEach( ([physicalRow, metaRow]) => Object.entries(metaRow).forEach(([physicalCol, metaArray]) => { if ( - !this.workbench.cells!.getCellMetaFromArray( + !this.workbench.cells.getCellMetaFromArray( metaArray, 'isSearchResult' ) @@ -533,8 +535,8 @@ export class WbUtils { const physicalCol = this.workbench.hot.toPhysicalColumn(currentCol); let nextCell = [currentRow, currentCol] as const; if ( - !this.workbench.cells!.cellIsType( - this.workbench.cells!.cellMeta[physicalRow]?.[physicalCol], + !this.workbench.cells.cellIsType( + this.workbench.cells.cellMeta[physicalRow]?.[physicalCol], 'searchResults' ) ) { diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index 1f2d795befd..648ee577d48 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -19,10 +19,10 @@ import type { Workbench } from './WbView'; type UploadResults = { readonly ambiguousMatches: WritableArray< WritableArray<{ - physicalCols: RA; - mappingPath: MappingPath; - ids: RA; - key: string; + readonly physicalCols: RA; + readonly mappingPath: MappingPath; + readonly ids: RA; + readonly key: string; }> >; readonly recordCounts: Partial, number>>; @@ -80,7 +80,7 @@ export class WbValidation { recordCounts: {}, newRecords: [], }; - this.workbench.cells!.cellMeta = []; + this.workbench.cells.cellMeta = []; switch (this.validationMode) { case 'live': { @@ -364,7 +364,7 @@ export class WbValidation { } getValidationResults(): void { - if (!this.workbench.mappings) return; + if (this.workbench.mappings === undefined) return; if (this.workbench.dataset.rowresults === null) { this.validationMode = 'off'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 2ef4da83758..86a1a35c3fd 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -12,10 +12,11 @@ import '../../../css/workbench.css'; +import type { HotTable } from '@handsontable/react'; +import type Handsontable from 'handsontable'; import React from 'react'; -import Handsontable from 'handsontable'; -import { HotTable } from '@handsontable/react'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { wbPlanText } from '../../localization/wbPlan'; import { Http } from '../../utils/ajax/definitions'; @@ -23,40 +24,40 @@ import type { GetSet, RA } from '../../utils/types'; import { clamp } from '../../utils/utils'; import { Button } from '../Atoms/Button'; import { Link } from '../Atoms/Link'; +import { ReadOnlyContext } from '../Core/Contexts'; import { hasPermission } from '../Permissions/helpers'; +import { WbActions } from '../WbActions'; +import { useResults } from '../WbActions/useResults'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { WbCellCounts, WbCellMeta } from './CellMeta'; +import { WbToolkit } from '../WbToolkit/WbToolkit'; +import type { WbCellCounts} from './CellMeta'; +import { WbCellMeta } from './CellMeta'; +import { DataSetName } from './DataSetMeta'; import { Disambiguation } from './DisambiguationLogic'; import type { WbMapping } from './mapping'; import { parseWbMappings } from './mapping'; import { WbUploaded } from './Results'; -import { WbActions } from '../WbActions/WbActions'; +import { useDisambiguationDialog } from './useDisambiguationDialog'; +import { WbSpreadsheet } from './WbSpreadsheet'; import { WbUtils, WbUtilsComponent } from './WbUtils'; import { WbValidation } from './WbValidation'; -import { DataSetName } from './DataSetMeta'; -import { WbSpreadsheet } from './WbSpreadsheet'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { WbToolkit } from '../WbToolkit/WbToolkit'; -import { ReadOnlyContext } from '../Core/Contexts'; -import { useResults } from '../WbActions/useResults'; -import { useDisambiguationDialog } from './useDisambiguationDialog'; export type WbStatus = 'unupload' | 'upload' | 'validate'; export type Workbench = { - dataset: Dataset; + readonly dataset: Dataset; cells: WbCellMeta; disambiguation: Disambiguation; validation: WbValidation; - data: RA>; - hot: Handsontable | undefined; - throttleRate: number; - mappings: WbMapping; + readonly data: RA>; + readonly hot: Handsontable | undefined; + readonly throttleRate: number; + readonly mappings: WbMapping | undefined; utils: WbUtils; - cellCounts: GetSet; - spreadsheetContainerRef: React.RefObject; + readonly cellCounts: GetSet; + readonly spreadsheetContainerRef: React.RefObject; undoRedoIsHandled: boolean; - spreadsheetChanged: () => void; + readonly spreadsheetChanged: () => void; }; export function WbView({ @@ -81,10 +82,8 @@ export function WbView({ const [hotTable, setHotTable] = React.useState(null); const hot = hotTable?.hotInstance ?? undefined; - const isUploaded = React.useMemo( - () => dataset.uploadresult !== null && dataset.uploadresult.success, - [dataset] - ); + const isUploaded = dataset.uploadresult !== null && dataset.uploadresult.success; + const isAlreadyReadOnly = React.useContext(ReadOnlyContext); const mappings = React.useMemo( (): WbMapping | undefined => parseWbMappings(dataset), @@ -107,8 +106,8 @@ export function WbView({ const workbench: Workbench = { data, dataset, - hot: hot, - mappings: mappings!, + hot, + mappings, throttleRate, cells: undefined!, disambiguation: undefined!, @@ -131,7 +130,7 @@ export function WbView({ return statusCode === Http.NOT_FOUND; }, []); - const isMapped = Boolean(dataset.uploadplan); + const isMapped = mappings !== undefined; const canUpdate = hasPermission('/workbench/dataset', 'update'); const [showToolkit, _openToolkit, _closeToolkit, toggleToolkit] = @@ -152,7 +151,7 @@ export function WbView({ }); return ( - +
) : undefined}
{showToolkit && typeof hot === 'object' ? ( ) : undefined}
{showResults ? ( @@ -222,11 +221,11 @@ export function WbView({
{disambiguationDialogs}
); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index fb084206bee..6e0a357ac04 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -13,7 +13,7 @@ import { overwriteReadOnly } from '../../utils/types'; import { sortFunction } from '../../utils/utils'; import { schema } from '../DataModel/schema'; import { getHotPlugin } from './handsontable'; -import { Workbench } from './WbView'; +import type { Workbench } from './WbView'; export function getHotHooks( workbench: Workbench, @@ -45,7 +45,7 @@ export function getHotHooks( : workbench.hot.toPhysicalColumn(visualCol); if (physicalCol >= workbench.dataset.columns.length) return; const metaArray = workbench.cells.cellMeta?.[physicalRow]?.[physicalCol]; - if (workbench.cells.getCellMetaFromArray(metaArray!, 'isModified')) + if (workbench.cells.getCellMetaFromArray(metaArray, 'isModified')) workbench.cells.runMetaUpdateEffects( td, 'isModified', @@ -53,7 +53,7 @@ export function getHotHooks( visualRow, visualCol ); - if (workbench.cells.getCellMetaFromArray(metaArray!, 'isNew')) + if (workbench.cells.getCellMetaFromArray(metaArray, 'isNew')) workbench.cells.runMetaUpdateEffects( td, 'isNew', @@ -61,7 +61,7 @@ export function getHotHooks( visualRow, visualCol ); - if (workbench.cells.getCellMetaFromArray(metaArray!, 'isSearchResult')) + if (workbench.cells.getCellMetaFromArray(metaArray, 'isSearchResult')) workbench.cells.runMetaUpdateEffects( td, 'isSearchResult', @@ -82,7 +82,7 @@ export function getHotHooks( const visualCol = workbench.hot.propToCol(property); const physicalCol = workbench.hot.toPhysicalColumn(visualCol); - return workbench.mappings.defaultValues[physicalCol] ?? value; + return workbench.mappings?.defaultValues[physicalCol] ?? value; }, afterValidate: ( @@ -117,7 +117,7 @@ export function getHotHooks( }) ), ]), - ...issues!.filter( + ...issues.filter( (issue) => !issue.endsWith( whitespaceSensitive( @@ -315,7 +315,7 @@ export function getHotHooks( workbench.cells.indexedCellMeta = undefined; addedRows - .filter((physicalRow) => physicalRow < workbench.cells!.cellMeta.length) + .filter((physicalRow) => physicalRow < workbench.cells.cellMeta.length) .forEach((physicalRow) => workbench.cells?.cellMeta.splice(physicalRow, 0, []) ); @@ -330,7 +330,7 @@ export function getHotHooks( const removedRows = Array.from({ length: amount }, (_, index) => workbench.hot!.toPhysicalRow(visualRowStart + index) ) - .filter((physicalRow) => physicalRow < workbench.cells!.cellMeta.length) + .filter((physicalRow) => physicalRow < workbench.cells.cellMeta.length) // REFACTOR: use sortFunction here .sort() .reverse(); @@ -358,8 +358,6 @@ export function getHotHooks( beforeColumnSort: (currentSortConfig, newSortConfig) => { workbench.cells.indexedCellMeta = undefined; - // if (wbView.coordinateConverterView) return false; - if ( workbench.mappings === undefined || sortConfigIsSet || diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx index 2d4f8dca6a2..a7ac07e1ec8 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx @@ -1,13 +1,13 @@ import React from 'react'; import ReactDOMServer from 'react-dom/server'; -import { userPreferences } from '../Preferences/userPreferences'; -import { Dataset } from '../WbPlanView/Wrapped'; -import { WbMapping } from './mapping'; -import { f } from '../../utils/functools'; -import { getTable } from '../DataModel/tables'; import { wbPlanText } from '../../localization/wbPlan'; +import { f } from '../../utils/functools'; import { icons } from '../Atoms/Icons'; +import { getTable } from '../DataModel/tables'; +import { userPreferences } from '../Preferences/userPreferences'; +import type { Dataset } from '../WbPlanView/Wrapped'; +import type { WbMapping } from './mapping'; const comments = { displayDelay: 100 }; @@ -33,14 +33,14 @@ function ColumnHeader({
{isMapped ? ( {tableLabel} ) : ( {icons.ban} @@ -97,10 +97,10 @@ export function getHotProps({ // REFACTOR: use new table icons return ReactDOMServer.renderToString( ); }, @@ -113,15 +113,13 @@ export function getHotProps({ 'enterBeginsEditing' ); - const hiddenColumns = React.useMemo(() => { - return { + const hiddenColumns = React.useMemo(() => ({ // Hide the disambiguation column columns: [dataset.columns.length], indicators: false, // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 copyPasteEnabled: false, - }; - }, []); + }), []); const minSpareRows = userPreferences.get( 'workBench', diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 82748fe02b1..92225d3e110 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -11,35 +11,14 @@ import { f } from '../../utils/functools'; import type { GetSet } from '../../utils/types'; import { className } from '../Atoms/className'; import { LoadingContext } from '../Core/Contexts'; -import { LoadingScreen } from '../Molecules/Dialog'; import { useMenuItem } from '../Header/MenuContext'; import { treeRanksPromise } from '../InitialContext/treeRanks'; +import { LoadingScreen } from '../Molecules/Dialog'; import { Dialog } from '../Molecules/Dialog'; import { NotFoundView } from '../Router/NotFoundView'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbView } from './WbView'; -const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); - -const fetchDataSet = async ( - datasetId: number | undefined -): Promise => - typeof datasetId === 'number' - ? ajax(`/api/workbench/dataset/${datasetId}/`, { - headers: { Accept: 'application/json' }, - }).then(({ data }) => data) - : undefined; - -// BUG: intercept 403 (if dataset has been transferred to another user) -function useDataSet( - datasetId: number | undefined -): GetSet { - return useAsyncState( - React.useCallback(async () => fetchDataSet(datasetId), [datasetId]), - true - ); -} - export function WorkBench(): JSX.Element { useMenuItem('workBench'); @@ -51,17 +30,16 @@ export function WorkBench(): JSX.Element { useErrorContext('dataSet', dataset); const loading = React.useContext(LoadingContext); const [isDeleted, handleDeleted] = useBooleanState(); - // TODO: figure out how handleDeletedConfirmation was being used in Backbone. possibly not used at all - // @ts-ignore + // @ts-ignore figure out how handleDeletedConfirmation was being used in Backbone. possibly not used at all const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); const navigate = useNavigate(); const spreadsheetContainerRef = React.useRef(null); - if (!dataset || !treeRanksLoaded) return ; + if (dataset === undefined || !treeRanksLoaded) return ; const triggerDatasetRefresh = () => { - loading(fetchDataSet(dataset!.id).then(setDataSet)); + loading(fetchDataSet(dataset.id).then(setDataSet)); }; return datasetId === undefined ? ( @@ -84,11 +62,33 @@ export function WorkBench(): JSX.Element { >
); } + +const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); + +// BUG: intercept 403 (if dataset has been transferred to another user) +function useDataSet( + datasetId: number | undefined +): GetSet { + return useAsyncState( + React.useCallback(async () => fetchDataSet(datasetId), [datasetId]), + true + ); +} + +const fetchDataSet = async ( + datasetId: number | undefined +): Promise => + typeof datasetId === 'number' + ? ajax(`/api/workbench/dataset/${datasetId}/`, { + headers: { Accept: 'application/json' }, + }).then(({ data }) => data) + : undefined; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx index d0a0427ae9a..a52f3089179 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx @@ -1,24 +1,24 @@ +import type Handsontable from 'handsontable'; import React from 'react'; -import Handsontable from 'handsontable'; +import { useBooleanState } from '../../hooks/useBooleanState'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; import { type RA } from '../../utils/types'; -import { MappingPath } from '../WbPlanView/Mapper'; +import { LoadingContext } from '../Core/Contexts'; +import type { AnySchema } from '../DataModel/helperTypes'; import type { Collection } from '../DataModel/specifyTable'; -import { AnySchema } from '../DataModel/helperTypes'; -import { useBooleanState } from '../../hooks/useBooleanState'; -import { getSelectedLast } from './hotHelpers'; -import { getTableFromMappingPath } from '../WbPlanView/navigator'; -import { hasTablePermission } from '../Permissions/helpers'; import { strictGetTable } from '../DataModel/tables'; -import { WbMapping } from './mapping'; -import { WbValidation } from './WbValidation'; import { Dialog } from '../Molecules/Dialog'; -import { wbText } from '../../localization/workbench'; -import { commonText } from '../../localization/common'; -import { DisambiguationDialog } from './Disambiguation'; -import { Disambiguation } from './DisambiguationLogic'; +import { hasTablePermission } from '../Permissions/helpers'; +import type { MappingPath } from '../WbPlanView/Mapper'; import { mappingPathToString } from '../WbPlanView/mappingHelpers'; -import { LoadingContext } from '../Core/Contexts'; +import { getTableFromMappingPath } from '../WbPlanView/navigator'; +import { DisambiguationDialog } from './Disambiguation'; +import type { Disambiguation } from './DisambiguationLogic'; +import { getSelectedLast } from './hotHelpers'; +import type { WbMapping } from './mapping'; +import type { WbValidation } from './WbValidation'; type DisambiguationMatches = { readonly physicalCols: RA; @@ -106,8 +106,8 @@ export function useDisambiguationDialog({ )} {disambiguationDialog && ( { disambiguation.setDisambiguation( From 59b787789b0f2dceee5a51a606742531c0b644a7 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Thu, 18 Apr 2024 10:34:37 -0400 Subject: [PATCH 075/153] Rename WbToolkit to index.tsx --- .../lib/components/WbToolkit/{WbToolkit.tsx => index.tsx} | 0 specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename specifyweb/frontend/js_src/lib/components/WbToolkit/{WbToolkit.tsx => index.tsx} (100%) diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WbToolkit/WbToolkit.tsx rename to specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 86a1a35c3fd..e78c8c47020 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -29,7 +29,7 @@ import { hasPermission } from '../Permissions/helpers'; import { WbActions } from '../WbActions'; import { useResults } from '../WbActions/useResults'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { WbToolkit } from '../WbToolkit/WbToolkit'; +import { WbToolkit } from '../WbToolkit'; import type { WbCellCounts} from './CellMeta'; import { WbCellMeta } from './CellMeta'; import { DataSetName } from './DataSetMeta'; From 1656f3eee9a016ee610d8df1a19ea0807eec9937 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Thu, 18 Apr 2024 11:14:36 -0400 Subject: [PATCH 076/153] Move utils components and logic to WbUtils --- .../{WorkBench => WbUtils}/Navigation.tsx | 4 +- .../WbUtils.tsx => WbUtils/Utils.ts} | 130 ++---------------- .../js_src/lib/components/WbUtils/index.tsx | 122 ++++++++++++++++ ...uationLogic.tsx => DisambiguationLogic.ts} | 0 .../lib/components/WorkBench/WbView.tsx | 3 +- 5 files changed, 134 insertions(+), 125 deletions(-) rename specifyweb/frontend/js_src/lib/components/{WorkBench => WbUtils}/Navigation.tsx (96%) rename specifyweb/frontend/js_src/lib/components/{WorkBench/WbUtils.tsx => WbUtils/Utils.ts} (78%) create mode 100644 specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx rename specifyweb/frontend/js_src/lib/components/WorkBench/{DisambiguationLogic.tsx => DisambiguationLogic.ts} (100%) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx similarity index 96% rename from specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx rename to specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx index 58c5405880a..999cc74156b 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Navigation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx @@ -7,8 +7,8 @@ import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; import { icons } from '../Atoms/Icons'; import { ReadOnlyContext } from '../Core/Contexts'; -import type { WbCellCounts } from './CellMeta'; -import type { WbUtils } from './WbUtils'; +import type { WbCellCounts } from '../WorkBench/CellMeta'; +import type { WbUtils } from './Utils'; export function Navigation({ name, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts similarity index 78% rename from specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx rename to specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index 4091c7652e7..60a9cd1e5af 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbUtils.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -9,133 +9,19 @@ import React from 'react'; import _ from 'underscore'; -import { commonText } from '../../localization/common'; -import { wbText } from '../../localization/workbench'; + import { f } from '../../utils/functools'; import type { RA, WritableArray } from '../../utils/types'; import { camelToKebab } from '../../utils/utils'; -import { Input } from '../Atoms/Form'; -import { ReadOnlyContext } from '../Core/Contexts'; -import { hasPermission } from '../Permissions/helpers'; -import type { WbSearchPreferences } from './AdvancedSearch'; + +import type { WbSearchPreferences } from '../WorkBench/AdvancedSearch'; import { getInitialSearchPreferences, - WbAdvancedSearch, -} from './AdvancedSearch'; -import type { WbCellCounts, WbCellMeta } from './CellMeta'; -import { getHotPlugin } from './handsontable'; -import { getSelectedLast } from './hotHelpers'; -import { Navigation } from './Navigation'; -import type { Workbench } from './WbView'; - -export function WbUtilsComponent({ - isUploaded, - cellCounts, - utils, - cells, - debounceRate, -}: { - readonly isUploaded: boolean; - readonly cellCounts: WbCellCounts; - readonly utils: WbUtils; - readonly cells: WbCellMeta; - readonly debounceRate: number; -}): JSX.Element { - const isReadOnly = React.useContext(ReadOnlyContext); - const searchRef = React.useRef(null); - const replaceRef = React.useRef(null); - - return ( -
- -
- ) => - utils.searchCells(event, searchRef.current!), - debounceRate, - true - )} - /> -
- {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( -
- - utils.replaceCells(event, replaceRef.current!) - } - /> -
- ) : undefined} - { - if ( - newSearchPreferences.navigation.direction !== - utils.searchPreferences.navigation.direction - ) { - cells.indexedCellMeta = undefined; - } - utils.searchPreferences = newSearchPreferences; - /* - * TODO: figure out what searchCells with SettingsChange does - * if (utils.searchPreferences.search.liveUpdate) - * utils.searchCells({ - * key: 'SettingsChange', - * }).catch(softFail); - */ - }} - /> -
- - {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( - - ) : undefined} - - {!isUploaded && ( - - )} -
- ); -} +} from '../WorkBench/AdvancedSearch'; +import type { WbCellCounts } from '../WorkBench/CellMeta'; +import { getHotPlugin } from '../WorkBench/handsontable'; +import { getSelectedLast } from '../WorkBench/hotHelpers'; +import type { Workbench } from '../WorkBench/WbView'; /* eslint-disable functional/no-this-expression */ export class WbUtils { diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx new file mode 100644 index 00000000000..559730aa97a --- /dev/null +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -0,0 +1,122 @@ +import React from 'react'; + +import { Input } from '../Atoms/Form'; +import { ReadOnlyContext } from '../Core/Contexts'; +import { hasPermission } from '../Permissions/helpers'; +import { commonText } from '../../localization/common'; +import { wbText } from '../../localization/workbench'; +import { Navigation } from './Navigation'; +import { + WbAdvancedSearch, +} from '../WorkBench/AdvancedSearch'; +import type { WbCellCounts, WbCellMeta } from '../WorkBench/CellMeta'; +import { WbUtils } from './Utils'; + +export function WbUtilsComponent({ + isUploaded, + cellCounts, + utils, + cells, + debounceRate, + }: { + readonly isUploaded: boolean; + readonly cellCounts: WbCellCounts; + readonly utils: WbUtils; + readonly cells: WbCellMeta; + readonly debounceRate: number; + }): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); + const searchRef = React.useRef(null); + const replaceRef = React.useRef(null); + + return ( +
+ +
+ ) => + utils.searchCells(event, searchRef.current!), + debounceRate, + true + )} + /> +
+ {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( +
+ + utils.replaceCells(event, replaceRef.current!) + } + /> +
+ ) : undefined} + { + if ( + newSearchPreferences.navigation.direction !== + utils.searchPreferences.navigation.direction + ) { + cells.indexedCellMeta = undefined; + } + utils.searchPreferences = newSearchPreferences; + /* + * TODO: figure out what searchCells with SettingsChange does + * if (utils.searchPreferences.search.liveUpdate) + * utils.searchCells({ + * key: 'SettingsChange', + * }).catch(softFail); + */ + }} + /> +
+ + {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( + + ) : undefined} + + {!isUploaded && ( + + )} +
+ ); + } \ No newline at end of file diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.ts similarity index 100% rename from specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.tsx rename to specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.ts diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index e78c8c47020..0f7e3fb01c8 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -39,7 +39,8 @@ import { parseWbMappings } from './mapping'; import { WbUploaded } from './Results'; import { useDisambiguationDialog } from './useDisambiguationDialog'; import { WbSpreadsheet } from './WbSpreadsheet'; -import { WbUtils, WbUtilsComponent } from './WbUtils'; +import { WbUtils } from '../WbUtils/Utils'; +import { WbUtilsComponent } from '../WbUtils'; import { WbValidation } from './WbValidation'; export type WbStatus = 'unupload' | 'upload' | 'validate'; From b7280f9b2156cffa269e17053c3675c5033406db Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:14:11 -0400 Subject: [PATCH 077/153] Move utils stuff into WbUtils folder --- specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts | 3 --- specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx | 1 + .../frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx | 4 ++-- .../frontend/js_src/lib/components/WorkBench/index.tsx | 5 ++++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index 60a9cd1e5af..a4335a75b7c 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -7,13 +7,10 @@ */ import React from 'react'; -import _ from 'underscore'; - import { f } from '../../utils/functools'; import type { RA, WritableArray } from '../../utils/types'; import { camelToKebab } from '../../utils/utils'; - import type { WbSearchPreferences } from '../WorkBench/AdvancedSearch'; import { getInitialSearchPreferences, diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx index 559730aa97a..6a82d1b078b 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import _ from "underscore"; import { Input } from '../Atoms/Form'; import { ReadOnlyContext } from '../Core/Contexts'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx index d9be877f831..83d67f74f03 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DataSetMeta.tsx @@ -361,7 +361,7 @@ export function DataSetName({ useTitle(name); return ( -
+ <>

{dataset.uploadplan !== null && ( @@ -391,6 +391,6 @@ export function DataSetName({ onDeleted={() => unsafeNavigate('/specify/', { replace: true })} /> )} -

+ ); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 92225d3e110..643a4f7e8ea 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -29,6 +29,8 @@ export function WorkBench(): JSX.Element { const [dataset, setDataSet] = useDataSet(datasetId); useErrorContext('dataSet', dataset); const loading = React.useContext(LoadingContext); + + const [refreshKey, setRefreshKey] = React.useState(0); const [isDeleted, handleDeleted] = useBooleanState(); // @ts-ignore figure out how handleDeletedConfirmation was being used in Backbone. possibly not used at all const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); @@ -40,6 +42,7 @@ export function WorkBench(): JSX.Element { const triggerDatasetRefresh = () => { loading(fetchDataSet(dataset.id).then(setDataSet)); + setRefreshKey(refreshKey + 1); }; return datasetId === undefined ? ( @@ -62,7 +65,7 @@ export function WorkBench(): JSX.Element { > Date: Mon, 22 Apr 2024 10:36:00 -0400 Subject: [PATCH 078/153] Fix workbench bugs - reset cell position on revert - show correct action message - fix toggling of live validation --- .../lib/components/WbActions/WbValidate.tsx | 9 ++- .../js_src/lib/components/WbActions/index.tsx | 33 ++++----- .../lib/components/WbUtils/Navigation.tsx | 12 +++- .../components/WorkBench/WbSpreadsheet.tsx | 70 +++++++++---------- .../lib/components/WorkBench/WbView.tsx | 1 + .../js_src/lib/components/WorkBench/index.tsx | 4 +- 6 files changed, 69 insertions(+), 60 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index 64eeaed1ae1..97ff1a43a82 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -6,6 +6,7 @@ import { Button } from '../Atoms/Button'; import { userPreferences } from '../Preferences/userPreferences'; import type { WbValidation } from '../WorkBench/WbValidation'; import type { WbStatus } from '../WorkBench/WbView'; +import { useBooleanState } from '../../hooks/useBooleanState'; export function WbValidate({ hasUnsavedChanges, @@ -21,8 +22,12 @@ export function WbValidate({ 'general', 'liveValidation' ); + const [isLiveValidateOn, _, __, toggleLiveValidate] = useBooleanState(); const handleValidate = () => startUpload('validate'); - const handleToggleDataCheck = () => validation.toggleDataCheck(); + const handleToggleDataCheck = () => { + validation.toggleDataCheck(); + toggleLiveValidate(); + } return ( <> @@ -31,7 +36,7 @@ export function WbValidate({ aria-pressed={validation.validationMode === 'live'} onClick={handleToggleDataCheck} > - {validation.validationMode === 'live' + {isLiveValidateOn && validation.validationMode === 'live' ? validation.liveValidationStack.length > 0 ? commonText.countLine({ resource: wbText.dataCheckOn(), diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 55cd257294e..022c36173d4 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -31,6 +31,7 @@ export function WbActions({ isUploaded, isResultsOpen, workbench, + cellCounts, mappings, checkDeletedFail, onDatasetRefresh: handleRefresh, @@ -42,6 +43,7 @@ export function WbActions({ readonly isUploaded: boolean; readonly isResultsOpen: boolean; readonly workbench: Workbench; + readonly cellCounts: WbCellCounts; readonly mappings: WbMapping | undefined; readonly checkDeletedFail: (statusCode: number) => void; readonly onDatasetRefresh: () => void; @@ -54,7 +56,7 @@ export function WbActions({ useBooleanState(); const [operationCompleted, openOperationCompleted, closeOperationCompleted] = useBooleanState(); - const { modeRef, refreshInitiatorAborted, ...actions } = useWbActions({ + const { mode, refreshInitiatorAborted, ...actions } = useWbActions({ datasetId: dataset.id, onRefresh: handleRefresh, checkDeletedFail, @@ -62,11 +64,7 @@ export function WbActions({ workbench, }); - const cellCounts = workbench.cellCounts[GET]; - const message = - modeRef.current === undefined - ? undefined - : getMessage(cellCounts, modeRef.current); + const message = mode === undefined ? undefined : getMessage(cellCounts, mode); return ( <> @@ -109,7 +107,7 @@ export function WbActions({ {!isUploaded && hasPermission('/workbench/dataset', 'upload') ? ( ) : undefined} - {typeof modeRef.current === 'string' && showStatus ? ( + {typeof mode === 'string' && showStatus ? ( - {cellCounts.invalidCells === 0 && modeRef.current === 'upload' && ( + {cellCounts.invalidCells === 0 && mode === 'upload' && ( { - modeRef.current = undefined; refreshInitiatorAborted.current = false; closeOperationCompleted(); }} @@ -194,17 +191,17 @@ export function WbActions({ - {modeRef.current === 'validate' + {mode === 'validate' ? wbText.validationCanceledDescription() - : modeRef.current === 'unupload' + : mode === 'unupload' ? wbText.rollbackCanceledDescription() : wbText.uploadCanceledDescription()} @@ -226,7 +223,7 @@ function useWbActions({ readonly onRefresh: () => void; readonly onOpenStatus: () => void; }) { - const modeRef = React.useRef(undefined); + const [mode, setMode] = React.useState(undefined); const refreshInitiatorAborted = React.useRef(false); const loading = React.useContext(LoadingContext); @@ -246,7 +243,7 @@ function useWbActions({ }; const triggerStatusComponent = (newMode: WbStatus): void => { - modeRef.current = newMode; + setMode(newMode); handleOpenStatus(); }; @@ -261,7 +258,7 @@ function useWbActions({ }; return { - modeRef, + mode, refreshInitiatorAborted, startUpload, triggerStatusComponent, diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx index 999cc74156b..9c8c90f2282 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx @@ -24,10 +24,12 @@ export function Navigation({ const isReadOnly = React.useContext(ReadOnlyContext); const [currentPosition, setCurrentPosition] = React.useState(0); const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); + const handleTypeToggle = () => { togglePress(); utils.toggleCellTypes(name, 'toggle'); }; + const handlePrevious = () => { const [_, position] = utils.navigateCells({ type: name, @@ -46,6 +48,12 @@ export function Navigation({ }); setCurrentPosition(position); }; + + // reset current position when total count resets + React.useEffect(() => { + if (totalCount == 0) setCurrentPosition(0); + }, [totalCount]) + return ( @@ -79,7 +87,7 @@ export function Navigation({ ({ - name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), - disabled: () => - isReadOnly || - (hot.getSelected()?.every((selection) => selection[0] === selection[2]) ?? - false), - callback: (_, selections) => - selections.forEach((selection) => - Array.from({ - length: selection.end.col + 1 - selection.start.col, - }).forEach((_, index) => { - const startRow = - mode === 'up' ? selection.start.row + 1 : selection.start.row; - const endRow = selection.end.row; - const col = selection.start.col + index; - const value = - mode === 'up' - ? hot.getDataAtCell(endRow, col) - : hot.getDataAtCell(startRow, col); - hot.setDataAtCell( - Array.from({ length: endRow - startRow }, (_, index) => [ - startRow + index + 1, - col, - value, - ]) - ); - }) - ), - }); - function WbSpreadsheetComponent({ dataset, setHotTable, @@ -283,3 +248,38 @@ function WbSpreadsheetComponent({ } export const WbSpreadsheet = React.memo(WbSpreadsheetComponent); + +// Context menu item definitions (common for fillUp and fillDown) +const fillCellsContextMenuItem = ( + hot: Handsontable, + mode: 'down' | 'up', + isReadOnly: boolean +): Handsontable.plugins.ContextMenu.MenuItemConfig => ({ + name: mode === 'up' ? wbText.fillUp() : wbText.fillDown(), + disabled: () => + isReadOnly || + (hot.getSelected()?.every((selection) => selection[0] === selection[2]) ?? + false), + callback: (_, selections) => + selections.forEach((selection) => + Array.from({ + length: selection.end.col + 1 - selection.start.col, + }).forEach((_, index) => { + const startRow = + mode === 'up' ? selection.start.row + 1 : selection.start.row; + const endRow = selection.end.row; + const col = selection.start.col + index; + const value = + mode === 'up' + ? hot.getDataAtCell(endRow, col) + : hot.getDataAtCell(startRow, col); + hot.setDataAtCell( + Array.from({ length: endRow - startRow }, (_, index) => [ + startRow + index + 1, + col, + value, + ]) + ); + }) + ), +}); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 0f7e3fb01c8..8addf9c4220 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -179,6 +179,7 @@ export function WbView({ isUploaded={isUploaded} mappings={mappings} workbench={workbench} + cellCounts={cellCounts} onDatasetRefresh={triggerDatasetRefresh} onSpreadsheetUpToDate={spreadsheetUpToDate} onToggleResults={toggleResults} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 643a4f7e8ea..89272af3337 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -30,7 +30,6 @@ export function WorkBench(): JSX.Element { useErrorContext('dataSet', dataset); const loading = React.useContext(LoadingContext); - const [refreshKey, setRefreshKey] = React.useState(0); const [isDeleted, handleDeleted] = useBooleanState(); // @ts-ignore figure out how handleDeletedConfirmation was being used in Backbone. possibly not used at all const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); @@ -42,7 +41,6 @@ export function WorkBench(): JSX.Element { const triggerDatasetRefresh = () => { loading(fetchDataSet(dataset.id).then(setDataSet)); - setRefreshKey(refreshKey + 1); }; return datasetId === undefined ? ( @@ -65,7 +63,7 @@ export function WorkBench(): JSX.Element { > Date: Mon, 22 Apr 2024 12:35:18 -0400 Subject: [PATCH 079/153] Show updated visual order when hot loads --- .../components/WorkBench/WbSpreadsheet.tsx | 12 +++- .../js_src/lib/components/WorkBench/hooks.ts | 18 +++-- .../lib/components/WorkBench/hotProps.tsx | 66 +++++++++---------- 3 files changed, 53 insertions(+), 43 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 41ba0d19be7..174775f9a8d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -29,6 +29,7 @@ import { getHotProps } from './hotProps'; import type { WbMapping } from './mapping'; import { fetchWbPickLists } from './pickLists'; import type { Workbench } from './WbView'; +import { writable } from '../../utils/types'; registerAllModules(); @@ -175,9 +176,14 @@ function WbSpreadsheetComponent({ (mappings === undefined ? Promise.resolve({}) : fetchWbPickLists(dataset.columns, mappings.tableNames, mappings.lines) - ).then((pickLists) => - configureHandsontable(hot, mappings, dataset, pickLists) - ); + ).then((pickLists) => { + configureHandsontable(hot, mappings, dataset, pickLists); + // check for reordered columns + if (dataset.visualorder?.some((column, index) => column !== index)) + hot.updateSettings({ + manualColumnMove: writable(dataset.visualorder), + }); + }); }, [hot]); // Highlight validation cells diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 6e0a357ac04..27c4765dc61 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -1,3 +1,4 @@ +import React from 'react'; import type Handsontable from 'handsontable'; import type { Events } from 'handsontable/pluginHooks'; import type { Action } from 'handsontable/plugins/undoRedo'; @@ -14,6 +15,7 @@ import { sortFunction } from '../../utils/utils'; import { schema } from '../DataModel/schema'; import { getHotPlugin } from './handsontable'; import type { Workbench } from './WbView'; +import { LoadingContext } from '../Core/Contexts'; export function getHotHooks( workbench: Workbench, @@ -23,6 +25,7 @@ export function getHotHooks( isReadOnly: boolean ): Partial { let sortConfigIsSet: boolean = false; + const loading = React.useContext(LoadingContext); return { /* @@ -469,8 +472,7 @@ export function getHotHooks( // Save new visualOrder on the back end afterColumnMove: (_columnIndexes, _finalIndex, dropIndex) => { // An ugly fix for jQuery's dialogs conflicting with HOT - if (dropIndex === undefined || !workbench.hot) return; - + if (dropIndex === undefined || workbench.hot == undefined) return; workbench.cells.indexedCellMeta = undefined; const columnOrder = workbench.dataset.columns.map((_, visualCol) => @@ -484,11 +486,13 @@ export function getHotHooks( ) ) { overwriteReadOnly(workbench.dataset, 'visualorder', columnOrder); - ping(`/api/workbench/dataset/${workbench.dataset.id}/`, { - method: 'PUT', - body: { visualorder: columnOrder }, - expectedErrors: [Http.NOT_FOUND], - }).then((status) => checkDeletedFail(status)); + loading( + ping(`/api/workbench/dataset/${workbench.dataset.id}/`, { + method: 'PUT', + body: { visualorder: columnOrder }, + expectedErrors: [Http.NOT_FOUND], + }).then(checkDeletedFail) + ); } }, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx index a7ac07e1ec8..310018800dd 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx @@ -18,39 +18,6 @@ const hiddenRows = { copyPasteEnabled: false, }; -function ColumnHeader({ - isMapped, - tableLabel, - tableIcon, - columnName, -}: { - readonly isMapped: boolean; - readonly tableLabel: string; - readonly tableIcon: string | undefined; - readonly columnName: string; -}): JSX.Element { - return ( -
- {isMapped ? ( - {tableLabel} - ) : ( - - {icons.ban} - - )} - {columnName} -
- ); -} - export function getHotProps({ dataset, mappings, @@ -146,3 +113,36 @@ export function getHotProps({ comments, }; } + +function ColumnHeader({ + isMapped, + tableLabel, + tableIcon, + columnName, +}: { + readonly isMapped: boolean; + readonly tableLabel: string; + readonly tableIcon: string | undefined; + readonly columnName: string; +}): JSX.Element { + return ( +
+ {isMapped ? ( + {tableLabel} + ) : ( + + {icons.ban} + + )} + {columnName} +
+ ); +} From 0c38297605a632aa8dca1728404d36643e5239c4 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:22:40 -0400 Subject: [PATCH 080/153] Override default handsontable fonts --- specifyweb/frontend/js_src/css/workbench.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/specifyweb/frontend/js_src/css/workbench.css b/specifyweb/frontend/js_src/css/workbench.css index 9c0af09723d..331dddfa985 100644 --- a/specifyweb/frontend/js_src/css/workbench.css +++ b/specifyweb/frontend/js_src/css/workbench.css @@ -77,6 +77,16 @@ --accent-color: var(--search-result); } +/* + * Override default font properties: required for handsontable 10 and above + * See: https://github.com/handsontable/handsontable/pull/8681 + */ +.handsontable { + @apply text-black dark:text-white; + font-family: inherit; + font-size: inherit; +} + /* Handsontable dark mode */ .handsontable td, .htContextMenu table tbody tr td { From 49ff2b9658a88086340d5782fe596d6794eebba7 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 15:50:36 -0400 Subject: [PATCH 081/153] Refactor to remove spreadsheetContainerRef from workbench object --- .../js_src/lib/components/WbUtils/Utils.ts | 13 +- .../lib/components/WorkBench/WbView.tsx | 162 +++++++++--------- .../js_src/lib/components/WorkBench/index.tsx | 21 +-- 3 files changed, 97 insertions(+), 99 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index a4335a75b7c..515bb951e26 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -12,9 +12,7 @@ import { f } from '../../utils/functools'; import type { RA, WritableArray } from '../../utils/types'; import { camelToKebab } from '../../utils/utils'; import type { WbSearchPreferences } from '../WorkBench/AdvancedSearch'; -import { - getInitialSearchPreferences, -} from '../WorkBench/AdvancedSearch'; +import { getInitialSearchPreferences } from '../WorkBench/AdvancedSearch'; import type { WbCellCounts } from '../WorkBench/CellMeta'; import { getHotPlugin } from '../WorkBench/handsontable'; import { getSelectedLast } from '../WorkBench/hotHelpers'; @@ -31,7 +29,10 @@ export class WbUtils { // eslint-disable-next-line functional/prefer-readonly-type public searchPreferences: WbSearchPreferences = getInitialSearchPreferences(); - constructor(private readonly workbench: Workbench) {} + constructor( + private readonly workbench: Workbench, + private readonly spreadsheetContainerRef: React.RefObject + ) {} navigateCells({ type, @@ -111,7 +112,7 @@ export class WbUtils { const orderIt = direction === 'next' ? f.id - : (array: RA): RA => Array.from(array).reverse(); + : (array: RA): RA => Array.from(array).reverse(); orderIt(Object.entries(cellMetaObject)).find(([visualRowString, metaRow]) => typeof metaRow === 'object' @@ -325,7 +326,7 @@ export class WbUtils { const groupName = camelToKebab(navigationType); const cssClassName = `wb-hide-${groupName}`; const { current: spreadsheetContainer } = - this.workbench.spreadsheetContainerRef; + this.spreadsheetContainerRef; if (spreadsheetContainer) spreadsheetContainer.classList[action](cssClassName); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 8addf9c4220..170909597fb 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -30,7 +30,7 @@ import { WbActions } from '../WbActions'; import { useResults } from '../WbActions/useResults'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbToolkit } from '../WbToolkit'; -import type { WbCellCounts} from './CellMeta'; +import type { WbCellCounts } from './CellMeta'; import { WbCellMeta } from './CellMeta'; import { DataSetName } from './DataSetMeta'; import { Disambiguation } from './DisambiguationLogic'; @@ -42,6 +42,7 @@ import { WbSpreadsheet } from './WbSpreadsheet'; import { WbUtils } from '../WbUtils/Utils'; import { WbUtilsComponent } from '../WbUtils'; import { WbValidation } from './WbValidation'; +import { className } from '../Atoms/className'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -56,7 +57,6 @@ export type Workbench = { readonly mappings: WbMapping | undefined; utils: WbUtils; readonly cellCounts: GetSet; - readonly spreadsheetContainerRef: React.RefObject; undoRedoIsHandled: boolean; readonly spreadsheetChanged: () => void; }; @@ -65,12 +65,10 @@ export function WbView({ dataset, onDatasetDeleted: handleDatasetDeleted, triggerDatasetRefresh, - spreadsheetContainerRef, }: { readonly dataset: Dataset; readonly onDatasetDeleted: () => void; readonly triggerDatasetRefresh: () => void; - readonly spreadsheetContainerRef: React.RefObject; }): JSX.Element { const data = React.useMemo>>( () => @@ -80,10 +78,12 @@ export function WbView({ [dataset] ); + const spreadsheetContainerRef = React.useRef(null); const [hotTable, setHotTable] = React.useState(null); const hot = hotTable?.hotInstance ?? undefined; - const isUploaded = dataset.uploadresult !== null && dataset.uploadresult.success; + const isUploaded = + dataset.uploadresult !== null && dataset.uploadresult.success; const isAlreadyReadOnly = React.useContext(ReadOnlyContext); const mappings = React.useMemo( @@ -115,14 +115,13 @@ export function WbView({ validation: undefined!, utils: undefined!, cellCounts: [cellCounts, setCellCounts], - spreadsheetContainerRef, undoRedoIsHandled: false, spreadsheetChanged, }; workbench.cells = new WbCellMeta(workbench); workbench.disambiguation = new Disambiguation(workbench); workbench.validation = new WbValidation(workbench); - workbench.utils = new WbUtils(workbench); + workbench.utils = new WbUtils(workbench, spreadsheetContainerRef); return workbench; }, [dataset, hot]); @@ -152,83 +151,90 @@ export function WbView({ }); return ( - -
+
- - - {commonText.tools()} - - - {canUpdate || isMapped ? ( - - {wbPlanText.dataMapper()} - + + + {commonText.tools()} + + + {canUpdate || isMapped ? ( + + {wbPlanText.dataMapper()} + + ) : undefined} + +
+ {showToolkit && typeof hot === 'object' ? ( + ) : undefined} - + + {showResults ? ( + + ) : undefined} +

+ {disambiguationDialogs} + -
- {showToolkit && typeof hot === 'object' ? ( - - ) : undefined} -
- - {showResults ? ( - - ) : undefined} -
- {disambiguationDialogs} - + ); } diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 89272af3337..609158e6742 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -35,7 +35,6 @@ export function WorkBench(): JSX.Element { const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); const navigate = useNavigate(); - const spreadsheetContainerRef = React.useRef(null); if (dataset === undefined || !treeRanksLoaded) return ; @@ -56,20 +55,12 @@ export function WorkBench(): JSX.Element { {wbText.dataSetDeletedDescription()} ) : ( -
-
- -
-
+ ); } From ac8655dda5e89f1c27107649da6a5595aed1c36b Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:34:34 -0400 Subject: [PATCH 082/153] Add unload protect to spreadsheet --- .../lib/components/WorkBench/WbView.tsx | 3 +++ .../js_src/lib/components/WorkBench/index.tsx | 19 +++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 170909597fb..ffe214e608a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -43,6 +43,8 @@ import { WbUtils } from '../WbUtils/Utils'; import { WbUtilsComponent } from '../WbUtils'; import { WbValidation } from './WbValidation'; import { className } from '../Atoms/className'; +import { useUnloadProtect } from '../../hooks/navigation'; +import { wbText } from '../../localization/workbench'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -95,6 +97,7 @@ export function WbView({ const [hasUnsavedChanges, spreadsheetChanged, spreadsheetUpToDate] = useBooleanState(); + useUnloadProtect(hasUnsavedChanges, wbText.wbUnloadProtect()); const [cellCounts, setCellCounts] = React.useState({ newCells: 0, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 609158e6742..35b0585ee99 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -9,7 +9,6 @@ import { wbText } from '../../localization/workbench'; import { ajax } from '../../utils/ajax'; import { f } from '../../utils/functools'; import type { GetSet } from '../../utils/types'; -import { className } from '../Atoms/className'; import { LoadingContext } from '../Core/Contexts'; import { useMenuItem } from '../Header/MenuContext'; import { treeRanksPromise } from '../InitialContext/treeRanks'; @@ -71,16 +70,16 @@ function useDataSet( datasetId: number | undefined ): GetSet { return useAsyncState( - React.useCallback(async () => fetchDataSet(datasetId), [datasetId]), + React.useCallback( + async () => + typeof datasetId === 'number' ? fetchDataSet(datasetId) : undefined, + [datasetId] + ), true ); } -const fetchDataSet = async ( - datasetId: number | undefined -): Promise => - typeof datasetId === 'number' - ? ajax(`/api/workbench/dataset/${datasetId}/`, { - headers: { Accept: 'application/json' }, - }).then(({ data }) => data) - : undefined; +const fetchDataSet = async (datasetId: number): Promise => + ajax(`/api/workbench/dataset/${datasetId}/`, { + headers: { Accept: 'application/json' }, + }).then(({ data }) => data); From 2f9a6d4939b989495e0528972bf31a570968817f Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:51:46 -0400 Subject: [PATCH 083/153] Rename DataSet to dataset --- .../frontend/js_src/lib/components/WbToolkit/index.tsx | 4 ++-- .../js_src/lib/components/WorkBench/helpers.ts | 2 +- .../frontend/js_src/lib/components/WorkBench/index.tsx | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx index 040c819cda5..74b6a595b11 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx @@ -10,7 +10,7 @@ import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { hasPermission, hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { downloadDataSet } from '../WorkBench/helpers'; +import { downloadDataset } from '../WorkBench/helpers'; import type { WbMapping } from '../WorkBench/mapping'; import { WbChangeOwner } from './ChangeOwner'; import { WbConvertCoordinates } from './CoordinateConverter'; @@ -42,7 +42,7 @@ export function WbToolkit({ 'exportFileDelimiter' ); - downloadDataSet( + downloadDataset( dataset.name, dataset.rows, dataset.columns, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts index 78a88b5d1ad..1c69068469e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts @@ -3,7 +3,7 @@ import { stringify } from 'csv-stringify/browser/esm'; import type { RA } from '../../utils/types'; import { downloadFile } from '../Molecules/FilePicker'; -export const downloadDataSet = async ( +export const downloadDataset = async ( name: string, rows: RA>, columns: RA, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 35b0585ee99..cce800fd44d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -25,7 +25,7 @@ export function WorkBench(): JSX.Element { const { id } = useParams(); const datasetId = f.parseInt(id); - const [dataset, setDataSet] = useDataSet(datasetId); + const [dataset, setDataset] = useDataset(datasetId); useErrorContext('dataSet', dataset); const loading = React.useContext(LoadingContext); @@ -38,7 +38,7 @@ export function WorkBench(): JSX.Element { if (dataset === undefined || !treeRanksLoaded) return ; const triggerDatasetRefresh = () => { - loading(fetchDataSet(dataset.id).then(setDataSet)); + loading(fetchDataset(dataset.id).then(setDataset)); }; return datasetId === undefined ? ( @@ -66,20 +66,20 @@ export function WorkBench(): JSX.Element { const fetchTreeRanks = async (): Promise => treeRanksPromise.then(f.true); // BUG: intercept 403 (if dataset has been transferred to another user) -function useDataSet( +function useDataset( datasetId: number | undefined ): GetSet { return useAsyncState( React.useCallback( async () => - typeof datasetId === 'number' ? fetchDataSet(datasetId) : undefined, + typeof datasetId === 'number' ? fetchDataset(datasetId) : undefined, [datasetId] ), true ); } -const fetchDataSet = async (datasetId: number): Promise => +const fetchDataset = async (datasetId: number): Promise => ajax(`/api/workbench/dataset/${datasetId}/`, { headers: { Accept: 'application/json' }, }).then(({ data }) => data); From 8baea247529941db816b2f1e08b3eb4d3fb48d6f Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 16:57:22 -0400 Subject: [PATCH 084/153] prettier --- .../js_src/lib/components/WbActions/WbValidate.tsx | 2 +- .../frontend/js_src/lib/components/WbToolkit/index.tsx | 10 +++++----- .../js_src/lib/components/WorkBench/hotProps.tsx | 7 +++++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index 97ff1a43a82..a0240bec349 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -27,7 +27,7 @@ export function WbValidate({ const handleToggleDataCheck = () => { validation.toggleDataCheck(); toggleLiveValidate(); - } + }; return ( <> diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx index 74b6a595b11..690337f704f 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx @@ -62,11 +62,11 @@ export function WbToolkit({ {hasPermission('/workbench/dataset', 'transfer') && hasTablePermission('SpecifyUser', 'read') ? ( - - + + ) : undefined} ({ + const hiddenColumns = React.useMemo( + () => ({ // Hide the disambiguation column columns: [dataset.columns.length], indicators: false, // TODO: Typing possibly doesn't match for handsontable 12.1.0, fixed in 14 copyPasteEnabled: false, - }), []); + }), + [] + ); const minSpareRows = userPreferences.get( 'workBench', From b030e91799d5a7cf6483f4e58c46dae2e8e1a446 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:21:32 -0400 Subject: [PATCH 085/153] lint --- .../lib/components/WbActions/WbValidate.tsx | 2 +- .../components/WorkBench/WbSpreadsheet.tsx | 7 ++++--- .../lib/components/WorkBench/WbView.tsx | 20 ++++++++++--------- .../js_src/lib/components/WorkBench/hooks.ts | 4 ++-- .../js_src/lib/components/WorkBench/index.tsx | 2 +- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index a0240bec349..43d61b2397e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -1,12 +1,12 @@ import React from 'react'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { Button } from '../Atoms/Button'; import { userPreferences } from '../Preferences/userPreferences'; import type { WbValidation } from '../WorkBench/WbValidation'; import type { WbStatus } from '../WorkBench/WbView'; -import { useBooleanState } from '../../hooks/useBooleanState'; export function WbValidate({ hasUnsavedChanges, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 174775f9a8d..e174b8f142f 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -16,6 +16,7 @@ import { commonText } from '../../localization/common'; import { LANGUAGE } from '../../localization/utils/config'; import { wbText } from '../../localization/workbench'; import { type RA } from '../../utils/types'; +import { writable } from '../../utils/types'; import { legacyNonJsxIcons } from '../Atoms/Icons'; import { iconClassName } from '../Atoms/Icons'; import { ReadOnlyContext } from '../Core/Contexts'; @@ -29,7 +30,6 @@ import { getHotProps } from './hotProps'; import type { WbMapping } from './mapping'; import { fetchWbPickLists } from './pickLists'; import type { Workbench } from './WbView'; -import { writable } from '../../utils/types'; registerAllModules(); @@ -178,7 +178,7 @@ function WbSpreadsheetComponent({ : fetchWbPickLists(dataset.columns, mappings.tableNames, mappings.lines) ).then((pickLists) => { configureHandsontable(hot, mappings, dataset, pickLists); - // check for reordered columns + // Check for reordered columns if (dataset.visualorder?.some((column, index) => column !== index)) hot.updateSettings({ manualColumnMove: writable(dataset.visualorder), @@ -228,7 +228,8 @@ function WbSpreadsheetComponent({ commentedCellClassName="htCommentCell" comments={comments} contextMenu={contextMenuConfig} - data={data as (string | null)[][]} + // @ts-expect-error data throws error for readonly input + data={data as readonly (readonly (string | null)[])[]} enterBeginsEditing={enterBeginsEditing} enterMoves={enterMoves} hiddenColumns={hiddenColumns} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index ffe214e608a..282e246ed1d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -16,13 +16,16 @@ import type { HotTable } from '@handsontable/react'; import type Handsontable from 'handsontable'; import React from 'react'; +import { useUnloadProtect } from '../../hooks/navigation'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { wbPlanText } from '../../localization/wbPlan'; +import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; import type { GetSet, RA } from '../../utils/types'; import { clamp } from '../../utils/utils'; import { Button } from '../Atoms/Button'; +import { className } from '../Atoms/className'; import { Link } from '../Atoms/Link'; import { ReadOnlyContext } from '../Core/Contexts'; import { hasPermission } from '../Permissions/helpers'; @@ -30,6 +33,8 @@ import { WbActions } from '../WbActions'; import { useResults } from '../WbActions/useResults'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbToolkit } from '../WbToolkit'; +import { WbUtilsComponent } from '../WbUtils'; +import { WbUtils } from '../WbUtils/Utils'; import type { WbCellCounts } from './CellMeta'; import { WbCellMeta } from './CellMeta'; import { DataSetName } from './DataSetMeta'; @@ -39,27 +44,24 @@ import { parseWbMappings } from './mapping'; import { WbUploaded } from './Results'; import { useDisambiguationDialog } from './useDisambiguationDialog'; import { WbSpreadsheet } from './WbSpreadsheet'; -import { WbUtils } from '../WbUtils/Utils'; -import { WbUtilsComponent } from '../WbUtils'; import { WbValidation } from './WbValidation'; -import { className } from '../Atoms/className'; -import { useUnloadProtect } from '../../hooks/navigation'; -import { wbText } from '../../localization/workbench'; export type WbStatus = 'unupload' | 'upload' | 'validate'; export type Workbench = { - readonly dataset: Dataset; + /* eslint-disable functional/prefer-readonly-type */ cells: WbCellMeta; disambiguation: Disambiguation; validation: WbValidation; + utils: WbUtils; + undoRedoIsHandled: boolean; + /* eslint-enable functional/prefer-readonly-type */ + readonly dataset: Dataset; readonly data: RA>; readonly hot: Handsontable | undefined; readonly throttleRate: number; readonly mappings: WbMapping | undefined; - utils: WbUtils; readonly cellCounts: GetSet; - undoRedoIsHandled: boolean; readonly spreadsheetChanged: () => void; }; @@ -180,6 +182,7 @@ export function WbView({ ) : undefined} Date: Mon, 22 Apr 2024 17:23:11 -0400 Subject: [PATCH 086/153] prettier + lint for WbUtils --- .../lib/components/WbUtils/Navigation.tsx | 10 +- .../js_src/lib/components/WbUtils/Utils.ts | 5 +- .../js_src/lib/components/WbUtils/index.tsx | 204 +++++++++--------- 3 files changed, 108 insertions(+), 111 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx index 9c8c90f2282..86564b7f9b7 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx @@ -24,7 +24,7 @@ export function Navigation({ const isReadOnly = React.useContext(ReadOnlyContext); const [currentPosition, setCurrentPosition] = React.useState(0); const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); - + const handleTypeToggle = () => { togglePress(); utils.toggleCellTypes(name, 'toggle'); @@ -49,10 +49,10 @@ export function Navigation({ setCurrentPosition(position); }; - // reset current position when total count resets + // Reset current position when total count resets React.useEffect(() => { if (totalCount == 0) setCurrentPosition(0); - }, [totalCount]) + }, [totalCount]); return ( @@ -87,7 +87,7 @@ export function Navigation({ (null); - const replaceRef = React.useRef(null); - - return ( -
- + isUploaded, + cellCounts, + utils, + cells, + debounceRate, +}: { + readonly isUploaded: boolean; + readonly cellCounts: WbCellCounts; + readonly utils: WbUtils; + readonly cells: WbCellMeta; + readonly debounceRate: number; +}): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); + const searchRef = React.useRef(null); + const replaceRef = React.useRef(null); + + return ( +
+ +
+ ) => + utils.searchCells(event, searchRef.current!), + debounceRate, + true + )} + /> +
+ {!isUploaded && hasPermission('/workbench/dataset', 'update') ? (
) => - utils.searchCells(event, searchRef.current!), - debounceRate, - true - )} + onKeyDown={(event) => + utils.replaceCells(event, replaceRef.current!) + } />
- {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( -
- - utils.replaceCells(event, replaceRef.current!) - } - /> -
- ) : undefined} - { - if ( - newSearchPreferences.navigation.direction !== - utils.searchPreferences.navigation.direction - ) { - cells.indexedCellMeta = undefined; - } - utils.searchPreferences = newSearchPreferences; - /* - * TODO: figure out what searchCells with SettingsChange does - * if (utils.searchPreferences.search.liveUpdate) - * utils.searchCells({ - * key: 'SettingsChange', - * }).catch(softFail); - */ - }} - /> -
+ ) : undefined} + { + if ( + newSearchPreferences.navigation.direction !== + utils.searchPreferences.navigation.direction + ) { + cells.indexedCellMeta = undefined; + } + utils.searchPreferences = newSearchPreferences; + /* + * TODO: figure out what searchCells with SettingsChange does + * if (utils.searchPreferences.search.liveUpdate) + * utils.searchCells({ + * key: 'SettingsChange', + * }).catch(softFail); + */ + }} + /> + + + {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( - {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( - - ) : undefined} + ) : undefined} + + {!isUploaded && ( - {!isUploaded && ( - - )} -
- ); - } \ No newline at end of file + )} +
+ ); +} From 34f813c9f92ba00419223de489082875eaca0daf Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:27:17 -0400 Subject: [PATCH 087/153] Fix type errors --- specifyweb/frontend/js_src/lib/components/WbActions/index.tsx | 1 - specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx | 4 ++-- .../frontend/js_src/lib/components/WorkBench/helpers.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 022c36173d4..a8b1269d6d7 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -5,7 +5,6 @@ import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { Http } from '../../utils/ajax/definitions'; import { ping } from '../../utils/ajax/ping'; -import { GET } from '../../utils/utils'; import { Button } from '../Atoms/Button'; import { LoadingContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx index 690337f704f..68a211fb1f0 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx @@ -10,7 +10,7 @@ import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { hasPermission, hasTablePermission } from '../Permissions/helpers'; import { userPreferences } from '../Preferences/userPreferences'; import type { Dataset } from '../WbPlanView/Wrapped'; -import { downloadDataset } from '../WorkBench/helpers'; +import { downloadDataSet } from '../WorkBench/helpers'; import type { WbMapping } from '../WorkBench/mapping'; import { WbChangeOwner } from './ChangeOwner'; import { WbConvertCoordinates } from './CoordinateConverter'; @@ -42,7 +42,7 @@ export function WbToolkit({ 'exportFileDelimiter' ); - downloadDataset( + downloadDataSet( dataset.name, dataset.rows, dataset.columns, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts index 1c69068469e..78a88b5d1ad 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/helpers.ts @@ -3,7 +3,7 @@ import { stringify } from 'csv-stringify/browser/esm'; import type { RA } from '../../utils/types'; import { downloadFile } from '../Molecules/FilePicker'; -export const downloadDataset = async ( +export const downloadDataSet = async ( name: string, rows: RA>, columns: RA, From aaf3d673a9ce90886672c6891a7fa7a3a3d06035 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Mon, 22 Apr 2024 17:31:06 -0400 Subject: [PATCH 088/153] Refactor hot props to use userPreferences.use() --- .../lib/components/WorkBench/hotProps.tsx | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx index e28c182331b..fae3638ad3f 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx @@ -27,9 +27,17 @@ export function getHotProps({ readonly mappings: WbMapping | undefined; readonly physicalColToMappingCol: (physicalCol: number) => number | undefined; }) { - const autoWrapCol = userPreferences.get('workBench', 'editor', 'autoWrapCol'); + const [autoWrapCol] = userPreferences.use( + 'workBench', + 'editor', + 'autoWrapCol' + ); - const autoWrapRow = userPreferences.get('workBench', 'editor', 'autoWrapRow'); + const [autoWrapRow] = userPreferences.use( + 'workBench', + 'editor', + 'autoWrapRow' + ); const columns = React.useMemo( () => @@ -44,10 +52,13 @@ export function getHotProps({ [dataset.columns.length] ); + const [enterMovesPref] = userPreferences.use( + 'workBench', + 'editor', + 'enterMoveDirection' + ); const enterMoves = - userPreferences.get('workBench', 'editor', 'enterMoveDirection') === 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 }; + enterMovesPref === 'col' ? { col: 1, row: 0 } : { col: 0, row: 1 }; const colHeaders = React.useCallback( (physicalCol: number) => { @@ -74,7 +85,7 @@ export function getHotProps({ [mappings] ); - const enterBeginsEditing = userPreferences.get( + const [enterBeginsEditing] = userPreferences.use( 'workBench', 'editor', 'enterBeginsEditing' @@ -91,16 +102,19 @@ export function getHotProps({ [] ); - const minSpareRows = userPreferences.get( + const [minSpareRows] = userPreferences.use( 'workBench', 'editor', 'minSpareRows' ); + const [tabMovesPref] = userPreferences.use( + 'workBench', + 'editor', + 'tabMoveDirection' + ); const tabMoves = - userPreferences.get('workBench', 'editor', 'tabMoveDirection') === 'col' - ? { col: 1, row: 0 } - : { col: 0, row: 1 }; + tabMovesPref === 'col' ? { col: 1, row: 0 } : { col: 0, row: 1 }; return { autoWrapCol, From 8f1263900e1ba2aba91fe945effba1faac9407e9 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 23 Apr 2024 13:01:15 -0400 Subject: [PATCH 089/153] Make data prop mutable --- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index e174b8f142f..8a1e264a110 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -228,8 +228,8 @@ function WbSpreadsheetComponent({ commentedCellClassName="htCommentCell" comments={comments} contextMenu={contextMenuConfig} - // @ts-expect-error data throws error for readonly input - data={data as readonly (readonly (string | null)[])[]} + // eslint-disable-next-line functional/prefer-readonly-type + data={data as (string | null)[][]} enterBeginsEditing={enterBeginsEditing} enterMoves={enterMoves} hiddenColumns={hiddenColumns} From 351876a0159ec2c703f44a0d932376a455622f61 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 23 Apr 2024 15:58:24 -0400 Subject: [PATCH 090/153] Fix loading bar when changing datasets --- .../js_src/lib/components/WorkBench/index.tsx | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index be0574fdaf2..1344bd3097d 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -1,10 +1,9 @@ import React from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { useParams } from 'react-router-dom'; import { useAsyncState } from '../../hooks/useAsyncState'; import { useBooleanState } from '../../hooks/useBooleanState'; import { useErrorContext } from '../../hooks/useErrorContext'; -import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { ajax } from '../../utils/ajax'; import { f } from '../../utils/functools'; @@ -13,7 +12,6 @@ import { LoadingContext } from '../Core/Contexts'; import { useMenuItem } from '../Header/MenuContext'; import { treeRanksPromise } from '../InitialContext/treeRanks'; import { LoadingScreen } from '../Molecules/Dialog'; -import { Dialog } from '../Molecules/Dialog'; import { NotFoundView } from '../Router/NotFoundView'; import type { Dataset } from '../WbPlanView/Wrapped'; import { WbView } from './WbView'; @@ -27,15 +25,12 @@ export function WorkBench(): JSX.Element { const [dataset, setDataset] = useDataset(datasetId); useErrorContext('dataSet', dataset); - const loading = React.useContext(LoadingContext); + const loading = React.useContext(LoadingContext); const [isDeleted, handleDeleted] = useBooleanState(); - // @ts-expect-error figure out how handleDeletedConfirmation was being used in Backbone. possibly not used at all - const [isDeletedConfirmation, handleDeletedConfirmation] = useBooleanState(); - - const navigate = useNavigate(); - if (dataset === undefined || !treeRanksLoaded) return ; + if (dataset === undefined || !treeRanksLoaded || dataset.id !== datasetId) + return ; const triggerDatasetRefresh = () => { loading(fetchDataset(dataset.id).then(setDataset)); @@ -45,14 +40,6 @@ export function WorkBench(): JSX.Element { ) : isDeleted ? ( <>{wbText.dataSetDeletedOrNotFound()} - ) : isDeletedConfirmation ? ( - navigate('/specify/', { replace: true })} - > - {wbText.dataSetDeletedDescription()} - ) : ( Date: Tue, 23 Apr 2024 16:22:56 -0400 Subject: [PATCH 091/153] Add error context --- .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 282e246ed1d..38002ecc096 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -45,6 +45,7 @@ import { WbUploaded } from './Results'; import { useDisambiguationDialog } from './useDisambiguationDialog'; import { WbSpreadsheet } from './WbSpreadsheet'; import { WbValidation } from './WbValidation'; +import { useErrorContext } from '../../hooks/useErrorContext'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -94,6 +95,7 @@ export function WbView({ (): WbMapping | undefined => parseWbMappings(dataset), [dataset] ); + useErrorContext('mappings', mappings) const throttleRate = Math.ceil(clamp(10, data.length / 10, 2000)); @@ -130,6 +132,11 @@ export function WbView({ return workbench; }, [dataset, hot]); + useErrorContext('cells', workbench.cells); + useErrorContext('disambiguation', workbench.disambiguation); + useErrorContext('validation', workbench.validation); + useErrorContext('utils', workbench.utils); + const checkDeletedFail = React.useCallback((statusCode: number): boolean => { if (statusCode === Http.NOT_FOUND) handleDatasetDeleted(); return statusCode === Http.NOT_FOUND; From 6ef0b8dc42933939d9ee686871c03b72969a2775 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:24:10 -0400 Subject: [PATCH 092/153] prettier + lint --- .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 38002ecc096..9b6889a6ad7 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { useUnloadProtect } from '../../hooks/navigation'; import { useBooleanState } from '../../hooks/useBooleanState'; +import { useErrorContext } from '../../hooks/useErrorContext'; import { commonText } from '../../localization/common'; import { wbPlanText } from '../../localization/wbPlan'; import { wbText } from '../../localization/workbench'; @@ -45,7 +46,6 @@ import { WbUploaded } from './Results'; import { useDisambiguationDialog } from './useDisambiguationDialog'; import { WbSpreadsheet } from './WbSpreadsheet'; import { WbValidation } from './WbValidation'; -import { useErrorContext } from '../../hooks/useErrorContext'; export type WbStatus = 'unupload' | 'upload' | 'validate'; @@ -95,7 +95,7 @@ export function WbView({ (): WbMapping | undefined => parseWbMappings(dataset), [dataset] ); - useErrorContext('mappings', mappings) + useErrorContext('mappings', mappings); const throttleRate = Math.ceil(clamp(10, data.length / 10, 2000)); From beb0af8c3dc40d859604ef0789141700fae82558 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Tue, 23 Apr 2024 16:36:30 -0400 Subject: [PATCH 093/153] update comments --- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 8a1e264a110..5ebea37b443 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -1,9 +1,5 @@ /** * Component for the Handsontable React wrapper - * - * TODO: - * - Upgrade to handsontable14 to fix copyPasteEnabled error - * - Fix font size and color in the table */ import { HotTable } from '@handsontable/react'; From 2082a62bff2b150e5abc94708d331ea2a5b2c20b Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:08:39 -0400 Subject: [PATCH 094/153] Add searchCells with SettingsChange for WbUtils --- .../js_src/lib/components/WbUtils/index.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx index 287ac8d3c08..d0227c75b68 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -78,13 +78,16 @@ export function WbUtilsComponent({ cells.indexedCellMeta = undefined; } utils.searchPreferences = newSearchPreferences; - /* - * TODO: figure out what searchCells with SettingsChange does - * if (utils.searchPreferences.search.liveUpdate) - * utils.searchCells({ - * key: 'SettingsChange', - * }).catch(softFail); - */ + if ( + utils.searchPreferences.search.liveUpdate && + searchRef.current !== null + ) + utils.searchCells( + { + key: 'SettingsChange', + }, + searchRef.current + ); }} />
From 1ebb231ac1793342898aca00e40b9529c1148255 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:34:04 -0400 Subject: [PATCH 095/153] Lift searchRef up to WbView --- .../js_src/lib/components/WbActions/WbSave.tsx | 7 +++---- .../js_src/lib/components/WbActions/index.tsx | 3 +++ .../frontend/js_src/lib/components/WbUtils/Utils.ts | 11 +++++------ .../frontend/js_src/lib/components/WbUtils/index.tsx | 8 +++++--- .../js_src/lib/components/WorkBench/WbView.tsx | 4 ++++ 5 files changed, 20 insertions(+), 13 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index 1618176bdb9..051dc6f6450 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -17,10 +17,12 @@ export function WbSave({ workbench, hasUnsavedChanges, checkDeletedFail, + searchRef, onSpreadsheetUpToDate: handleSpreadsheetUpToDate, }: { readonly workbench: Workbench; readonly hasUnsavedChanges: boolean; + readonly searchRef: React.MutableRefObject; readonly checkDeletedFail: (statusCode: number) => void; readonly onSpreadsheetUpToDate: () => void; }): JSX.Element { @@ -47,10 +49,7 @@ export function WbSave({ .then(() => { handleSpreadsheetUpToDate(); workbench.cells.cellMeta = []; - /* - * TODO: Figure out how to rework searchCells for SettingsChange as input - * workbench.utils?.searchCells({ key: 'SettingsChange' }); - */ + workbench.utils?.searchCells({ key: 'SettingsChange' }, searchRef.current); workbench.hot?.render(); closeProgressBar(); }) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index a8b1269d6d7..33306917832 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -33,6 +33,7 @@ export function WbActions({ cellCounts, mappings, checkDeletedFail, + searchRef, onDatasetRefresh: handleRefresh, onSpreadsheetUpToDate: handleSpreadsheetUpToDate, onToggleResults: handleToggleResults, @@ -44,6 +45,7 @@ export function WbActions({ readonly workbench: Workbench; readonly cellCounts: WbCellCounts; readonly mappings: WbMapping | undefined; + readonly searchRef: React.MutableRefObject; readonly checkDeletedFail: (statusCode: number) => void; readonly onDatasetRefresh: () => void; readonly onSpreadsheetUpToDate: () => void; @@ -128,6 +130,7 @@ export function WbActions({ checkDeletedFail={checkDeletedFail} hasUnsavedChanges={hasUnsavedChanges} workbench={workbench} + searchRef={searchRef} onSpreadsheetUpToDate={handleSpreadsheetUpToDate} />
diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index 8f515573d5b..f92b3e8a14a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -179,9 +179,9 @@ export class WbUtils { event: | React.KeyboardEvent | { readonly key: 'SettingsChange' }, - searchQueryElement: HTMLInputElement + searchQueryElement: HTMLInputElement | null ) { - if (this.workbench.hot === undefined) return; + if (this.workbench.hot === undefined || searchQueryElement === null) return; /* * Don't rerun search on live search if search query did not change * (e.x, if Ctrl/Cmd+A is clicked in the search box) @@ -348,17 +348,16 @@ export class WbUtils { replaceCells( event: React.KeyboardEvent, - replacementValueElement: HTMLInputElement + replacementValueElement: HTMLInputElement | null ) { if ( event.key !== 'Enter' || (this.searchPreferences.search.useRegex && this.searchQuery === undefined) || - this.workbench.hot === undefined + this.workbench.hot === undefined || replacementValueElement === null ) return; - - if (replacementValueElement === undefined) return; + const replacementValue = this.searchPreferences.search.useRegex ? replacementValueElement.value : replacementValueElement.value.trim(); diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx index d0227c75b68..4b95ac51f7d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -17,15 +17,17 @@ export function WbUtilsComponent({ utils, cells, debounceRate, + searchRef, }: { readonly isUploaded: boolean; readonly cellCounts: WbCellCounts; readonly utils: WbUtils; readonly cells: WbCellMeta; readonly debounceRate: number; + readonly searchRef: React.MutableRefObject; }): JSX.Element { + const isReadOnly = React.useContext(ReadOnlyContext); - const searchRef = React.useRef(null); const replaceRef = React.useRef(null); return ( @@ -46,7 +48,7 @@ export function WbUtilsComponent({ type="search" onKeyDown={_.debounce( (event: React.KeyboardEvent) => - utils.searchCells(event, searchRef.current!), + utils.searchCells(event, searchRef.current), debounceRate, true )} @@ -63,7 +65,7 @@ export function WbUtilsComponent({ title={wbText.replacementValue()} type="search" onKeyDown={(event) => - utils.replaceCells(event, replaceRef.current!) + utils.replaceCells(event, replaceRef.current) } />
diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 9b6889a6ad7..05598843236 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -162,6 +162,8 @@ export function WbView({ disambiguation: workbench.disambiguation, }); + const searchRef = React.useRef(null); + return ( From b79906c395332f08280b94ce5cab45dae9be6aa8 Mon Sep 17 00:00:00 2001 From: Sharad <16229739+sharadsw@users.noreply.github.com> Date: Wed, 24 Apr 2024 10:35:02 -0400 Subject: [PATCH 096/153] prettier + lint --- .../frontend/js_src/lib/components/WbActions/WbSave.tsx | 5 ++++- .../frontend/js_src/lib/components/WbActions/index.tsx | 2 +- specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts | 5 +++-- specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx | 1 - .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 4 ++-- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index 051dc6f6450..815614baead 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -49,7 +49,10 @@ export function WbSave({ .then(() => { handleSpreadsheetUpToDate(); workbench.cells.cellMeta = []; - workbench.utils?.searchCells({ key: 'SettingsChange' }, searchRef.current); + workbench.utils?.searchCells( + { key: 'SettingsChange' }, + searchRef.current + ); workbench.hot?.render(); closeProgressBar(); }) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 33306917832..eb7ea22de24 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -129,8 +129,8 @@ export function WbActions({ diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index f92b3e8a14a..215074a69aa 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -354,10 +354,11 @@ export class WbUtils { event.key !== 'Enter' || (this.searchPreferences.search.useRegex && this.searchQuery === undefined) || - this.workbench.hot === undefined || replacementValueElement === null + this.workbench.hot === undefined || + replacementValueElement === null ) return; - + const replacementValue = this.searchPreferences.search.useRegex ? replacementValueElement.value : replacementValueElement.value.trim(); diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx index 4b95ac51f7d..5962ffce71d 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -26,7 +26,6 @@ export function WbUtilsComponent({ readonly debounceRate: number; readonly searchRef: React.MutableRefObject; }): JSX.Element { - const isReadOnly = React.useContext(ReadOnlyContext); const replaceRef = React.useRef(null); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 05598843236..b208ae2b383 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -198,8 +198,8 @@ export function WbView({ isResultsOpen={showResults} isUploaded={isUploaded} mappings={mappings} - workbench={workbench} searchRef={searchRef} + workbench={workbench} onDatasetRefresh={triggerDatasetRefresh} onSpreadsheetUpToDate={spreadsheetUpToDate} onToggleResults={toggleResults} @@ -247,8 +247,8 @@ export function WbView({ cells={workbench.cells} debounceRate={throttleRate} isUploaded={isUploaded} - utils={workbench.utils} searchRef={searchRef} + utils={workbench.utils} /> From b7ac0ffd9e7e13ace98a037de045618ee7e2c11a Mon Sep 17 00:00:00 2001 From: Caroline D <108160931+CarolineDenis@users.noreply.github.com> Date: Thu, 25 Apr 2024 07:30:41 -0700 Subject: [PATCH 097/153] Remove unused strings --- .../js_src/lib/localization/workbench.ts | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/localization/workbench.ts b/specifyweb/frontend/js_src/lib/localization/workbench.ts index b5f655a3e4b..6de524dbb78 100644 --- a/specifyweb/frontend/js_src/lib/localization/workbench.ts +++ b/specifyweb/frontend/js_src/lib/localization/workbench.ts @@ -835,29 +835,6 @@ export const wbText = createDictionary({ 'uk-ua': 'Набір даних має бути перевірений або завантажений', 'de-ch': 'Der Datensatz muss validiert oder hochgeladen werden', }, - wbValidateUnavailable: { - 'en-us': - 'An Upload Plan needs to defined before this Data Set can be Validated', - 'ru-ru': ` - План загрузки должен быть определен до того, как этот набор данных может - быть проверен - `, - 'es-es': ` - Se necesita definir un Plan de Carga antes de poder Validar este Conjunto - de Datos - `, - 'fr-fr': ` - Un plan de téléchargement doit être défini avant que cet ensemble de - données puisse être validé - `, - 'uk-ua': ` - Перед перевіркою цього набору даних необхідно визначити план завантаження - `, - 'de-ch': ` - Bevor dieser Datensatz validiert werden kann, muss ein Upload-Plan - definiert werden - `, - }, unavailableWhileEditing: { 'en-us': 'This action requires all changes to be saved', 'ru-ru': 'Это действие требует сохранения всех изменений', @@ -888,33 +865,6 @@ export const wbText = createDictionary({ aufweisen `, }, - unavailableWhileViewingResults: { - 'en-us': 'This action is unavailable while viewing the upload results', - 'ru-ru': 'Это действие недоступно при просмотре результатов загрузки', - 'es-es': ` - Acción no disponible cuando se visualizan resultados de la carga de datos - `, - 'fr-fr': ` - Cette action n'est pas disponible lors de l'affichage des résultats du - téléchargement - `, - 'uk-ua': 'Ця дія недоступна під час перегляду результатів завантаження', - 'de-ch': - 'Diese Aktion ist beim Anzeigen der Upload-Ergebnisse nicht verfügbar', - }, - unavailableWhileValidating: { - 'en-us': 'This action is unavailable while Data Check is in progress', - 'ru-ru': 'Это действие недоступно, пока выполняется проверка данных', - 'es-es': 'Acción no disponible durante la Comprobación de Datos', - 'fr-fr': ` - Cette action n'est pas disponible lorsque la vérification des données est - en cours - `, - 'uk-ua': 'Ця дія недоступна, поки триває перевірка даних', - 'de-ch': ` - Diese Aktion ist nicht verfügbar, während die Datenprüfung ausgeführt wird - `, - }, unavailableWithoutLocality: { 'en-us': 'This tool requires locality columns to be mapped', 'ru-ru': @@ -925,15 +875,6 @@ export const wbText = createDictionary({ 'uk-ua': 'Цей інструмент вимагає відображення стовпців місцевості', 'de-ch': 'Für dieses Tool müssen Ortsspalten zugeordnet werden', }, - unavailableWhenUploaded: { - 'en-us': 'This tool does not work with uploaded Data Sets', - 'ru-ru': 'Этот инструмент не работает с загруженными наборами данных', - 'es-es': 'Esta herramienta no funciona con Conjuntos de Datos cargados', - 'fr-fr': - 'Cet outil ne fonctionne pas avec les ensembles de données téléchargés', - 'uk-ua': 'Цей інструмент не працює із завантаженими наборами даних', - 'de-ch': 'Dieses Tool funktioniert nicht mit hochgeladenen Datensätzen', - }, dataSetDeletedOrNotFound: { 'en-us': 'Data Set was deleted by another session.', 'ru-ru': 'Набор данных был удален другим сеансом.', From b1dc3cd1aa0df368c43df50c6e812e8f7acc8014 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 6 May 2024 16:12:50 -0400 Subject: [PATCH 098/153] re-add disambiguation fix --- .../WorkBench/useDisambiguationDialog.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx index a52f3089179..a24c6cda10c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx @@ -127,19 +127,20 @@ export function useDisambiguationDialog({ visualRow-- ) { const physicalRow = hot.toPhysicalRow(visualRow); - if ( - !validation.uploadResults.ambiguousMatches[physicalRow]?.find( + const ambiguousMatchToDisambiguate = + validation.uploadResults.ambiguousMatches[physicalRow]?.find( ({ key, mappingPath }) => key === disambiguationMatches!.key && typeof disambiguation.getDisambiguation(physicalRow)[ mappingPathToString(mappingPath) ] !== 'number' - ) - ) - continue; + ); + + if (ambiguousMatchToDisambiguate === undefined) continue; + disambiguation.setDisambiguation( physicalRow, - disambiguationMatches!.mappingPath, + ambiguousMatchToDisambiguate.mappingPath, selected.id ); validation.startValidateRow(physicalRow); From d9ff45b147b6a4ad2b22aaa4ccae08aa5452b84a Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 6 May 2024 17:49:15 -0400 Subject: [PATCH 099/153] add back missed strings --- .../lib/components/WbActions/WbUpload.tsx | 2 +- .../lib/components/WbActions/WbValidate.tsx | 22 +++++++++++-- .../js_src/lib/components/WbActions/index.tsx | 19 +++++++++-- .../lib/components/WbToolkit/ChangeOwner.tsx | 2 +- .../WbToolkit/CoordinateConverter.tsx | 16 ++++++++-- .../lib/components/WbToolkit/GeoLocate.tsx | 16 ++++++++-- .../js_src/lib/components/WbToolkit/index.tsx | 10 +++++- .../js_src/lib/components/WbUtils/index.tsx | 6 +++- .../lib/components/WorkBench/WbView.tsx | 2 ++ .../js_src/lib/localization/workbench.ts | 32 +++++++++++++++++++ 10 files changed, 115 insertions(+), 12 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx index 4af08318c90..c9d8dff3b4b 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx @@ -47,7 +47,7 @@ export function WbUpload({ ? wbText.unavailableWhileEditing() : cellCounts.invalidCells > 0 ? wbText.uploadUnavailableWhileHasErrors() - : '' + : undefined } onClick={handleUpload} > diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index 43d61b2397e..d3ddcb69abc 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -12,10 +12,14 @@ export function WbValidate({ hasUnsavedChanges, startUpload, validation, + isMapped, + isResultsOpen, }: { readonly hasUnsavedChanges: boolean; readonly startUpload: (mode: WbStatus) => void; readonly validation: WbValidation; + readonly isMapped: boolean; + readonly isResultsOpen: boolean; }): JSX.Element { const [canLiveValidate] = userPreferences.use( 'workBench', @@ -35,6 +39,14 @@ export function WbValidate({ {isLiveValidateOn && validation.validationMode === 'live' ? validation.liveValidationStack.length > 0 @@ -48,8 +60,14 @@ export function WbValidate({ ) : undefined} {wbText.validate()} diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index eb7ea22de24..5604f4864d4 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -67,6 +67,13 @@ export function WbActions({ const message = mode === undefined ? undefined : getMessage(cellCounts, mode); + const isMapped = mappings !== undefined; + + const dataCheckInProgress = React.useMemo( + () => workbench.validation.liveValidationStack.length > 0, + [workbench.validation.liveValidationStack.length] + ); + return ( <> ) : undefined} @@ -90,8 +99,14 @@ export function WbActions({ {commonText.results()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx index 453068aa112..e6743b2e691 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/ChangeOwner.tsx @@ -35,7 +35,7 @@ export function WbChangeOwner({ aria-haspopup="dialog" aria-pressed={showChangeOwner} disabled={hasUnsavedChanges} - title={hasUnsavedChanges ? wbText.unavailableWhileEditing() : ''} + title={hasUnsavedChanges ? wbText.unavailableWhileEditing() : undefined} onClick={openChangeOwner} > {wbText.changeOwner()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 4fe52feff18..090298457ba 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -27,12 +27,16 @@ export function WbConvertCoordinates({ data, mappings, hot, + isUploaded, + isResultsOpen, }: { readonly hasLocality: boolean; readonly dataset: Dataset; readonly data: RA>; readonly mappings: WbMapping | undefined; readonly hot: Handsontable; + readonly isUploaded: boolean; + readonly isResultsOpen: boolean; }): JSX.Element { const [showConvertCoords, openConvertCoords, closeConvertCoords] = useBooleanState(); @@ -41,8 +45,16 @@ export function WbConvertCoordinates({ {wbText.convertCoordinates()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index 9b3fbbbdc29..f7e884bd1a0 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -29,11 +29,15 @@ export function WbGeoLocate({ hot, dataset, mappings, + isUploaded, + isResultsOpen, }: { readonly hasLocality: boolean; readonly hot: Handsontable; readonly dataset: Dataset; readonly mappings: WbMapping | undefined; + readonly isUploaded: boolean; + readonly isResultsOpen: boolean; }): JSX.Element { const [showGeoLocate, openGeoLocate, closeGeoLocate] = useBooleanState(); return ( @@ -41,8 +45,16 @@ export function WbGeoLocate({ {localityText.geoLocate()} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx index 68a211fb1f0..fd33140abf8 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx @@ -22,6 +22,8 @@ export function WbToolkit({ dataset, hot, mappings, + isUploaded, + isResultsOpen, data, onDatasetDeleted: handleDatasetDeleted, hasUnsavedChanges, @@ -31,6 +33,8 @@ export function WbToolkit({ readonly hot: Handsontable; readonly mappings: WbMapping | undefined; readonly data: RA>; + readonly isUploaded: boolean; + readonly isResultsOpen: boolean; readonly onDatasetDeleted: () => void; readonly hasUnsavedChanges: boolean; readonly triggerDatasetRefresh: () => void; @@ -77,7 +81,7 @@ export function WbToolkit({ {commonText.export()} @@ -92,6 +96,8 @@ export function WbToolkit({ hasLocality={hasLocality} hot={hot} mappings={mappings} + isResultsOpen={isResultsOpen} + isUploaded={isUploaded} /> @@ -100,6 +106,8 @@ export function WbToolkit({ hasLocality={hasLocality} hot={hot} mappings={mappings} + isResultsOpen={isResultsOpen} + isUploaded={isUploaded} /> diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx index 5962ffce71d..9a44fe89ee6 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -61,7 +61,11 @@ export function WbUtilsComponent({ disabled={isReadOnly} forwardRef={replaceRef} placeholder={wbText.replace()} - title={wbText.replacementValue()} + title={ + isReadOnly + ? wbText.unavailableWhileViewingResults() + : wbText.replacementValue() + } type="search" onKeyDown={(event) => utils.replaceCells(event, replaceRef.current) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index b208ae2b383..07163bd7423 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -212,6 +212,8 @@ export function WbView({ hasUnsavedChanges={hasUnsavedChanges} hot={hot} mappings={mappings} + isResultsOpen={showResults} + isUploaded={isUploaded} triggerDatasetRefresh={triggerDatasetRefresh} onDatasetDeleted={handleDatasetDeleted} /> diff --git a/specifyweb/frontend/js_src/lib/localization/workbench.ts b/specifyweb/frontend/js_src/lib/localization/workbench.ts index 42a1fb93245..f734419944e 100644 --- a/specifyweb/frontend/js_src/lib/localization/workbench.ts +++ b/specifyweb/frontend/js_src/lib/localization/workbench.ts @@ -847,6 +847,29 @@ export const wbText = createDictionary({ 'uk-ua': 'Набір даних має бути перевірений або завантажений', 'de-ch': 'Der Datensatz muss validiert oder hochgeladen werden', }, + wbValidateUnavailable: { + 'en-us': + 'An Upload Plan needs to defined before this Data Set can be Validated', + 'ru-ru': ` + План загрузки должен быть определен до того, как этот набор данных может + быть проверен + `, + 'es-es': ` + Se necesita definir un Plan de Carga antes de poder Validar este Conjunto + de Datos + `, + 'fr-fr': ` + Un plan de téléchargement doit être défini avant que cet ensemble de + données puisse être validé + `, + 'uk-ua': ` + Перед перевіркою цього набору даних необхідно визначити план завантаження + `, + 'de-ch': ` + Bevor dieser Datensatz validiert werden kann, muss ein Upload-Plan + definiert werden + `, + }, unavailableWhileEditing: { 'en-us': 'This action requires all changes to be saved', 'ru-ru': 'Это действие требует сохранения всех изменений', @@ -915,6 +938,15 @@ export const wbText = createDictionary({ 'uk-ua': 'Цей інструмент вимагає відображення стовпців місцевості', 'de-ch': 'Für dieses Tool müssen Ortsspalten zugeordnet werden', }, + unavailableWhenUploaded: { + 'en-us': 'This tool does not work with uploaded Data Sets', + 'ru-ru': 'Этот инструмент не работает с загруженными наборами данных', + 'es-es': 'Esta herramienta no funciona con Conjuntos de Datos cargados', + 'fr-fr': + 'Cet outil ne fonctionne pas avec les ensembles de données téléchargés', + 'uk-ua': 'Цей інструмент не працює із завантаженими наборами даних', + 'de-ch': 'Dieses Tool funktioniert nicht mit hochgeladenen Datensätzen', + }, dataSetDeletedOrNotFound: { 'en-us': 'Data Set was deleted by another session.', 'ru-ru': 'Набор данных был удален другим сеансом.', From 2489249e0a5e1a9eea2a7270fe3e9748fe077683 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 6 May 2024 17:56:09 -0400 Subject: [PATCH 100/153] lint --- .../js_src/lib/components/WbActions/WbValidate.tsx | 14 +++++++------- .../js_src/lib/components/WbActions/index.tsx | 10 +++++----- .../components/WbToolkit/CoordinateConverter.tsx | 6 +++--- .../js_src/lib/components/WbToolkit/GeoLocate.tsx | 6 +++--- .../js_src/lib/components/WbToolkit/index.tsx | 4 ++-- .../js_src/lib/components/WorkBench/WbView.tsx | 2 +- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index d3ddcb69abc..e4907eaeb70 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -38,15 +38,15 @@ export function WbValidate({ {canLiveValidate ? ( {isLiveValidateOn && validation.validationMode === 'live' ? validation.liveValidationStack.length > 0 @@ -64,9 +64,9 @@ export function WbValidate({ title={ hasUnsavedChanges ? wbText.unavailableWhileEditing() - : !isMapped - ? wbText.wbValidateUnavailable() - : undefined + : isMapped + ? undefined + : wbText.wbValidateUnavailable() } onClick={handleValidate} > diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 5604f4864d4..699b76739a1 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -88,10 +88,10 @@ export function WbActions({ ) : undefined} @@ -103,9 +103,9 @@ export function WbActions({ title={ dataCheckInProgress ? wbText.unavailableWhileValidating() - : !isUploaded - ? wbText.wbUploadedUnavailable() - : undefined + : isUploaded + ? undefined + : wbText.wbUploadedUnavailable() } onClick={handleToggleResults} > diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 090298457ba..9dbadbfb839 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -47,13 +47,13 @@ export function WbConvertCoordinates({ aria-pressed={showConvertCoords} disabled={!hasLocality || isResultsOpen || isUploaded} title={ - !hasLocality - ? wbText.unavailableWithoutLocality() - : isUploaded + hasLocality + ? isUploaded ? wbText.unavailableWhenUploaded() : isResultsOpen ? wbText.unavailableWhileViewingResults() : undefined + : wbText.unavailableWithoutLocality() } onClick={openConvertCoords} > diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index f7e884bd1a0..555b61939bf 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -47,13 +47,13 @@ export function WbGeoLocate({ aria-pressed={showGeoLocate} disabled={!hasLocality || isResultsOpen || isUploaded} title={ - !hasLocality - ? wbText.unavailableWithoutLocality() - : isUploaded + hasLocality + ? isUploaded ? wbText.unavailableWhenUploaded() : isResultsOpen ? wbText.unavailableWhileViewingResults() : undefined + : wbText.unavailableWithoutLocality() } onClick={openGeoLocate} > diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx index fd33140abf8..7186f342011 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/index.tsx @@ -95,9 +95,9 @@ export function WbToolkit({ dataset={dataset} hasLocality={hasLocality} hot={hot} - mappings={mappings} isResultsOpen={isResultsOpen} isUploaded={isUploaded} + mappings={mappings} /> @@ -105,9 +105,9 @@ export function WbToolkit({ dataset={dataset} hasLocality={hasLocality} hot={hot} - mappings={mappings} isResultsOpen={isResultsOpen} isUploaded={isUploaded} + mappings={mappings} /> diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 07163bd7423..a6a238b6225 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -211,9 +211,9 @@ export function WbView({ dataset={dataset} hasUnsavedChanges={hasUnsavedChanges} hot={hot} - mappings={mappings} isResultsOpen={showResults} isUploaded={isUploaded} + mappings={mappings} triggerDatasetRefresh={triggerDatasetRefresh} onDatasetDeleted={handleDatasetDeleted} /> From 3d320f0312b5edc23f4bddba83479ca317cef1b8 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 6 May 2024 21:59:56 +0000 Subject: [PATCH 101/153] Lint code with ESLint and Prettier Triggered by 2489249e0a5e1a9eea2a7270fe3e9748fe077683 on branch refs/heads/issue-4611 --- .../js_src/lib/components/WbActions/WbValidate.tsx | 4 ++-- .../lib/components/WbToolkit/CoordinateConverter.tsx | 8 ++++---- .../js_src/lib/components/WbToolkit/GeoLocate.tsx | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index e4907eaeb70..76d73e995c9 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -42,8 +42,8 @@ export function WbValidate({ title={ isMapped ? isResultsOpen - ? wbText.unavailableWhileViewingResults() - : undefined + ? wbText.unavailableWhileViewingResults() + : undefined : wbText.wbValidateUnavailable() } onClick={handleToggleDataCheck} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 9dbadbfb839..4b6445793f3 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -49,10 +49,10 @@ export function WbConvertCoordinates({ title={ hasLocality ? isUploaded - ? wbText.unavailableWhenUploaded() - : isResultsOpen - ? wbText.unavailableWhileViewingResults() - : undefined + ? wbText.unavailableWhenUploaded() + : isResultsOpen + ? wbText.unavailableWhileViewingResults() + : undefined : wbText.unavailableWithoutLocality() } onClick={openConvertCoords} diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx index 555b61939bf..5665ffbaebc 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/GeoLocate.tsx @@ -49,10 +49,10 @@ export function WbGeoLocate({ title={ hasLocality ? isUploaded - ? wbText.unavailableWhenUploaded() - : isResultsOpen - ? wbText.unavailableWhileViewingResults() - : undefined + ? wbText.unavailableWhenUploaded() + : isResultsOpen + ? wbText.unavailableWhileViewingResults() + : undefined : wbText.unavailableWithoutLocality() } onClick={openGeoLocate} From b0f18a0112d9c2a696b53d7698dae672d8afae83 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Wed, 8 May 2024 14:02:43 -0400 Subject: [PATCH 102/153] Override readonly context set by upload - Allows record set meta to be edited after dataset upload --- .../lib/components/WorkBench/RecordSet.tsx | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx index f6d5f4653be..6e33a396998 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/RecordSet.tsx @@ -6,7 +6,7 @@ import { wbText } from '../../localization/workbench'; import { ajax } from '../../utils/ajax'; import { formData } from '../../utils/ajax/helpers'; import { Button } from '../Atoms/Button'; -import { LoadingContext } from '../Core/Contexts'; +import { LoadingContext, ReadOnlyContext } from '../Core/Contexts'; import { tables } from '../DataModel/tables'; import { ProtectedAction, @@ -70,21 +70,26 @@ function CreateRecordSetDialog({ const loading = React.useContext(LoadingContext); return ( - { - unsetUnloadProtect(); - loading( - ajax(`/api/workbench/create_recordset/${datasetId}/`, { - method: 'POST', - headers: { Accept: 'application/json' }, - body: formData({ name: recordSet.get('name') }), - errorMode: 'dismissible', - }).then(({ data }) => unsafeNavigate(`/specify/record-set/${data}/`)) - ); - return false; - }} - /> + // Override readonly context set by workbench after upload so recordset meta can be edited + + { + unsetUnloadProtect(); + loading( + ajax(`/api/workbench/create_recordset/${datasetId}/`, { + method: 'POST', + headers: { Accept: 'application/json' }, + body: formData({ name: recordSet.get('name') }), + errorMode: 'dismissible', + }).then(({ data }) => + unsafeNavigate(`/specify/record-set/${data}/`) + ) + ); + return false; + }} + /> + ); } From 85a1e67a3e8bf1d824c8fe8666fd42351511f1f5 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 10 May 2024 13:47:49 -0400 Subject: [PATCH 103/153] Improve validation performance - Batch hot renders when validating --- .../js_src/lib/components/WorkBench/WbValidation.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index 648ee577d48..dbbadc051f5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -371,8 +371,10 @@ export class WbValidation { return; } - this.workbench.dataset.rowresults.forEach((result, physicalRow) => { - this.applyRowValidationResults(physicalRow, result); + this.workbench.hot?.batch(() => { + this.workbench.dataset.rowresults?.forEach((result, physicalRow) => { + this.applyRowValidationResults(physicalRow, result); + }); }); this.workbench.cells?.updateCellInfoStats(); From 7ee6fc755eefc57e934c822b8b0b926881d0b944 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 13 May 2024 12:44:27 -0400 Subject: [PATCH 104/153] Fix handsontable text colors - makes the colors same as edge --- specifyweb/frontend/js_src/css/workbench.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/css/workbench.css b/specifyweb/frontend/js_src/css/workbench.css index 331dddfa985..f7839c71071 100644 --- a/specifyweb/frontend/js_src/css/workbench.css +++ b/specifyweb/frontend/js_src/css/workbench.css @@ -82,7 +82,7 @@ * See: https://github.com/handsontable/handsontable/pull/8681 */ .handsontable { - @apply text-black dark:text-white; + @apply text-inherit; font-family: inherit; font-size: inherit; } From 5497c8524374a3370d1a41beb2d835a847dab888 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 13 May 2024 16:01:36 -0400 Subject: [PATCH 105/153] Fix error cells disappearing on load --- .../components/WorkBench/WbSpreadsheet.tsx | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 5ebea37b443..d0767e20f7a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -169,28 +169,27 @@ function WbSpreadsheetComponent({ React.useEffect(() => { if (hot === undefined) return; - (mappings === undefined - ? Promise.resolve({}) - : fetchWbPickLists(dataset.columns, mappings.tableNames, mappings.lines) - ).then((pickLists) => { - configureHandsontable(hot, mappings, dataset, pickLists); - // Check for reordered columns - if (dataset.visualorder?.some((column, index) => column !== index)) - hot.updateSettings({ - manualColumnMove: writable(dataset.visualorder), - }); + hot.batch(() => { + (mappings === undefined + ? Promise.resolve({}) + : fetchWbPickLists(dataset.columns, mappings.tableNames, mappings.lines) + ).then((pickLists) => { + configureHandsontable(hot, mappings, dataset, pickLists); + // Check for reordered columns + if (dataset.visualorder?.some((column, index) => column !== index)) + hot.updateSettings({ + manualColumnMove: writable(dataset.visualorder), + }); + // Highlight validation cells + if (dataset.rowresults) { + validation.getValidationResults(); + if (validation.validationMode === 'static' && !isUploaded) + workbench.utils.toggleCellTypes('invalidCells', 'remove'); + workbench.cells.indexedCellMeta = undefined; + } + }); }); - }, [hot]); - - // Highlight validation cells - React.useEffect(() => { - if (dataset.rowresults !== null && hot !== undefined) { - validation.getValidationResults(); - if (validation.validationMode === 'static' && !isUploaded) - workbench.utils.toggleCellTypes('invalidCells', 'remove'); - workbench.cells.indexedCellMeta = undefined; - } - }, [dataset.rowresults, hot]); + }, [hot, dataset.rowresults]); const { autoWrapCol, From a4a5a8a2fb38482be5dbb7dc395ba8c6ee3d2db4 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 13 May 2024 16:02:24 -0400 Subject: [PATCH 106/153] Fix default values and error cells disappearing when toggling results --- .../lib/components/WbActions/useResults.ts | 38 +++++++++++-------- .../lib/components/WorkBench/WbView.tsx | 1 + .../lib/components/WorkBench/handsontable.ts | 2 +- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts index 646ef42ef0e..3acf6413813 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -2,15 +2,17 @@ import type Handsontable from 'handsontable'; import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; -import { getHotPlugin } from '../WorkBench/handsontable'; +import { getHotPlugin, identifyDefaultValues } from '../WorkBench/handsontable'; import type { Workbench } from '../WorkBench/WbView'; export function useResults({ hot, workbench, + triggerDatasetRefresh, }: { readonly hot: Handsontable | undefined; readonly workbench: Workbench; + readonly triggerDatasetRefresh: () => void; }) { const [showResults, _, closeResults, toggleResults] = useBooleanState(); @@ -57,21 +59,27 @@ export function useResults({ ) .map(hot.toVisualColumn); - if (showResults) { - getHotPlugin(hot, 'hiddenRows').hideRows(rowsToHide); - getHotPlugin(hot, 'hiddenColumns').hideColumns(colsToHide); + hot.batch(() => { + if (showResults) { + identifyDefaultValues(hot, workbench.mappings); + getHotPlugin(hot, 'hiddenRows').hideRows(rowsToHide); + getHotPlugin(hot, 'hiddenColumns').hideColumns(colsToHide); - workbench.utils.toggleCellTypes('newCells', 'remove'); - } else { - getHotPlugin(hot, 'hiddenRows').showRows( - rowsToHide.filter((visualRow) => !initialHiddenRows.includes(visualRow)) - ); - getHotPlugin(hot, 'hiddenColumns').showColumns( - colsToHide.filter((visualCol) => !initialHiddenCols.includes(visualCol)) - ); - } - - hot.render(); + workbench.utils.toggleCellTypes('newCells', 'remove'); + } else { + getHotPlugin(hot, 'hiddenRows').showRows( + rowsToHide.filter( + (visualRow) => !initialHiddenRows.includes(visualRow) + ) + ); + getHotPlugin(hot, 'hiddenColumns').showColumns( + colsToHide.filter( + (visualCol) => !initialHiddenCols.includes(visualCol) + ) + ); + triggerDatasetRefresh(); + } + }); }, [showResults]); return { showResults, closeResults, toggleResults }; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index a6a238b6225..55332d7154c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -151,6 +151,7 @@ export function WbView({ const { showResults, closeResults, toggleResults } = useResults({ hot, workbench, + triggerDatasetRefresh, }); const { openDisambiguationDialog, disambiguationDialogs } = diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts index 392d24b0a4e..0ac6dfe3cda 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/handsontable.ts @@ -20,7 +20,7 @@ export function configureHandsontable( setSort(hot, dataset); } -function identifyDefaultValues( +export function identifyDefaultValues( hot: Handsontable, mappings: WbMapping | undefined ): void { From 298e0a8084e33aa53af544138b9b9dc53b862b0d Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 10:16:20 -0400 Subject: [PATCH 107/153] Fix search results highlighting becoming inverted --- .../js_src/lib/components/WbUtils/Navigation.tsx | 10 ++++++++-- .../js_src/lib/components/WbUtils/index.tsx | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx index 86564b7f9b7..dfee2cc6e9b 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx @@ -15,18 +15,24 @@ export function Navigation({ label, totalCount, utils, + isPressed = false, + onToggle: handleToggle, }: { readonly name: keyof WbCellCounts; readonly label: LocalizedString; readonly totalCount: number; readonly utils: WbUtils; + readonly isPressed?: boolean; + readonly onToggle?: () => void; }): JSX.Element { const isReadOnly = React.useContext(ReadOnlyContext); const [currentPosition, setCurrentPosition] = React.useState(0); - const [buttonIsPressed, _press, _unpress, togglePress] = useBooleanState(); + const [buttonIsPressed, _press, _unpress, togglePress] = + useBooleanState(isPressed); const handleTypeToggle = () => { - togglePress(); + if (typeof handleToggle === 'function') handleToggle(); + else togglePress(); utils.toggleCellTypes(name, 'toggle'); }; diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx index 9a44fe89ee6..c62bdb1e992 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import _ from 'underscore'; +import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; import { Input } from '../Atoms/Form'; @@ -29,6 +30,9 @@ export function WbUtilsComponent({ const isReadOnly = React.useContext(ReadOnlyContext); const replaceRef = React.useRef(null); + const [isSearchClicked, clickSearch, unclickSearch, toggleSearch] = + useBooleanState(true); + return (
) => - utils.searchCells(event, searchRef.current), + (event: React.KeyboardEvent) => { + if (searchRef.current && searchRef.current?.value.length > 0) + unclickSearch(); + else clickSearch(); + utils.searchCells(event, searchRef.current); + }, debounceRate, true )} @@ -97,10 +105,12 @@ export function WbUtilsComponent({ /> {!isUploaded && hasPermission('/workbench/dataset', 'update') ? ( Date: Thu, 16 May 2024 11:29:02 -0400 Subject: [PATCH 108/153] Rename rollback to confirmRollback --- .../frontend/js_src/lib/components/WbActions/WbRollback.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx index fb7d4016a3e..a9f9eaa6d08 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx @@ -16,7 +16,7 @@ export function WbRollback({ readonly datasetId: number; readonly triggerStatusComponent: (mode: WbStatus) => void; }): JSX.Element { - const [rollback, handleOpen, handleClose] = useBooleanState(); + const [confirmRollback, handleOpen, handleClose] = useBooleanState(); const handleRollback = () => triggerStatusComponent('unupload'); @@ -24,12 +24,12 @@ export function WbRollback({ <> {wbText.rollback()} - {rollback ? ( + {confirmRollback ? ( Date: Thu, 16 May 2024 12:21:21 -0400 Subject: [PATCH 109/153] improve imports Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- specifyweb/frontend/js_src/lib/components/WbActions/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 699b76739a1..1a4f1f39ec2 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -10,8 +10,7 @@ import { LoadingContext } from '../Core/Contexts'; import { ErrorBoundary } from '../Errors/ErrorBoundary'; import { Dialog } from '../Molecules/Dialog'; import { hasPermission } from '../Permissions/helpers'; -import type { Status } from '../WbPlanView/Wrapped'; -import type { Dataset } from '../WbPlanView/Wrapped'; +import type { Dataset, Status } from '../WbPlanView/Wrapped'; import type { WbCellCounts } from '../WorkBench/CellMeta'; import type { WbMapping } from '../WorkBench/mapping'; import { CreateRecordSetButton } from '../WorkBench/RecordSet'; From 45771ffb3c6a9afecff5a17c5825188b1138caa2 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:25:20 -0400 Subject: [PATCH 110/153] destructure useWbActions --- .../js_src/lib/components/WbActions/index.tsx | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 1a4f1f39ec2..418ca41c67e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -56,13 +56,14 @@ export function WbActions({ useBooleanState(); const [operationCompleted, openOperationCompleted, closeOperationCompleted] = useBooleanState(); - const { mode, refreshInitiatorAborted, ...actions } = useWbActions({ - datasetId: dataset.id, - onRefresh: handleRefresh, - checkDeletedFail, - onOpenStatus: openStatus, - workbench, - }); + const { mode, refreshInitiatorAborted, startUpload, triggerStatusComponent } = + useWbActions({ + datasetId: dataset.id, + onRefresh: handleRefresh, + checkDeletedFail, + onOpenStatus: openStatus, + workbench, + }); const message = mode === undefined ? undefined : getMessage(cellCounts, mode); @@ -89,7 +90,7 @@ export function WbActions({ hasUnsavedChanges={hasUnsavedChanges} isMapped={isMapped} isResultsOpen={isResultsOpen} - startUpload={actions.startUpload} + startUpload={startUpload} validation={workbench.validation} /> @@ -115,7 +116,7 @@ export function WbActions({ ) : undefined} @@ -126,7 +127,7 @@ export function WbActions({ hasUnsavedChanges={hasUnsavedChanges} mappings={mappings} openNoUploadPlan={openNoUploadPlan} - startUpload={actions.startUpload} + startUpload={startUpload} /> ) : undefined} From cdbe9e3eff6052f93f4857c0f43da6310f9f3ca7 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:35:12 -0400 Subject: [PATCH 111/153] remove extra promise chain --- .../frontend/js_src/lib/components/WbActions/index.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 418ca41c67e..979797856ab 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -250,12 +250,11 @@ function useWbActions({ ping(`/api/workbench/${newMode}/${datasetId}/`, { method: 'POST', expectedErrors: [Http.CONFLICT], + }).then((statusCode): void => { + checkDeletedFail(statusCode); + checkConflictFail(statusCode); + triggerStatusComponent(newMode); }) - .then((statusCode): void => { - checkDeletedFail(statusCode); - checkConflictFail(statusCode); - }) - .then(() => triggerStatusComponent(newMode)) ); }; From 720adba6db2d1c33d881e87e74d5a22762e99cca Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:35:22 -0400 Subject: [PATCH 112/153] add results return type --- .../js_src/lib/components/WbActions/useResults.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts index 3acf6413813..8dc04aa69a0 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -13,20 +13,24 @@ export function useResults({ readonly hot: Handsontable | undefined; readonly workbench: Workbench; readonly triggerDatasetRefresh: () => void; -}) { +}): { + readonly showResults: boolean; + readonly closeResults: () => void; + readonly toggleResults: () => void; +} { const [showResults, _, closeResults, toggleResults] = useBooleanState(); const initialHiddenRows = React.useMemo( () => hot === undefined ? [] : getHotPlugin(hot, 'hiddenRows').getHiddenRows(), - [hot] + [hot, workbench.dataset.columns] ); const initialHiddenCols = React.useMemo( () => hot === undefined ? [] : getHotPlugin(hot, 'hiddenColumns').getHiddenColumns(), - [hot] + [hot, workbench.dataset.columns] ); // Makes the hot changes required for upload view results From 25722189a572f3c7c21d1216f641ba09300bb5b4 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:36:00 -0400 Subject: [PATCH 113/153] add set type --- .../frontend/js_src/lib/components/WbActions/useResults.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts index 8dc04aa69a0..0317a2016fc 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -37,8 +37,8 @@ export function useResults({ React.useEffect(() => { if (hot === undefined) return; - const rowsToInclude = new Set(); - const colsToInclude = new Set(); + const rowsToInclude = new Set(); + const colsToInclude = new Set(); Object.entries(workbench.cells.cellMeta).forEach(([physicalRow, rowMeta]) => rowMeta.forEach((metaArray, physicalCol) => { if (!workbench.cells.getCellMetaFromArray(metaArray, 'isNew')) return; From 6049e058d0a6a006d67533583fb7bac5459c63c6 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:43:13 -0400 Subject: [PATCH 114/153] change ref variable name --- .../js_src/lib/components/WbToolkit/CoordinateConverter.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 4b6445793f3..6d54cecccb1 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -135,7 +135,7 @@ function CoordinateConverter({ >(undefined); const [showDirection, setShowDirection] = React.useState(false); - const changeCount = React.useRef(0); + const changeCountRef = React.useRef(0); // List of coordinate columns const columnsToWorkWith = React.useMemo( @@ -205,7 +205,7 @@ function CoordinateConverter({ return value !== data[physicalRow][physicalCol]; }); if (changes.length > 0) { - changeCount.current += 1; + changeCountRef.current += 1; setHotData(hot, changes); } }, [ @@ -231,7 +231,7 @@ function CoordinateConverter({ modal={false} onClose={(): void => { hot.batch(() => - Array.from({ length: changeCount.current }).forEach(() => hot.undo()) + Array.from({ length: changeCountRef.current }).forEach(() => hot.undo()) ); handleClose(); }} From b0c9d4195e36009b48d095da3574ab5ae01b3321 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:46:20 -0400 Subject: [PATCH 115/153] use triple equals --- .../frontend/js_src/lib/components/WbUtils/Navigation.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx index dfee2cc6e9b..41e1d815d9a 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx @@ -57,7 +57,7 @@ export function Navigation({ // Reset current position when total count resets React.useEffect(() => { - if (totalCount == 0) setCurrentPosition(0); + if (totalCount === 0) setCurrentPosition(0); }, [totalCount]); return ( From 8f2606b23d03d3c5ccc9d572610e43beead0011b Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:46:29 -0400 Subject: [PATCH 116/153] make constructor public --- specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index 215074a69aa..edf3133084f 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -29,7 +29,7 @@ export class WbUtils { // eslint-disable-next-line functional/prefer-readonly-type public searchPreferences: WbSearchPreferences = getInitialSearchPreferences(); - constructor( + public constructor( private readonly workbench: Workbench, private readonly spreadsheetContainerRef: React.RefObject ) {} From aadc396d33423dd8bebf4c556a26ca7f6eaa5e1c Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 12:58:44 -0400 Subject: [PATCH 117/153] improve function access modifiers --- .../js_src/lib/components/WbUtils/Utils.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index edf3133084f..fe03e320dca 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -34,7 +34,7 @@ export class WbUtils { private readonly spreadsheetContainerRef: React.RefObject ) {} - navigateCells({ + public navigateCells({ type, direction, currentCellPosition = 0, @@ -175,12 +175,12 @@ export class WbUtils { return [matchedCell, finalCellPosition]; } - searchCells( + public searchCells( event: | React.KeyboardEvent | { readonly key: 'SettingsChange' }, searchQueryElement: HTMLInputElement | null - ) { + ): void { if (this.workbench.hot === undefined || searchQueryElement === null) return; /* * Don't rerun search on live search if search query did not change @@ -277,7 +277,7 @@ export class WbUtils { }); } - parseSearchQuery(searchQueryElement: HTMLInputElement) { + private parseSearchQuery(searchQueryElement: HTMLInputElement) { if (searchQueryElement === null) return; this.rawSearchQuery = searchQueryElement.value; @@ -319,7 +319,7 @@ export class WbUtils { return this.searchQuery; } - toggleCellTypes( + public toggleCellTypes( navigationType: keyof WbCellCounts, action: 'add' | 'remove' | 'toggle' = 'toggle' ): void { @@ -330,7 +330,7 @@ export class WbUtils { spreadsheetContainer.classList[action](cssClassName); } - searchFunction(initialCellValue = '') { + public searchFunction(initialCellValue = ''): boolean { let cellValue = initialCellValue; if (this.searchQuery === undefined) return false; @@ -346,10 +346,10 @@ export class WbUtils { : cellValue.includes(this.searchQuery as string); } - replaceCells( + public replaceCells( event: React.KeyboardEvent, replacementValueElement: HTMLInputElement | null - ) { + ): void { if ( event.key !== 'Enter' || (this.searchPreferences.search.useRegex && From 90fd281ea7f641f0ac2c851ec50826a3dd59426a Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 14:46:58 -0400 Subject: [PATCH 118/153] Remove extra loading indicator in save --- .../lib/components/WbActions/WbSave.tsx | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index 815614baead..58af512769f 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -8,7 +8,6 @@ import { ping } from '../../utils/ajax/ping'; import { overwriteReadOnly } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { className } from '../Atoms/className'; -import { LoadingContext } from '../Core/Contexts'; import { loadingBar } from '../Molecules'; import { Dialog } from '../Molecules/Dialog'; import type { Workbench } from '../WorkBench/WbView'; @@ -28,7 +27,6 @@ export function WbSave({ }): JSX.Element { const [showProgressBar, openProgressBar, closeProgressBar] = useBooleanState(); - const loading = React.useContext(LoadingContext); const handleSave = () => { // Clear validation @@ -39,24 +37,22 @@ export function WbSave({ openProgressBar(); // Send data - loading( - ping(`/api/workbench/rows/${workbench.dataset.id}/`, { - method: 'PUT', - body: workbench.data, - expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], - }) - .then((status) => checkDeletedFail(status)) - .then(() => { - handleSpreadsheetUpToDate(); - workbench.cells.cellMeta = []; - workbench.utils?.searchCells( - { key: 'SettingsChange' }, - searchRef.current - ); - workbench.hot?.render(); - closeProgressBar(); - }) - ); + ping(`/api/workbench/rows/${workbench.dataset.id}/`, { + method: 'PUT', + body: workbench.data, + expectedErrors: [Http.NO_CONTENT, Http.NOT_FOUND], + }) + .then((status) => checkDeletedFail(status)) + .then(() => { + handleSpreadsheetUpToDate(); + workbench.cells.cellMeta = []; + workbench.utils?.searchCells( + { key: 'SettingsChange' }, + searchRef.current + ); + workbench.hot?.render(); + closeProgressBar(); + }); }; return ( From 4a43a97006ba8a63f57d6b56ed0a2f2c81579297 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 14:49:19 -0400 Subject: [PATCH 119/153] Add return type to useWbActions --- .../frontend/js_src/lib/components/WbActions/index.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 979797856ab..7c1b28e5394 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -239,7 +239,12 @@ function useWbActions({ readonly checkDeletedFail: (statusCode: number) => void; readonly onRefresh: () => void; readonly onOpenStatus: () => void; -}) { +}): { + readonly mode: WbStatus | undefined; + readonly refreshInitiatorAborted: React.MutableRefObject; + readonly startUpload: (newMode: WbStatus) => void; + readonly triggerStatusComponent: (newMode: WbStatus) => void; +} { const [mode, setMode] = React.useState(undefined); const refreshInitiatorAborted = React.useRef(false); const loading = React.useContext(LoadingContext); From bf771357723b85208fa4c00e7dcc90c091b21b58 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 14:50:35 -0400 Subject: [PATCH 120/153] Add return type to getMessage --- .../frontend/js_src/lib/components/WbActions/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 7c1b28e5394..0ade275bad0 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -22,6 +22,7 @@ import { WbRollback } from './WbRollback'; import { WbSave } from './WbSave'; import { WbUpload } from './WbUpload'; import { WbValidate } from './WbValidate'; +import { LocalizedString } from 'typesafe-i18n'; export function WbActions({ dataset, @@ -286,7 +287,13 @@ function useWbActions({ }; } -function getMessage(cellCounts: WbCellCounts, mode: WbStatus) { +function getMessage( + cellCounts: WbCellCounts, + mode: WbStatus +): { + readonly header: LocalizedString; + readonly message: JSX.Element | LocalizedString; +} { const messages = { validate: cellCounts.invalidCells === 0 From fc2c0d191b5cd98f4595451429964e0be2ce5e8c Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:02:30 -0400 Subject: [PATCH 121/153] remove magic number in utils --- .../frontend/js_src/lib/components/WbUtils/Utils.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index fe03e320dca..7dc1900a776 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -18,6 +18,8 @@ import { getHotPlugin } from '../WorkBench/handsontable'; import { getSelectedLast } from '../WorkBench/hotHelpers'; import type { Workbench } from '../WorkBench/WbView'; +const HOT_OFFSET = 3; + /* eslint-disable functional/no-this-expression */ export class WbUtils { // eslint-disable-next-line functional/prefer-readonly-type @@ -204,19 +206,19 @@ export class WbUtils { const data = this.workbench.dataset.rows; const firstVisibleRow = - getHotPlugin(this.workbench.hot, 'autoRowSize').getFirstVisibleRow() - 3; + getHotPlugin(this.workbench.hot, 'autoRowSize').getFirstVisibleRow() - HOT_OFFSET; const lastVisibleRow = - getHotPlugin(this.workbench.hot, 'autoRowSize').getLastVisibleRow() + 3; + getHotPlugin(this.workbench.hot, 'autoRowSize').getLastVisibleRow() + HOT_OFFSET; const firstVisibleColumn = getHotPlugin( this.workbench.hot, 'autoColumnSize' - ).getFirstVisibleColumn() - 3; + ).getFirstVisibleColumn() - HOT_OFFSET; const lastVisibleColumn = getHotPlugin( this.workbench.hot, 'autoColumnSize' - ).getLastVisibleColumn() + 3; + ).getLastVisibleColumn() + HOT_OFFSET; for (let visualRow = 0; visualRow < data.length; visualRow++) { const physicalRow = this.workbench.hot.toPhysicalRow(visualRow); From 7c1f270b9bb0b841cf4b376d52243e2e2d9c6ff3 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:08:31 -0400 Subject: [PATCH 122/153] make conditional functional call clearer --- .../frontend/js_src/lib/components/WbUtils/Utils.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index 7dc1900a776..bc9f57637a0 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -206,9 +206,11 @@ export class WbUtils { const data = this.workbench.dataset.rows; const firstVisibleRow = - getHotPlugin(this.workbench.hot, 'autoRowSize').getFirstVisibleRow() - HOT_OFFSET; + getHotPlugin(this.workbench.hot, 'autoRowSize').getFirstVisibleRow() - + HOT_OFFSET; const lastVisibleRow = - getHotPlugin(this.workbench.hot, 'autoRowSize').getLastVisibleRow() + HOT_OFFSET; + getHotPlugin(this.workbench.hot, 'autoRowSize').getLastVisibleRow() + + HOT_OFFSET; const firstVisibleColumn = getHotPlugin( this.workbench.hot, @@ -253,7 +255,10 @@ export class WbUtils { render = Boolean(cell); } - this.workbench.cells[render ? 'updateCellMeta' : 'setCellMeta']( + const updateWorkbenchCells = + this.workbench.cells[render ? 'updateCellMeta' : 'setCellMeta']; + + updateWorkbenchCells( physicalRow, physicalCol, 'isSearchResult', From 1dc80c83e4b856a779a525a07666555d9cdcda86 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:10:48 -0400 Subject: [PATCH 123/153] cleaner condition --- .../WorkBench/useDisambiguationDialog.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx index a24c6cda10c..f8df6836892 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx @@ -136,14 +136,14 @@ export function useDisambiguationDialog({ ] !== 'number' ); - if (ambiguousMatchToDisambiguate === undefined) continue; - - disambiguation.setDisambiguation( - physicalRow, - ambiguousMatchToDisambiguate.mappingPath, - selected.id - ); - validation.startValidateRow(physicalRow); + if (ambiguousMatchToDisambiguate !== undefined) { + disambiguation.setDisambiguation( + physicalRow, + ambiguousMatchToDisambiguate.mappingPath, + selected.id + ); + validation.startValidateRow(physicalRow); + } } }) } From 355c99d216cd4c826ac783ac2b81c6e2ffbf81c0 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:11:56 -0400 Subject: [PATCH 124/153] add return type to hook --- .../lib/components/WorkBench/useDisambiguationDialog.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx index f8df6836892..6272bb7090b 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/useDisambiguationDialog.tsx @@ -39,7 +39,10 @@ export function useDisambiguationDialog({ readonly disambiguation: Disambiguation; readonly mappings: WbMapping | undefined; readonly hot: Handsontable | undefined; -}) { +}): { + readonly openDisambiguationDialog: () => void; + readonly disambiguationDialogs: JSX.Element; +} { const [disambiguationMatches, setDisambiguationMatches] = React.useState(); const [disambiguationPhysicalRow, setPhysicalRow] = React.useState(); From b7da4b4fa53b2fe6be67b61eb0a54025f9083961 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:12:56 -0400 Subject: [PATCH 125/153] rename getHotProps --- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 4 ++-- .../frontend/js_src/lib/components/WorkBench/hotProps.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index d0767e20f7a..80742d301a0 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -22,7 +22,7 @@ import type { Dataset } from '../WbPlanView/Wrapped'; import { configureHandsontable } from './handsontable'; import { getHotHooks } from './hooks'; import { getSelectedRegions } from './hotHelpers'; -import { getHotProps } from './hotProps'; +import { useHotProps } from './hotProps'; import type { WbMapping } from './mapping'; import { fetchWbPickLists } from './pickLists'; import type { Workbench } from './WbView'; @@ -203,7 +203,7 @@ function WbSpreadsheetComponent({ minSpareRows, tabMoves, comments, - } = getHotProps({ dataset, mappings, physicalColToMappingCol }); + } = useHotProps({ dataset, mappings, physicalColToMappingCol }); const hooks = getHotHooks( workbench, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx index fae3638ad3f..e91763f109c 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hotProps.tsx @@ -18,7 +18,7 @@ const hiddenRows = { copyPasteEnabled: false, }; -export function getHotProps({ +export function useHotProps({ dataset, mappings, physicalColToMappingCol, From ce206152c05d70419a22852f98b1df2d42980b05 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:15:05 -0400 Subject: [PATCH 126/153] rename getHotHooks --- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 4 ++-- specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 80742d301a0..9a73eec906e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -20,7 +20,7 @@ import { strictGetTable } from '../DataModel/tables'; import { getIcon, unknownIcon } from '../InitialContext/icons'; import type { Dataset } from '../WbPlanView/Wrapped'; import { configureHandsontable } from './handsontable'; -import { getHotHooks } from './hooks'; +import { useHotHooks } from './hooks'; import { getSelectedRegions } from './hotHelpers'; import { useHotProps } from './hotProps'; import type { WbMapping } from './mapping'; @@ -205,7 +205,7 @@ function WbSpreadsheetComponent({ comments, } = useHotProps({ dataset, mappings, physicalColToMappingCol }); - const hooks = getHotHooks( + const hooks = useHotHooks( workbench, physicalColToMappingCol, spreadsheetChanged, diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 679746b0c09..38109aa4de6 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -17,7 +17,7 @@ import { schema } from '../DataModel/schema'; import { getHotPlugin } from './handsontable'; import type { Workbench } from './WbView'; -export function getHotHooks( +export function useHotHooks( workbench: Workbench, physicalColToMappingCol: (physicalCol: number) => number | undefined, spreadsheetChanged: () => void, From 7a32dc5c3cbd90a6bac9eedc63f5df4816758a12 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Thu, 16 May 2024 16:22:11 -0400 Subject: [PATCH 127/153] remove ternary in WbSave Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- .../frontend/js_src/lib/components/WbActions/WbSave.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx index 58af512769f..0b05fe8fa61 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbSave.tsx @@ -65,7 +65,7 @@ export function WbSave({ > {commonText.save()} - {showProgressBar ? ( + {showProgressBar && ( {loadingBar} - ) : undefined} + )} ); } From fa58922595f6947074bb83c9cad76301c9100ed0 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Thu, 16 May 2024 16:22:47 -0400 Subject: [PATCH 128/153] remove ternary in WbNoUploadPlan Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- .../js_src/lib/components/WbActions/WbNoUploadPlan.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx index 7cbf7c099e9..9e3cc357c91 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbNoUploadPlan.tsx @@ -35,7 +35,7 @@ export function WbNoUploadPlan({ return ( <> - {noUploadPlan ? ( + {noUploadPlan && ( @@ -50,7 +50,7 @@ export function WbNoUploadPlan({ > {wbPlanText.noUploadPlanDescription()} - ) : undefined} + )} ); } From 4a0f0a64585caf8317b36e5e02e2bb55f49a9802 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Thu, 16 May 2024 16:23:38 -0400 Subject: [PATCH 129/153] remove ternary in WbUpload Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- .../frontend/js_src/lib/components/WbActions/WbUpload.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx index c9d8dff3b4b..4823c588881 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbUpload.tsx @@ -53,7 +53,7 @@ export function WbUpload({ > {wbText.upload()} - {showUpload ? ( + {showUpload && ( @@ -68,7 +68,7 @@ export function WbUpload({ > {wbText.startUploadDescription()} - ) : undefined} + )} ); } From 2526c6933d8e751fd3c4ff6e983aa65378bc7258 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Thu, 16 May 2024 16:24:10 -0400 Subject: [PATCH 130/153] make triggerDatasetRefresh a one-liner Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx index 1344bd3097d..5bb210a296f 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/index.tsx @@ -32,9 +32,8 @@ export function WorkBench(): JSX.Element { if (dataset === undefined || !treeRanksLoaded || dataset.id !== datasetId) return ; - const triggerDatasetRefresh = () => { + const triggerDatasetRefresh = () => loading(fetchDataset(dataset.id).then(setDataset)); - }; return datasetId === undefined ? ( From 5e77c097a3f8037370c745715c3827df68fff4d3 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Thu, 16 May 2024 16:24:45 -0400 Subject: [PATCH 131/153] remove ternary for live validate Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- .../frontend/js_src/lib/components/WbActions/WbValidate.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx index 76d73e995c9..5398e16c159 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbValidate.tsx @@ -35,7 +35,7 @@ export function WbValidate({ return ( <> - {canLiveValidate ? ( + {canLiveValidate && ( - ) : undefined} + )} Date: Thu, 16 May 2024 16:33:24 -0400 Subject: [PATCH 132/153] use StringToJsx --- .../js_src/lib/components/WbUtils/Navigation.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx index 41e1d815d9a..cd2e0f69eed 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Navigation.tsx @@ -9,6 +9,8 @@ import { icons } from '../Atoms/Icons'; import { ReadOnlyContext } from '../Core/Contexts'; import type { WbCellCounts } from '../WorkBench/CellMeta'; import type { WbUtils } from './Utils'; +import { StringToJsx } from '../../localization/utils'; +import { localized } from '../../utils/types'; export function Navigation({ name, @@ -87,8 +89,15 @@ export function Navigation({ variant="bg-inherit text-gray-800 dark:text-gray-100" onClick={handleTypeToggle} > - {label} ({currentPosition}/ - {totalCount}) + {currentPosition} + ), + totalCount: {totalCount}, + }} + string={localized(`${label} (/)`)} + /> Date: Thu, 16 May 2024 16:34:37 -0400 Subject: [PATCH 133/153] code golf for spreadsheetContainer Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index bc9f57637a0..2466f50a0ef 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -332,9 +332,7 @@ export class WbUtils { ): void { const groupName = camelToKebab(navigationType); const cssClassName = `wb-hide-${groupName}`; - const { current: spreadsheetContainer } = this.spreadsheetContainerRef; - if (spreadsheetContainer) - spreadsheetContainer.classList[action](cssClassName); + this.spreadsheetContainerRef?.current?.classList[action](cssClassName); } public searchFunction(initialCellValue = ''): boolean { From 214587ffce587421ce5a26f836f91d865611c5ca Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:41:33 -0400 Subject: [PATCH 134/153] use setHotData helper --- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 9a73eec906e..6e07a8e8d88 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -21,7 +21,7 @@ import { getIcon, unknownIcon } from '../InitialContext/icons'; import type { Dataset } from '../WbPlanView/Wrapped'; import { configureHandsontable } from './handsontable'; import { useHotHooks } from './hooks'; -import { getSelectedRegions } from './hotHelpers'; +import { getSelectedRegions, setHotData } from './hotHelpers'; import { useHotProps } from './hotProps'; import type { WbMapping } from './mapping'; import { fetchWbPickLists } from './pickLists'; @@ -275,7 +275,8 @@ const fillCellsContextMenuItem = ( mode === 'up' ? hot.getDataAtCell(endRow, col) : hot.getDataAtCell(startRow, col); - hot.setDataAtCell( + setHotData( + hot, Array.from({ length: endRow - startRow }, (_, index) => [ startRow + index + 1, col, From 13bafa907ac914c03f337669170ca8508b4d620d Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:51:21 -0400 Subject: [PATCH 135/153] allow reordering columns on upload --- .../js_src/lib/components/WorkBench/hooks.ts | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 38109aa4de6..ba08786b129 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -17,13 +17,21 @@ import { schema } from '../DataModel/schema'; import { getHotPlugin } from './handsontable'; import type { Workbench } from './WbView'; -export function useHotHooks( - workbench: Workbench, - physicalColToMappingCol: (physicalCol: number) => number | undefined, - spreadsheetChanged: () => void, - checkDeletedFail: (statusCode: number) => boolean, - isReadOnly: boolean -): Partial { +export function useHotHooks({ + workbench, + physicalColToMappingCol, + spreadsheetChanged, + checkDeletedFail, + isReadOnly, + isResultsOpen, +}: { + readonly workbench: Workbench; + readonly physicalColToMappingCol: (physicalCol: number) => number | undefined; + readonly spreadsheetChanged: () => void; + readonly checkDeletedFail: (statusCode: number) => boolean; + readonly isReadOnly: boolean; + readonly isResultsOpen: boolean; +}): Partial { let sortConfigIsSet: boolean = false; const loading = React.useContext(LoadingContext); @@ -464,10 +472,7 @@ export function useHotHooks( }, beforeColumnMove: (_columnIndexes, _finalIndex, dropIndex) => - // Don't allow moving columns when isReadOnly - !isReadOnly && - // An ugly fix for jQuery's dialogs conflicting with HOT - (dropIndex !== undefined || workbench.hot !== undefined), + dropIndex !== undefined || workbench.hot !== undefined, // Save new visualOrder on the back end afterColumnMove: (_columnIndexes, _finalIndex, dropIndex) => { From 687fd7ad3825b177ca61295091a3183679828eba Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:56:40 -0400 Subject: [PATCH 136/153] block column reorder in results view --- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 9 ++++++--- .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 1 + .../frontend/js_src/lib/components/WorkBench/hooks.ts | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 6e07a8e8d88..4faab25b3bf 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -37,6 +37,7 @@ function WbSpreadsheetComponent({ data, workbench, mappings, + isResultsOpen, checkDeletedFail, spreadsheetChanged, onClickDisambiguate: handleClickDisambiguate, @@ -48,6 +49,7 @@ function WbSpreadsheetComponent({ readonly data: RA>; readonly workbench: Workbench; readonly mappings: WbMapping | undefined; + readonly isResultsOpen: boolean; readonly checkDeletedFail: (statusCode: number) => boolean; readonly spreadsheetChanged: () => void; readonly onClickDisambiguate: () => void; @@ -205,13 +207,14 @@ function WbSpreadsheetComponent({ comments, } = useHotProps({ dataset, mappings, physicalColToMappingCol }); - const hooks = useHotHooks( + const hooks = useHotHooks({ workbench, physicalColToMappingCol, spreadsheetChanged, checkDeletedFail, - isReadOnly - ); + isReadOnly, + isResultsOpen, + }); return (
diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 55332d7154c..f7c04a614e5 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -227,6 +227,7 @@ export function WbView({ hot={hot} isUploaded={isUploaded} mappings={mappings} + isResultsOpen={showResults} setHotTable={setHotTable} spreadsheetChanged={spreadsheetChanged} workbench={workbench} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index ba08786b129..b6a4a3254f0 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -472,7 +472,7 @@ export function useHotHooks({ }, beforeColumnMove: (_columnIndexes, _finalIndex, dropIndex) => - dropIndex !== undefined || workbench.hot !== undefined, + !isResultsOpen && (dropIndex !== undefined || workbench.hot !== undefined), // Save new visualOrder on the back end afterColumnMove: (_columnIndexes, _finalIndex, dropIndex) => { From 89b0d4dbef84a8819838e7ab476fc226fbaf70b1 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:57:55 -0400 Subject: [PATCH 137/153] remove ternary for rollback --- .../frontend/js_src/lib/components/WbActions/WbRollback.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx index a9f9eaa6d08..7e437f74347 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/WbRollback.tsx @@ -29,13 +29,13 @@ export function WbRollback({ > {wbText.rollback()} - {confirmRollback ? ( + {confirmRollback && ( - ) : undefined} + )} ); } From 848bf5f6757a3e41e4018a3da6f44aa44449a231 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 16:59:00 -0400 Subject: [PATCH 138/153] remove ternary in actions --- .../frontend/js_src/lib/components/WbActions/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 0ade275bad0..4f7951acd96 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -181,7 +181,7 @@ export function WbActions({ }} /> ) : undefined} - {operationCompleted ? ( + {operationCompleted && ( @@ -204,8 +204,8 @@ export function WbActions({ > {message?.message} - ) : undefined} - {operationAborted ? ( + )} + {operationAborted && ( - ) : undefined} + )} ); } From 484c02408bfc85e5cc9acdaf60d4b7a5bf9fc945 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 21:00:03 +0000 Subject: [PATCH 139/153] Lint code with ESLint and Prettier Triggered by 687fd7ad3825b177ca61295091a3183679828eba on branch refs/heads/issue-4611 --- specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx | 2 +- specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index f7c04a614e5..6e83945a522 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -225,9 +225,9 @@ export function WbView({ data={data} dataset={dataset} hot={hot} + isResultsOpen={showResults} isUploaded={isUploaded} mappings={mappings} - isResultsOpen={showResults} setHotTable={setHotTable} spreadsheetChanged={spreadsheetChanged} workbench={workbench} diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index b6a4a3254f0..4dcc4fd36f2 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -472,7 +472,8 @@ export function useHotHooks({ }, beforeColumnMove: (_columnIndexes, _finalIndex, dropIndex) => - !isResultsOpen && (dropIndex !== undefined || workbench.hot !== undefined), + !isResultsOpen && + (dropIndex !== undefined || workbench.hot !== undefined), // Save new visualOrder on the back end afterColumnMove: (_columnIndexes, _finalIndex, dropIndex) => { From 02d367c786e853a1fac5c2f837dea6de336df11f Mon Sep 17 00:00:00 2001 From: Sharad S Date: Thu, 16 May 2024 21:34:00 +0000 Subject: [PATCH 140/153] Lint code with ESLint and Prettier Triggered by ee1156499f09b9d6541c8669c0845057ba493d1c on branch refs/heads/issue-4611 --- specifyweb/frontend/js_src/lib/components/WbActions/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx index 4f7951acd96..6349a50e37e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbActions/index.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type { LocalizedString } from 'typesafe-i18n'; import { useBooleanState } from '../../hooks/useBooleanState'; import { commonText } from '../../localization/common'; @@ -22,7 +23,6 @@ import { WbRollback } from './WbRollback'; import { WbSave } from './WbSave'; import { WbUpload } from './WbUpload'; import { WbValidate } from './WbValidate'; -import { LocalizedString } from 'typesafe-i18n'; export function WbActions({ dataset, From fea2d2b0c25668e60ec320b693f74f003b799b55 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 17 May 2024 09:10:20 -0400 Subject: [PATCH 141/153] add f.fastParseInt --- .../lib/components/WbActions/useResults.ts | 3 ++- .../js_src/lib/components/WbUtils/Utils.ts | 8 ++++---- .../js_src/lib/components/WorkBench/CellMeta.ts | 5 +++-- .../frontend/js_src/lib/utils/functools.ts | 16 ++++++++++++++++ 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts index 0317a2016fc..77cb004b869 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -4,6 +4,7 @@ import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; import { getHotPlugin, identifyDefaultValues } from '../WorkBench/handsontable'; import type { Workbench } from '../WorkBench/WbView'; +import { f } from '../../utils/functools'; export function useResults({ hot, @@ -42,7 +43,7 @@ export function useResults({ Object.entries(workbench.cells.cellMeta).forEach(([physicalRow, rowMeta]) => rowMeta.forEach((metaArray, physicalCol) => { if (!workbench.cells.getCellMetaFromArray(metaArray, 'isNew')) return; - rowsToInclude.add((physicalRow as unknown as number) | 0); + rowsToInclude.add(f.fastParseInt(physicalRow)); colsToInclude.add(physicalCol); }) ); diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index 2466f50a0ef..01d8d488176 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -124,8 +124,8 @@ export class WbUtils { * This is 10 times faster then Number.parseInt because of a slow * Babel polyfill */ - const visualRow = (visualRowString as unknown as number) | 0; - const visualCol = (visualColString as unknown as number) | 0; + const visualRow = f.fastParseInt(visualRowString); + const visualCol = f.fastParseInt(visualColString); const cellTypeMatches = this.workbench.cells?.cellIsType( metaArray, @@ -394,10 +394,10 @@ export class WbUtils { ) return; const visualRow = this.workbench.hot!.toVisualRow( - (physicalRow as unknown as number) | 0 + f.fastParseInt(physicalRow) ); const visualCol = this.workbench.hot!.toVisualColumn( - (physicalCol as unknown as number) | 0 + f.fastParseInt(physicalCol) ); const cellValue = this.workbench.hot!.getDataAtCell(visualRow, visualCol) || ''; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index 06555bf8d52..bcd63fc2aa9 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -1,4 +1,5 @@ import { backEndText } from '../../localization/backEnd'; +import { f } from '../../utils/functools'; import type { RA, WritableArray } from '../../utils/types'; import { SET, throttle } from '../../utils/utils'; import { getHotPlugin } from './handsontable'; @@ -304,10 +305,10 @@ export class WbCellMeta { Object.entries(this.cellMeta).forEach(([physicalRow, metaRow]) => Object.entries(metaRow).forEach(([physicalCol, cellMeta]) => { const visualRow = this.workbench.hot!.toVisualRow( - (physicalRow as unknown as number) | 0 + f.fastParseInt(physicalRow) ); const visualCol = this.workbench.hot!.toVisualColumn( - (physicalCol as unknown as number) | 0 + f.fastParseInt(physicalCol) ); indexedCellMeta[resolveIndex(visualRow, visualCol, true)] ??= []; indexedCellMeta[resolveIndex(visualRow, visualCol, true)][ diff --git a/specifyweb/frontend/js_src/lib/utils/functools.ts b/specifyweb/frontend/js_src/lib/utils/functools.ts index 39c23dd1528..8420ecb92a8 100644 --- a/specifyweb/frontend/js_src/lib/utils/functools.ts +++ b/specifyweb/frontend/js_src/lib/utils/functools.ts @@ -144,6 +144,22 @@ export const f = { const number = Number.parseInt(value); return Number.isNaN(number) ? undefined : number; }, + /** + * This is 10 times faster then Number.parseInt because of a slow + * Babel polyfill. + * + * Some caveats over f.parseInt or Number.parseInt: + * ```ts + * f.fastParseInt(undefined) // 0 + * f.fastParseInt(null) // 0 + * f.fastParseInt('') // 0 + * f.fastParseInt('nonNumber') // 0 + * ``` + */ + fastParseInt(value: string): number { + // eslint-disable-next-line unicorn/prefer-math-trunc, no-bitwise + return (value as unknown as number) | 0; + }, /** Like f.parseInt, but for floats */ parseFloat(value: string | undefined): number | undefined { if (value === undefined) return undefined; From 2394d547e690351b3ed068860baa6db878bae5be Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 17 May 2024 09:13:13 -0400 Subject: [PATCH 142/153] improve cellMeta types --- .../lib/components/WorkBench/CellMeta.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index bcd63fc2aa9..4b3361fd5f9 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -31,11 +31,11 @@ export type WbCellCounts = { // REFACTOR: replace usages of WbMetaArray with WbMeta and test performance/memory // eslint-disable-next-line functional/prefer-readonly-type export type WbMetaArray = [ - boolean, - boolean, - boolean, - RA, - string | undefined + isNew: boolean, + isModified: boolean, + isSearchResult: boolean, + issues: RA, + originalValue: string | undefined ]; const defaultMetaValues = Object.freeze([ @@ -63,7 +63,7 @@ export class WbCellMeta { ); } - getCellMeta( + public getCellMeta( physicalRow: number, physicalCol: number, key: KEY @@ -73,7 +73,7 @@ export class WbCellMeta { defaultMetaValues[index]) as unknown as WbMeta[KEY]; } - getCellMetaFromArray( + public getCellMetaFromArray( metaCell: WbMetaArray, key: KEY ): WbMeta[KEY] { @@ -85,7 +85,7 @@ export class WbCellMeta { * This does not run visual side effects * For changing meta with side effects, use this.updateCellMeta */ - setCellMeta( + public setCellMeta( physicalRow: number, physicalCol: number, key: KEY, @@ -184,7 +184,7 @@ export class WbCellMeta { * This is the case when rendering off-screen issues (which don't require a cell * reference) */ - runMetaUpdateEffects( + public runMetaUpdateEffects( cell: HTMLTableCellElement | undefined, key: KEY, value: WbMeta[KEY], @@ -231,7 +231,7 @@ export class WbCellMeta { ); } - updateCellMeta( + public updateCellMeta( physicalRow: number, physicalCol: number, key: KEY, From 39a65979be0ad9146e6ed475779bdbc201a59d7c Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 17 May 2024 13:14:13 +0000 Subject: [PATCH 143/153] Lint code with ESLint and Prettier Triggered by 98e433138b5682725f7fd2e2e8be05b077ab178c on branch refs/heads/issue-4611 --- .../frontend/js_src/lib/components/WbActions/useResults.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts index 77cb004b869..19212389933 100644 --- a/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts +++ b/specifyweb/frontend/js_src/lib/components/WbActions/useResults.ts @@ -2,9 +2,9 @@ import type Handsontable from 'handsontable'; import React from 'react'; import { useBooleanState } from '../../hooks/useBooleanState'; +import { f } from '../../utils/functools'; import { getHotPlugin, identifyDefaultValues } from '../WorkBench/handsontable'; import type { Workbench } from '../WorkBench/WbView'; -import { f } from '../../utils/functools'; export function useResults({ hot, From 196b15e3868864db7653c17d2eb96af92ff0d0e5 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 17 May 2024 09:37:49 -0400 Subject: [PATCH 144/153] remove falsy checks for workbench.hot --- .../frontend/js_src/lib/components/WorkBench/hooks.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 4dcc4fd36f2..2a5b7e328ba 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -195,7 +195,7 @@ export function useHotHooks({ 'UndoRedo.undo', 'UndoRedo.redo', ].includes(source) || - !workbench.hot || + workbench.hot !== undefined || unfilteredChanges === null ) return; @@ -330,7 +330,7 @@ export function useHotHooks({ .forEach((physicalRow) => workbench.cells?.cellMeta.splice(physicalRow, 0, []) ); - if (workbench.hot && source !== 'auto') spreadsheetChanged(); + if (workbench.hot !== undefined && source !== 'auto') spreadsheetChanged(); return true; }, @@ -353,7 +353,7 @@ export function useHotHooks({ workbench.cells.indexedCellMeta = undefined; - if (workbench.hot && source !== 'auto') { + if (source !== 'auto') { spreadsheetChanged(); workbench.cells.updateCellInfoStats(); } From 3dbe8aa13edc0e5967b0f15de7ea7cd9073d3b51 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 17 May 2024 09:50:19 -0400 Subject: [PATCH 145/153] Improve method access in CellMeta, Disambiguation and Validation - also makes triggerLiveValidation async - adds return types - makes parseRowValidationResults more readable --- .../lib/components/WorkBench/CellMeta.ts | 2 +- .../WorkBench/DisambiguationLogic.ts | 2 +- .../lib/components/WorkBench/WbValidation.tsx | 134 +++++++++++------- 3 files changed, 85 insertions(+), 53 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts index 4b3361fd5f9..b83586c3f72 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/CellMeta.ts @@ -322,7 +322,7 @@ export class WbCellMeta { } // MetaData - updateCellInfoStats() { + public updateCellInfoStats() { const cellMeta = this.cellMeta.flat(); this.workbench.cellCounts[SET]({ diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.ts index 7f269314e6e..15a9f608e66 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/DisambiguationLogic.ts @@ -93,7 +93,7 @@ export class Disambiguation { this.workbench.cells?.updateCellInfoStats(); } - clearDisambiguation(physicalRow: number): void { + public clearDisambiguation(physicalRow: number): void { const disambiguation = this.getDisambiguation(physicalRow); if (Object.keys(disambiguation).length === 0) // Nothing to clear diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx index dbbadc051f5..b220a90885e 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbValidation.tsx @@ -67,7 +67,7 @@ export class WbValidation { this.workbench.dataset.rowresults === null ? 'off' : 'static'; } - toggleDataCheck(): void { + public toggleDataCheck(): void { if (!this.workbench?.hot) return; this.validationMode = this.validationMode === 'live' || @@ -111,8 +111,8 @@ export class WbValidation { this.triggerLiveValidation(); } - triggerLiveValidation() { - const pumpValidation = (): void => { + private async triggerLiveValidation(): Promise { + const pumpValidation = async (): Promise => { if (this.liveValidationStack.length === 0) { this.liveValidationActive = false; return; @@ -121,7 +121,7 @@ export class WbValidation { const physicalRow = this.liveValidationStack.pop(); if (physicalRow === undefined || this.workbench.hot === undefined) return; const rowData = this.workbench.hot.getSourceDataAtRow(physicalRow); - ajax<{ + await ajax<{ readonly result: UploadResult; } | null>(`/api/workbench/validate_row/${this.workbench.dataset.id}/`, { method: 'POST', @@ -137,11 +137,14 @@ export class WbValidation { }; if (!this.liveValidationActive) { - pumpValidation(); + return pumpValidation(); } } - gotRowValidationResult(physicalRow: number, result: UploadResult): void { + private gotRowValidationResult( + physicalRow: number, + result: UploadResult + ): void { if ( this.validationMode !== 'live' || this.workbench.hot?.isDestroyed !== false @@ -154,7 +157,7 @@ export class WbValidation { this.workbench.cells?.updateCellInfoStats(); } - getHeadersFromMappingPath( + private getHeadersFromMappingPath( mappingPathFilter: RA, tryBest = true ): RA { @@ -181,10 +184,10 @@ export class WbValidation { ); } - resolveValidationColumns( + private resolveValidationColumns( initialColumns: RA, inferColumnsCallback: (() => RA) | undefined = undefined - ) { + ): RA { // See https://github.com/specify/specify7/issues/810 let columns: RA = initialColumns.filter(Boolean); if (typeof inferColumnsCallback === 'function') { @@ -197,7 +200,10 @@ export class WbValidation { .filter((physicalCol) => physicalCol !== -1); } - applyRowValidationResults(physicalRow: number, result: UploadResult) { + private applyRowValidationResults( + physicalRow: number, + result: UploadResult + ): void { const rowMeta: WritableArray>> = this.workbench.dataset.columns.map(() => ({ isNew: false, @@ -237,27 +243,18 @@ export class WbValidation { }); } - parseRowValidationResults( - result: UploadResult, + private resolveUploadStatus( + uploadStatus: keyof UploadResult['UploadResult']['record_result'], + recordResult: UploadResult['UploadResult']['record_result'], + physicalRow: number, + mappingPath: MappingPath, setMetaCallback: ( key: KEY, value: KEY extends 'issues' ? string : WbMeta[KEY], columns: RA, inferColumnsCallback: (() => RA) | undefined - ) => void, - physicalRow: number, - initialMappingPath: MappingPath | undefined = [] - ) { - const uploadResult = result.UploadResult; - const uploadStatus = Object.keys(uploadResult.record_result)[0]; - const statusData = uploadResult.record_result[uploadStatus]; - const data = uploadResult.record_result; - - const isTree = 'info' in statusData && statusData.info?.treeInfo !== null; - const mappingPath = isTree - ? [...initialMappingPath, formatTreeRank(statusData.info.treeInfo.rank)] - : initialMappingPath; - + ) => void + ): void { const resolveColumns = this.getHeadersFromMappingPath.bind( this, mappingPath @@ -266,7 +263,7 @@ export class WbValidation { // Ignore these statuses if (['NullRecord', 'PropagatedFailure', 'Matched'].includes(uploadStatus)) { } else if (uploadStatus === 'ParseFailures') - data.ParseFailures.failures.forEach((line) => { + recordResult.ParseFailures.failures.forEach((line) => { const [issueMessage, payload, column] = line.length === 2 ? [line[0], {}, line[1]] : line; setMetaCallback( @@ -282,7 +279,7 @@ export class WbValidation { setMetaCallback( 'issues', wbText.noMatchErrorMessage(), - data.NoMatch.info.columns, + recordResult.NoMatch.info.columns, resolveColumns ); else if (uploadStatus === 'FailedBusinessRule') @@ -290,48 +287,54 @@ export class WbValidation { 'issues', whitespaceSensitive( resolveValidationMessage( - data.FailedBusinessRule.message, - data.FailedBusinessRule.payload ?? {} + recordResult.FailedBusinessRule.message, + recordResult.FailedBusinessRule.payload ?? {} ) ), - data.FailedBusinessRule.info.columns, + recordResult.FailedBusinessRule.info.columns, resolveColumns ); else if (uploadStatus === 'MatchedMultiple') { this.uploadResults.ambiguousMatches[physicalRow] ??= []; this.uploadResults.ambiguousMatches[physicalRow].push({ physicalCols: this.resolveValidationColumns( - data.MatchedMultiple.info.columns, + recordResult.MatchedMultiple.info.columns, resolveColumns ), mappingPath, - ids: data.MatchedMultiple.ids, - key: data.MatchedMultiple.key, + ids: recordResult.MatchedMultiple.ids, + key: recordResult.MatchedMultiple.key, }); setMetaCallback( 'issues', whitespaceSensitive(wbText.matchedMultipleErrorMessage()), - data.MatchedMultiple.info.columns, + recordResult.MatchedMultiple.info.columns, resolveColumns ); } else if (uploadStatus === 'Uploaded') { - setMetaCallback('isNew', true, data.Uploaded.info.columns, undefined); - const tableName = toLowerCase(data.Uploaded.info.tableName); + setMetaCallback( + 'isNew', + true, + recordResult.Uploaded.info.columns, + undefined + ); + const tableName = toLowerCase(recordResult.Uploaded.info.tableName); this.uploadResults.recordCounts[tableName] ??= 0; this.uploadResults.recordCounts[tableName]! += 1; this.uploadResults.newRecords[physicalRow] ??= []; - this.resolveValidationColumns(data.Uploaded.info.columns, undefined).map( - (physicalCol) => { - this.uploadResults.newRecords[physicalRow]![physicalCol] ??= []; - this.uploadResults.newRecords[physicalRow]![physicalCol].push([ - tableName, - data.Uploaded.id, - data.Uploaded.info?.treeInfo - ? `${data.Uploaded.info.treeInfo.name} (${data.Uploaded.info.treeInfo.rank})` - : '', - ]); - } - ); + this.resolveValidationColumns( + recordResult.Uploaded.info.columns, + undefined + ).forEach((physicalCol) => { + this.uploadResults.newRecords[physicalRow]![physicalCol] ??= []; + this.uploadResults.newRecords[physicalRow]![physicalCol].push([ + tableName, + recordResult.Uploaded.id, + recordResult.Uploaded.info?.treeInfo + ? `${recordResult.Uploaded.info.treeInfo.name} (${recordResult.Uploaded.info.treeInfo.rank})` + : '', + ]); + }); } else raise( new Error( @@ -339,6 +342,35 @@ export class WbValidation { row ${this.workbench.hot?.toVisualRow(physicalRow) ?? ''}` ) ); + } + + private parseRowValidationResults( + result: UploadResult, + setMetaCallback: ( + key: KEY, + value: KEY extends 'issues' ? string : WbMeta[KEY], + columns: RA, + inferColumnsCallback: (() => RA) | undefined + ) => void, + physicalRow: number, + initialMappingPath: MappingPath | undefined = [] + ): void { + const uploadResult = result.UploadResult; + const uploadStatus = Object.keys(uploadResult.record_result)[0]; + const statusData = uploadResult.record_result[uploadStatus]; + + const isTree = 'info' in statusData && statusData.info?.treeInfo !== null; + const mappingPath = isTree + ? [...initialMappingPath, formatTreeRank(statusData.info.treeInfo.rank)] + : initialMappingPath; + + this.resolveUploadStatus( + uploadStatus, + uploadResult.record_result, + physicalRow, + mappingPath, + setMetaCallback + ); Object.entries(uploadResult.toOne).forEach(([fieldName, uploadResult]) => this.parseRowValidationResults( @@ -363,7 +395,7 @@ export class WbValidation { ); } - getValidationResults(): void { + public getValidationResults(): void { if (this.workbench.mappings === undefined) return; if (this.workbench.dataset.rowresults === null) { @@ -380,7 +412,7 @@ export class WbValidation { this.workbench.cells?.updateCellInfoStats(); } - stopLiveValidation(): void { + public stopLiveValidation(): void { this.liveValidationStack = []; this.liveValidationActive = false; this.validationMode = 'off'; From 3005137678c71a8bbf55f267e8784ae8bf1ffac9 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Fri, 17 May 2024 09:53:01 -0400 Subject: [PATCH 146/153] remove ternary for showResults Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index 6e83945a522..ca4fc8500d7 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -233,7 +233,7 @@ export function WbView({ workbench={workbench} onClickDisambiguate={openDisambiguationDialog} /> - {showResults ? ( + {showResults && ( - ) : undefined} + )}
{disambiguationDialogs} Date: Fri, 17 May 2024 10:01:52 -0400 Subject: [PATCH 147/153] merge imports --- .../js_src/lib/components/WbToolkit/CoordinateConverter.tsx | 3 ++- .../js_src/lib/components/WorkBench/Disambiguation.tsx | 3 +-- .../js_src/lib/components/WorkBench/WbSpreadsheet.tsx | 5 ++--- .../frontend/js_src/lib/components/WorkBench/WbView.tsx | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 6d54cecccb1..616b8a0a20e 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -6,6 +6,7 @@ import { useCachedState } from '../../hooks/useCachedState'; import { commonText } from '../../localization/common'; import { localityText } from '../../localization/locality'; import { wbText } from '../../localization/workbench'; +import { f } from '../../utils/functools'; import type { ConversionFunction } from '../../utils/latLong'; import { Lat, Long } from '../../utils/latLong'; import type { RA, RR } from '../../utils/types'; @@ -141,7 +142,7 @@ function CoordinateConverter({ const columnsToWorkWith = React.useMemo( () => Object.keys(coordinateColumns).map((physicalCol) => - hot.toVisualColumn(Number.parseInt(physicalCol)) + hot.toVisualColumn(f.fastParseInt(physicalCol)) ), [] ); diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx index 1a28adf1767..9c86a809c42 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/Disambiguation.tsx @@ -3,8 +3,7 @@ import React from 'react'; import { useAsyncState } from '../../hooks/useAsyncState'; import { commonText } from '../../localization/common'; import { wbText } from '../../localization/workbench'; -import type { RA } from '../../utils/types'; -import type { WritableArray } from '../../utils/types'; +import type { RA, WritableArray } from '../../utils/types'; import { Button } from '../Atoms/Button'; import { Input, Label } from '../Atoms/Form'; import type { AnySchema } from '../DataModel/helperTypes'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx index 4faab25b3bf..76421f90275 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbSpreadsheet.tsx @@ -11,10 +11,9 @@ import React from 'react'; import { commonText } from '../../localization/common'; import { LANGUAGE } from '../../localization/utils/config'; import { wbText } from '../../localization/workbench'; -import { type RA } from '../../utils/types'; +import type { RA } from '../../utils/types'; import { writable } from '../../utils/types'; -import { legacyNonJsxIcons } from '../Atoms/Icons'; -import { iconClassName } from '../Atoms/Icons'; +import { iconClassName, legacyNonJsxIcons } from '../Atoms/Icons'; import { ReadOnlyContext } from '../Core/Contexts'; import { strictGetTable } from '../DataModel/tables'; import { getIcon, unknownIcon } from '../InitialContext/icons'; diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx index ca4fc8500d7..cfbba9c157a 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/WbView.tsx @@ -97,6 +97,7 @@ export function WbView({ ); useErrorContext('mappings', mappings); + // Throttle cell count update depending on the DS size (between 10ms and 2s) const throttleRate = Math.ceil(clamp(10, data.length / 10, 2000)); const [hasUnsavedChanges, spreadsheetChanged, spreadsheetUpToDate] = From c1d37ca850f9a0a3a027fecc94755630bd03f038 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 17 May 2024 14:05:41 +0000 Subject: [PATCH 148/153] Lint code with ESLint and Prettier Triggered by 09010680998bd98970b22658b43c57dfd3566dde on branch refs/heads/issue-4611 --- .../js_src/lib/components/WbToolkit/CoordinateConverter.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx index 616b8a0a20e..049665853a2 100644 --- a/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbToolkit/CoordinateConverter.tsx @@ -232,7 +232,9 @@ function CoordinateConverter({ modal={false} onClose={(): void => { hot.batch(() => - Array.from({ length: changeCountRef.current }).forEach(() => hot.undo()) + Array.from({ length: changeCountRef.current }).forEach(() => + hot.undo() + ) ); handleClose(); }} From 2b4d6191cba77c194949cbdf582e3028eed200ab Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Fri, 17 May 2024 12:53:36 -0400 Subject: [PATCH 149/153] Fix save button not triggering Co-authored-by: Jason Melton <64045831+melton-jason@users.noreply.github.com> --- specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 2a5b7e328ba..953bff108d1 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -195,7 +195,7 @@ export function useHotHooks({ 'UndoRedo.undo', 'UndoRedo.redo', ].includes(source) || - workbench.hot !== undefined || + workbench.hot === undefined || unfilteredChanges === null ) return; From 3fd6d6b6e2c5ba80842bf13176d9264ef1cef715 Mon Sep 17 00:00:00 2001 From: Sharad S <16229739+sharadsw@users.noreply.github.com> Date: Fri, 17 May 2024 16:56:59 +0000 Subject: [PATCH 150/153] Lint code with ESLint and Prettier Triggered by 2b4d6191cba77c194949cbdf582e3028eed200ab on branch refs/heads/issue-4611 --- specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts index 953bff108d1..a58971906e9 100644 --- a/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts +++ b/specifyweb/frontend/js_src/lib/components/WorkBench/hooks.ts @@ -330,7 +330,8 @@ export function useHotHooks({ .forEach((physicalRow) => workbench.cells?.cellMeta.splice(physicalRow, 0, []) ); - if (workbench.hot !== undefined && source !== 'auto') spreadsheetChanged(); + if (workbench.hot !== undefined && source !== 'auto') + spreadsheetChanged(); return true; }, From cc3ac8a2118b0c24e1872c83a90cf0cf4def50a3 Mon Sep 17 00:00:00 2001 From: Sharad S Date: Fri, 17 May 2024 14:50:12 -0400 Subject: [PATCH 151/153] Add TODO for fastParseInt --- specifyweb/frontend/js_src/lib/utils/functools.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/specifyweb/frontend/js_src/lib/utils/functools.ts b/specifyweb/frontend/js_src/lib/utils/functools.ts index 8420ecb92a8..573ec0c14f0 100644 --- a/specifyweb/frontend/js_src/lib/utils/functools.ts +++ b/specifyweb/frontend/js_src/lib/utils/functools.ts @@ -157,6 +157,7 @@ export const f = { * ``` */ fastParseInt(value: string): number { + // TODO: update babel config to not polyfil Number.parseInt and then replace fastParseInt usages with Number.parseInt // eslint-disable-next-line unicorn/prefer-math-trunc, no-bitwise return (value as unknown as number) | 0; }, From f08cfad58451f91dc1f17a493c3e564f8b1de98f Mon Sep 17 00:00:00 2001 From: Sharad S Date: Sun, 19 May 2024 16:14:50 -0400 Subject: [PATCH 152/153] fix search --- specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts index 01d8d488176..9c8b45ba327 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/Utils.ts @@ -255,10 +255,7 @@ export class WbUtils { render = Boolean(cell); } - const updateWorkbenchCells = - this.workbench.cells[render ? 'updateCellMeta' : 'setCellMeta']; - - updateWorkbenchCells( + this.workbench.cells[render ? 'updateCellMeta' : 'setCellMeta']( physicalRow, physicalCol, 'isSearchResult', From 8095d39a8208f418a760cf297e9d8961f8c4a07c Mon Sep 17 00:00:00 2001 From: Sharad S Date: Mon, 20 May 2024 08:43:55 -0400 Subject: [PATCH 153/153] add search handler to callback --- .../js_src/lib/components/WbUtils/index.tsx | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx index c62bdb1e992..d4e8e0bfb73 100644 --- a/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx +++ b/specifyweb/frontend/js_src/lib/components/WbUtils/index.tsx @@ -33,6 +33,20 @@ export function WbUtilsComponent({ const [isSearchClicked, clickSearch, unclickSearch, toggleSearch] = useBooleanState(true); + const handleSearch = React.useCallback( + _.debounce( + (event: React.KeyboardEvent) => { + if (searchRef.current && searchRef.current?.value.length > 0) + unclickSearch(); + else clickSearch(); + utils.searchCells(event, searchRef.current); + }, + debounceRate, + false + ), + [debounceRate] + ); + return (
) => { - if (searchRef.current && searchRef.current?.value.length > 0) - unclickSearch(); - else clickSearch(); - utils.searchCells(event, searchRef.current); - }, - debounceRate, - true - )} + onKeyDown={handleSearch} />
{!isUploaded && hasPermission('/workbench/dataset', 'update') ? (