From 1f9301e0d5605232e14a0f654497931c2e71892a Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 18 Apr 2024 16:32:53 -0500 Subject: [PATCH 01/31] Update getOperationNotPermittedMessage to work for both Edit in Grid and Edit in Bulk scenarios --- packages/components/releaseNotes/components.md | 5 +++++ .../components/editable/EditableGridPanelForUpdate.tsx | 4 ++-- .../src/internal/components/samples/utils.test.ts | 4 ++-- .../components/src/internal/components/samples/utils.tsx | 9 +++++++-- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 9e06505aa8..d925fa2e92 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,6 +1,11 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages. +### version TBD +*Released*: TBD +- Support cross-folder "Edit in Bulk" + - Update getOperationNotPermittedMessage to work for both Edit in Grid and Edit in Bulk scenarios + ### version 3.39.1 *Released*: 18 April 2024 - Update CSS for notebook review status pills diff --git a/packages/components/src/internal/components/editable/EditableGridPanelForUpdate.tsx b/packages/components/src/internal/components/editable/EditableGridPanelForUpdate.tsx index fc54e5af83..c31cb03d4f 100644 --- a/packages/components/src/internal/components/editable/EditableGridPanelForUpdate.tsx +++ b/packages/components/src/internal/components/editable/EditableGridPanelForUpdate.tsx @@ -88,8 +88,8 @@ export const EditableGridPanelForUpdate: FC = p const hasValidUserComment = comment?.trim()?.length > 0; const notPermittedText = useMemo( - () => getOperationNotPermittedMessage(editStatusData, pluralNoun), - [editStatusData, pluralNoun] + () => getOperationNotPermittedMessage(editStatusData, singularNoun, pluralNoun), + [editStatusData, singularNoun, pluralNoun] ); useEffect(() => { diff --git a/packages/components/src/internal/components/samples/utils.test.ts b/packages/components/src/internal/components/samples/utils.test.ts index 8a8df085ac..4a86793c28 100644 --- a/packages/components/src/internal/components/samples/utils.test.ts +++ b/packages/components/src/internal/components/samples/utils.test.ts @@ -281,10 +281,10 @@ describe('getOperationNotPermittedMessage', () => { test('with notPermitted', () => { expect(getOperationNotPermittedMessage(new OperationConfirmationData({ notPermitted: [1] }))).toBe( - "1 of the selected samples isn't shown because you don't have permissions to edit in that project." + 'The selection includes 1 sample that you do not have permission to edit. Updates will only be made to the samples you have edit permission for.' ); expect(getOperationNotPermittedMessage(new OperationConfirmationData({ notPermitted: [1, 2] }))).toBe( - "2 of the selected samples aren't shown because you don't have permissions to edit in that project." + 'The selection includes 2 samples that you do not have permission to edit. Updates will only be made to the samples you have edit permission for.' ); }); }); diff --git a/packages/components/src/internal/components/samples/utils.tsx b/packages/components/src/internal/components/samples/utils.tsx index 88673ee673..a1311db546 100644 --- a/packages/components/src/internal/components/samples/utils.tsx +++ b/packages/components/src/internal/components/samples/utils.tsx @@ -200,10 +200,15 @@ export function getOperationNotAllowedMessage( return null; } -export function getOperationNotPermittedMessage(statusData: OperationConfirmationData, nounPlural = 'samples'): string { +export function getOperationNotPermittedMessage( + statusData: OperationConfirmationData, + nounSingular = 'sample', + nounPlural = 'samples' +): string { if (statusData && statusData.notPermitted?.length > 0) { const notPermittedCount = statusData.notPermitted.length; - return `${notPermittedCount.toLocaleString()} of the selected ${nounPlural.toLowerCase()} ${notPermittedCount > 1 ? "aren't" : "isn't"} shown because you don't have permissions to edit in that project.`; + const noun = notPermittedCount === 1 ? nounSingular : nounPlural; + return `The selection includes ${notPermittedCount.toLocaleString()} ${noun.toLowerCase()} that you do not have permission to edit. Updates will only be made to the ${nounPlural.toLowerCase()} you have edit permission for.`; } return null; } From cbe6338170a286532754ea041ee788a0c4c287a1 Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 18 Apr 2024 16:33:57 -0500 Subject: [PATCH 02/31] BulkUpdateForm getUpdatedData() to include Folder in updated rows, if it exists in originalData --- packages/components/releaseNotes/components.md | 1 + packages/components/src/internal/util/utils.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index d925fa2e92..78226547d8 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -5,6 +5,7 @@ Components, models, actions, and utility functions for LabKey applications and p *Released*: TBD - Support cross-folder "Edit in Bulk" - Update getOperationNotPermittedMessage to work for both Edit in Grid and Edit in Bulk scenarios + - BulkUpdateForm getUpdatedData() to include Folder in updated rows, if it exists in originalData ### version 3.39.1 *Released*: 18 April 2024 diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index f2621e6005..54e66947d2 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -300,18 +300,23 @@ function isSameWithStringCompare(value1: any, value2: any): boolean { * @param originalData a map from an id field to a Map from fieldKeys to an object with a 'value' field * @param updatedValues an object mapping fieldKeys to values that are being updated * @param primaryKeys the list of primary fieldKey names - * @param additionalPkCols additional array of primary fieldKey names + * @param additionalCols additional array of fieldKeys to include */ export function getUpdatedData( originalData: Map, updatedValues: any, primaryKeys: string[], - additionalPkCols?: Set + additionalCols?: Set ): any[] { const updateValuesMap = Map(updatedValues); const pkColsLc = new Set(); primaryKeys.forEach(key => pkColsLc.add(key.toLowerCase())); - additionalPkCols?.forEach(col => pkColsLc.add(col.toLowerCase())); + additionalCols?.forEach(col => pkColsLc.add(col.toLowerCase())); + + // if the originalData has the container/folder values, keep those as well (i.e. treat it as a primary key) + const folderKey = originalData.first().keySeq().find(key => key.toLowerCase() === 'folder'); + if (folderKey) pkColsLc.add(folderKey.toLowerCase()); + const updatedData = originalData.map(originalRowMap => { return originalRowMap.reduce((m, fieldValueMap, key) => { // Issue 42672: The original data has keys that are names or captions for the columns. We need to use From 8cbeda1122e32170e2a082b06ac76e8e0131938d Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 18 Apr 2024 16:34:25 -0500 Subject: [PATCH 03/31] 3.39.1-fb-crossFolderEditInBulk.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 1af909080c..56a577d8e8 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.1", + "version": "3.39.1-fb-crossFolderEditInBulk.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.1", + "version": "3.39.1-fb-crossFolderEditInBulk.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index d1b0e0a393..40ddf40df1 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.1", + "version": "3.39.1-fb-crossFolderEditInBulk.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From e7dc321c4d3ecfad70b1c35214203bdabab7f4a7 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 19 Apr 2024 09:15:31 -0500 Subject: [PATCH 04/31] Add getSelectedIds(filterIds) to QueryModel --- packages/components/releaseNotes/components.md | 1 + .../editable/EditableGridLoaderFromSelection.tsx | 7 +++---- .../src/internal/components/forms/BulkUpdateForm.tsx | 6 +++--- .../components/src/public/QueryModel/QueryModel.ts | 10 ++++++++++ 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 78226547d8..bdb190d50c 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -6,6 +6,7 @@ Components, models, actions, and utility functions for LabKey applications and p - Support cross-folder "Edit in Bulk" - Update getOperationNotPermittedMessage to work for both Edit in Grid and Edit in Bulk scenarios - BulkUpdateForm getUpdatedData() to include Folder in updated rows, if it exists in originalData + - Add getSelectedIds(filterIds) to QueryModel ### version 3.39.1 *Released*: 18 April 2024 diff --git a/packages/components/src/internal/components/editable/EditableGridLoaderFromSelection.tsx b/packages/components/src/internal/components/editable/EditableGridLoaderFromSelection.tsx index 62ec4c02c3..4b2698b407 100644 --- a/packages/components/src/internal/components/editable/EditableGridLoaderFromSelection.tsx +++ b/packages/components/src/internal/components/editable/EditableGridLoaderFromSelection.tsx @@ -61,14 +61,13 @@ export class EditableGridLoaderFromSelection implements EditableGridLoader { fetch(gridModel: QueryModel): Promise { return new Promise((resolve, reject) => { - const { queryName, queryParameters, schemaName, selections, sortString, viewName } = gridModel; - - const selections_ = [...selections].filter(s => this.idsNotPermitted.indexOf(parseInt(s, 10)) === -1); + const { queryName, queryParameters, schemaName, sortString, viewName } = gridModel; + const selectedIds = gridModel.getSelectedIds(this.idsNotPermitted); return getSelectedData( schemaName, queryName, - selections_, + selectedIds, gridModel.getRequestColumnsString(this.requiredColumns, this.omittedColumns, true), sortString, queryParameters, diff --git a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx index 8714cfc3d7..92328631b7 100644 --- a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx +++ b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx @@ -35,7 +35,7 @@ interface Props { queryInfo: QueryInfo; readOnlyColumns?: string[]; requiredColumns?: string[]; - selectedIds: Set; + selectedIds: string[]; singularNoun?: string; // sortString is used so we render editable grids with the proper sorts when using onSubmitForEdit sortString?: string; @@ -100,7 +100,7 @@ export class BulkUpdateForm extends PureComponent { const { data, dataIds } = await getSelectedData( schemaName, name, - Array.from(selectedIds), + selectedIds, columnString, sortString, undefined, @@ -160,7 +160,7 @@ export class BulkUpdateForm extends PureComponent { } getSelectionCount(): number { - return this.props.selectedIds.size; + return this.props.selectedIds.length; } getSelectionNoun(): string { diff --git a/packages/components/src/public/QueryModel/QueryModel.ts b/packages/components/src/public/QueryModel/QueryModel.ts index 80a17eee67..0958f5cde2 100644 --- a/packages/components/src/public/QueryModel/QueryModel.ts +++ b/packages/components/src/public/QueryModel/QueryModel.ts @@ -956,6 +956,16 @@ export class QueryModel { return undefined; } + /** + * Return the selection ids as an optionally filtered, array. + */ + getSelectedIds(filterIds: number[] = []): string[] { + if (this.selections) { + return Array.from(this.selections).filter(s => filterIds.indexOf(parseInt(s, 10)) === -1); + } + return undefined; + } + /** * Get the row selection state (ALL, SOME, or NONE) for the QueryModel. */ From cfdcd8bfe5c370554c7928839286e2deaa194a88 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 19 Apr 2024 09:16:17 -0500 Subject: [PATCH 05/31] 3.39.1-fb-crossFolderEditInBulk.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 56a577d8e8..59e7accc51 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.0", + "version": "3.39.1-fb-crossFolderEditInBulk.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.0", + "version": "3.39.1-fb-crossFolderEditInBulk.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 40ddf40df1..6adeea489b 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.0", + "version": "3.39.1-fb-crossFolderEditInBulk.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From aefb117369b65f73ced018c33d2542e43efeef43 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 19 Apr 2024 10:09:00 -0500 Subject: [PATCH 06/31] saveRowsByContainer prop for containerField to be optional since it has a default --- packages/components/src/internal/query/APIWrapper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/src/internal/query/APIWrapper.ts b/packages/components/src/internal/query/APIWrapper.ts index ca74cbb5fa..5164db8c34 100644 --- a/packages/components/src/internal/query/APIWrapper.ts +++ b/packages/components/src/internal/query/APIWrapper.ts @@ -100,7 +100,7 @@ export interface QueryAPIWrapper { inherit: boolean, shared: boolean ) => Promise; - saveRowsByContainer: (options: SaveRowsOptions, containerField: string) => Promise; + saveRowsByContainer: (options: SaveRowsOptions, containerField?: string) => Promise; saveSessionView: ( schemaQuery: SchemaQuery, containerPath: string, From b31b80a59f1f61d334f5327090a0f9d497efab48 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 19 Apr 2024 11:46:10 -0500 Subject: [PATCH 07/31] 3.39.1-fb-crossFolderEditInBulk.2 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 59e7accc51..2cd840b1bf 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.1", + "version": "3.39.1-fb-crossFolderEditInBulk.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.1", + "version": "3.39.1-fb-crossFolderEditInBulk.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 6adeea489b..75582a37bf 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.1", + "version": "3.39.1-fb-crossFolderEditInBulk.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From dfbbe538e5df286524139084864ab3ab67b62bfc Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 19 Apr 2024 13:44:32 -0500 Subject: [PATCH 08/31] AppendUnitsInput fixes for grid cell rendering and enable/disable in bulk form --- packages/components/releaseNotes/components.md | 1 + .../components/src/internal/components/editable/Cell.tsx | 5 +++-- .../internal/components/forms/input/AppendUnitsInput.tsx | 7 +++++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index bdb190d50c..1e2cb73585 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -7,6 +7,7 @@ Components, models, actions, and utility functions for LabKey applications and p - Update getOperationNotPermittedMessage to work for both Edit in Grid and Edit in Bulk scenarios - BulkUpdateForm getUpdatedData() to include Folder in updated rows, if it exists in originalData - Add getSelectedIds(filterIds) to QueryModel + - AppendUnitsInput fixes for grid cell rendering and enable/disable in bulk form ### version 3.39.1 *Released*: 18 April 2024 diff --git a/packages/components/src/internal/components/editable/Cell.tsx b/packages/components/src/internal/components/editable/Cell.tsx index e11fcf06a3..781d817acf 100644 --- a/packages/components/src/internal/components/editable/Cell.tsx +++ b/packages/components/src/internal/components/editable/Cell.tsx @@ -385,6 +385,7 @@ export class Cell extends React.PureComponent { const { filteredLookupKeys } = this.state; const isDateTimeField = this.isDateTimeField; const showLookup = this.isLookup; + const showMenu = showLookup || (col.inputRenderer && col.inputRenderer !== 'AppendUnitsInput'); if (!focused) { let valueDisplay = values @@ -403,7 +404,7 @@ export class Cell extends React.PureComponent { 'cell-warning': message !== undefined, 'cell-read-only': this.isReadOnly, 'cell-locked': locked, - 'cell-menu': showLookup || col.inputRenderer, + 'cell-menu': showMenu, 'cell-placeholder': valueDisplay.length === 0 && placeholder !== undefined, }), onDoubleClick: this.handleDblClick, @@ -418,7 +419,7 @@ export class Cell extends React.PureComponent { if (valueDisplay.length === 0 && placeholder) valueDisplay = placeholder; let cell: ReactNode; - if (showLookup || col.inputRenderer) { + if (showMenu) { cell = (
{valueDisplay}
diff --git a/packages/components/src/internal/components/forms/input/AppendUnitsInput.tsx b/packages/components/src/internal/components/forms/input/AppendUnitsInput.tsx index a6438ac65c..6065dbe6bc 100644 --- a/packages/components/src/internal/components/forms/input/AppendUnitsInput.tsx +++ b/packages/components/src/internal/components/forms/input/AppendUnitsInput.tsx @@ -9,7 +9,8 @@ const isNumericWithError = (values: any, v: string | number): any => validationRules.isNumeric(values, v) || 'Please enter a number.'; export const AppendUnitsInput: FC = memo(props => { - const { col, formsy, initiallyDisabled, inputClass, showLabel, value } = props; + const { col, formsy, inputClass, showLabel, value, ...otherProps } = props; + const { allowFieldDisable, initiallyDisabled, onToggleDisable } = otherProps; // Issue 23462: Global Formsy validation rule for numbers if (!validationRules.isNumericWithError) { @@ -24,9 +25,11 @@ export const AppendUnitsInput: FC = memo(props => { return ( {col.units}} elementWrapperClassName={inputClass} - initiallyDisabled={initiallyDisabled} queryColumn={col} showLabel={showLabel} validations="isNumericWithError" From 0cb7686a9b2f8e5781be729fdbd115519b95cfa0 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 19 Apr 2024 13:45:15 -0500 Subject: [PATCH 09/31] 3.39.1-fb-crossFolderEditInBulk.3 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 2cd840b1bf..633520bc79 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.2", + "version": "3.39.1-fb-crossFolderEditInBulk.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.2", + "version": "3.39.1-fb-crossFolderEditInBulk.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 75582a37bf..8fdfa6e864 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.1-fb-crossFolderEditInBulk.2", + "version": "3.39.1-fb-crossFolderEditInBulk.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 37ab44c321df8f5132db0abc9baa68969ad926c6 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 22 Apr 2024 10:22:18 -0500 Subject: [PATCH 10/31] 3.39.4-fb-crossFolderEditInBulk.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 0b1900bbda..0287b3576a 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.4", + "version": "3.39.4-fb-crossFolderEditInBulk.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.4", + "version": "3.39.4-fb-crossFolderEditInBulk.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 6045457f67..9fe0307836 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.4", + "version": "3.39.4-fb-crossFolderEditInBulk.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 996da535a942af873a89b6a6a1bac9a83fa514b8 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 22 Apr 2024 15:21:24 -0500 Subject: [PATCH 11/31] Edit in Grid and Bulk lookup fields to use containerPath based on selected row(s) - for BulkUpdateForm, disable lookup fields toggle when more than one containerPath in selection --- .../components/releaseNotes/components.md | 1 + packages/components/src/index.ts | 2 + .../components/buttons/ToggleButtons.tsx | 34 ++++++-- .../src/internal/components/editable/Cell.tsx | 1 + .../components/editable/LookupCell.tsx | 18 ++++- .../components/forms/BulkUpdateForm.tsx | 77 ++++++++++++------- .../internal/components/forms/FieldLabel.tsx | 3 + .../components/forms/QueryFormInputs.tsx | 20 ++++- .../components/forms/detail/DetailDisplay.tsx | 5 +- .../components/forms/input/SelectInput.tsx | 9 ++- .../src/internal/util/utils.test.ts | 29 +++++++ .../components/src/internal/util/utils.ts | 10 +++ packages/components/src/theme/fields.scss | 4 + 13 files changed, 168 insertions(+), 45 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index d646e7dfe4..35e51464b0 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -8,6 +8,7 @@ Components, models, actions, and utility functions for LabKey applications and p - BulkUpdateForm getUpdatedData() to include Folder in updated rows, if it exists in originalData - Add getSelectedIds(filterIds) to QueryModel - AppendUnitsInput fixes for grid cell rendering and enable/disable in bulk form + - Edit in Grid and Bulk lookup fields to use containerPath based on selected row(s) ### version 3.39.4 *Released*: 19 April 2024 diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index d97a2a7328..a58b63a49d 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -76,6 +76,7 @@ import { uncapitalizeFirstChar, valueIsEmpty, withTransformedKeys, + getValueFromRow, } from './internal/util/utils'; import { AutoForm } from './internal/components/AutoForm'; import { HelpIcon } from './internal/components/HelpIcon'; @@ -1577,6 +1578,7 @@ export { generateId, debounce, valueIsEmpty, + getValueFromRow, getActionErrorMessage, getConfirmDeleteMessage, resolveErrorMessage, diff --git a/packages/components/src/internal/components/buttons/ToggleButtons.tsx b/packages/components/src/internal/components/buttons/ToggleButtons.tsx index 48265540d9..2df7310df8 100644 --- a/packages/components/src/internal/components/buttons/ToggleButtons.tsx +++ b/packages/components/src/internal/components/buttons/ToggleButtons.tsx @@ -2,6 +2,7 @@ import React, { FC, memo, useCallback } from 'react'; import classNames from 'classnames'; import { FormsyInput } from '../forms/input/FormsyReactComponents'; +import { LabelHelpTip } from "../base/LabelHelpTip"; interface Props { active: string; @@ -16,6 +17,7 @@ interface Props { inputFieldName?: string; onClick: (selected: string) => void; second?: string; + toolTip?: string; } export const ToggleButtons: FC = memo(props => { @@ -71,17 +73,34 @@ export const ToggleButtons: FC = memo(props => { }); export const ToggleIcon: FC = memo(props => { - const { first = 'on', second = 'off', onClick, active = 'off', className, inputFieldName, id } = props; + const { + first = 'on', + second = 'off', + onClick, + active = 'off', + className, + inputFieldName, + id, + disabled = false, + toolTip, + } = props; const firstActive = active === first; const secondActive = active === second; const firstBtnClick = useCallback(() => { - if (secondActive) onClick(first); - }, [first, secondActive, onClick]); + if (secondActive && !disabled) onClick(first); + }, [first, secondActive, onClick, disabled]); const secondBtnClick = useCallback(() => { - if (firstActive) onClick(second); - }, [second, firstActive, onClick]); + if (firstActive && !disabled) onClick(second); + }, [second, firstActive, onClick, disabled]); + + const body = ( + <> + {firstActive && } + {secondActive && } + + ); return ( <> @@ -92,12 +111,13 @@ export const ToggleIcon: FC = memo(props => { className={classNames('toggle', 'toggle-group-icon', 'btn-group', { 'toggle-on': firstActive, 'toggle-off': secondActive, + disabled, [className]: !!className, })} id={id} > - {firstActive && } - {secondActive && } + {toolTip && {toolTip}} + {!toolTip && body}
); diff --git a/packages/components/src/internal/components/editable/Cell.tsx b/packages/components/src/internal/components/editable/Cell.tsx index 781d817acf..8828a469f6 100644 --- a/packages/components/src/internal/components/editable/Cell.tsx +++ b/packages/components/src/internal/components/editable/Cell.tsx @@ -492,6 +492,7 @@ export class Cell extends React.PureComponent { forUpdate={forUpdate} modifyCell={cellActions.modifyCell} onKeyDown={this.handleFocusedDropdownKeys} + row={row} rowIdx={rowIdx} select={cellActions.selectCell} values={values} diff --git a/packages/components/src/internal/components/editable/LookupCell.tsx b/packages/components/src/internal/components/editable/LookupCell.tsx index f68bf5b0ea..68f2b2abd4 100644 --- a/packages/components/src/internal/components/editable/LookupCell.tsx +++ b/packages/components/src/internal/components/editable/LookupCell.tsx @@ -34,6 +34,7 @@ import { MODIFICATION_TYPES, SELECTION_TYPES } from './constants'; import { ValueDescriptor } from './models'; import { getLookupFilters, gridCellSelectInputProps, onCellSelectChange } from './utils'; +import { getValueFromRow } from "../../util/utils"; export interface LookupCellProps { col: QueryColumn; @@ -48,9 +49,10 @@ export interface LookupCellProps { lookupValueFilters?: Filter.IFilter[]; modifyCell: (colIdx: number, rowIdx: number, newValues: ValueDescriptor[], mod: MODIFICATION_TYPES) => void; onKeyDown?: (event: KeyboardEvent) => void; + row: any; rowIdx: number; - values: List; select: (colIdx: number, rowIdx: number, selection?: SELECTION_TYPES, resetValue?: boolean) => void; + values: List; } interface QueryLookupCellProps extends LookupCellProps { @@ -126,7 +128,7 @@ const QueryLookupCell: FC = memo(props => { QueryLookupCell.displayName = 'QueryLookupCell'; export const LookupCell: FC = memo(props => { - const { col, colIdx, disabled, modifyCell, onKeyDown, rowIdx, select, values } = props; + const { col, colIdx, disabled, modifyCell, onKeyDown, row, rowIdx, select, values, containerPath } = props; const onSelectChange = useCallback( (fieldName, formValue, options, props_) => { @@ -158,7 +160,17 @@ export const LookupCell: FC = memo(props => { ); } - return ; + // if the column is a lookup, we need to pass the containerPath to the QuerySelect + const containerPath_ = containerPath ?? getValueFromRow(row.toJS(), 'Folder')?.toString(); + + return ( + + ); }); LookupCell.displayName = 'LookupCell'; diff --git a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx index 92328631b7..9dfa0dcd21 100644 --- a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx +++ b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx @@ -9,7 +9,7 @@ import { SchemaQuery } from '../../../public/SchemaQuery'; import { getSelectedData } from '../../actions'; -import { capitalizeFirstChar, getCommonDataValues, getUpdatedData } from '../../util/utils'; +import {capitalizeFirstChar, caseInsensitive, getCommonDataValues, getUpdatedData} from '../../util/utils'; import { QueryInfoForm } from './QueryInfoForm'; @@ -46,10 +46,10 @@ interface Props { } interface State { + containerPaths: string[]; dataForSelection: Map; dataIdsForSelection: List; displayFieldUpdates: any; - errorMsg: string; isLoadingDataForSelection: boolean; originalDataForSelection: Map; } @@ -65,11 +65,11 @@ export class BulkUpdateForm extends PureComponent { super(props); this.state = { + containerPaths: undefined, originalDataForSelection: undefined, dataForSelection: undefined, displayFieldUpdates: {}, dataIdsForSelection: undefined, - errorMsg: undefined, isLoadingDataForSelection: true, }; } @@ -93,7 +93,6 @@ export class BulkUpdateForm extends PureComponent { : undefined; let columnString = columns?.map(c => c.fieldKey).join(','); if (requiredColumns) columnString = `${columnString ? columnString + ',' : ''}${requiredColumns.join(',')}`; - const { schemaName, name } = queryInfo; try { @@ -108,6 +107,7 @@ export class BulkUpdateForm extends PureComponent { ); const mappedData = this.mapDataForDisplayFields(data); this.setState({ + containerPaths: mappedData.containerPaths, originalDataForSelection: data, dataForSelection: mappedData.data, displayFieldUpdates: mappedData.bulkUpdates, @@ -121,42 +121,54 @@ export class BulkUpdateForm extends PureComponent { } }; - mapDataForDisplayFields(data: Map): { bulkUpdates: OrderedMap; data: Map } { + mapDataForDisplayFields(data: Map): { + bulkUpdates: OrderedMap; + containerPaths?: string[]; + data: Map; + } { const { displayValueFields } = this.props; let updates = Map(); let bulkUpdates = OrderedMap(); - - if (!displayValueFields) return { data, bulkUpdates }; + const containerPaths = new Set(); let conflictKeys = new Set(); data.forEach((rowData, id) => { if (rowData) { - let updatedRow = Map(); - rowData.forEach((field, key) => { - if (displayValueFields.includes(key)) { - const valuesDiffer = - field.has('displayValue') && field.get('value') !== field.get('displayValue'); - let comparisonValue = field.get('displayValue') ?? field.get('value'); - if (comparisonValue) comparisonValue += ''; // force to string - if (!conflictKeys.has(key)) { - if (!bulkUpdates.has(key)) { - bulkUpdates = bulkUpdates.set(key, comparisonValue); - } else if (bulkUpdates.get(key) !== comparisonValue) { - bulkUpdates = bulkUpdates.remove(key); - conflictKeys = conflictKeys.add(key); + const containerPath = caseInsensitive(rowData.toJS(), 'Folder'); + if (containerPath?.value) containerPaths.add(containerPath.value); + + if (displayValueFields) { + let updatedRow = Map(); + rowData.forEach((field, key) => { + if (displayValueFields.includes(key)) { + const valuesDiffer = + field.has('displayValue') && field.get('value') !== field.get('displayValue'); + let comparisonValue = field.get('displayValue') ?? field.get('value'); + if (comparisonValue) comparisonValue += ''; // force to string + if (!conflictKeys.has(key)) { + if (!bulkUpdates.has(key)) { + bulkUpdates = bulkUpdates.set(key, comparisonValue); + } else if (bulkUpdates.get(key) !== comparisonValue) { + bulkUpdates = bulkUpdates.remove(key); + conflictKeys = conflictKeys.add(key); + } + } + if (valuesDiffer) { + field = field.set('value', comparisonValue); } } - if (valuesDiffer) { - field = field.set('value', comparisonValue); - } + updatedRow = updatedRow.set(key, field); + }); + if (!updatedRow.isEmpty()) { + updates = updates.set(id, updatedRow); } - updatedRow = updatedRow.set(key, field); - }); - if (!updatedRow.isEmpty()) updates = updates.set(id, updatedRow); + } } }); - if (!updates.isEmpty()) return { data: data.merge(updates), bulkUpdates }; - return { data, bulkUpdates }; + if (!updates.isEmpty()) { + return { data: data.merge(updates), bulkUpdates, containerPaths: Array.from(containerPaths) }; + } + return { data, bulkUpdates, containerPaths: Array.from(containerPaths) }; } getSelectionCount(): number { @@ -216,7 +228,7 @@ export class BulkUpdateForm extends PureComponent { } render() { - const { isLoadingDataForSelection, dataForSelection } = this.state; + const { isLoadingDataForSelection, dataForSelection, containerPaths } = this.state; const { containerFilter, onCancel, @@ -230,6 +242,11 @@ export class BulkUpdateForm extends PureComponent { const fieldValues = isLoadingDataForSelection || !dataForSelection ? undefined : getCommonDataValues(dataForSelection); + // if all selectedIds are from the same containerPath, use that for the lookups via QueryFormInputs > QuerySelect, + // otherwise, disable lookup fields + const containerPath = containerPaths?.length === 1 ? containerPaths[0] : undefined; + const preventLookupsEnable = containerPaths?.length > 1; + return ( { checkRequiredFields={false} columnFilter={this.columnFilter} containerFilter={containerFilter} + containerPath={containerPath} disabled={disabled} + preventLookupsEnable={preventLookupsEnable} fieldValues={fieldValues} header={this.renderBulkUpdateHeader()} includeCommentField={true} diff --git a/packages/components/src/internal/components/forms/FieldLabel.tsx b/packages/components/src/internal/components/forms/FieldLabel.tsx index 42eebc29ef..f544647d04 100644 --- a/packages/components/src/internal/components/forms/FieldLabel.tsx +++ b/packages/components/src/internal/components/forms/FieldLabel.tsx @@ -15,6 +15,7 @@ import { LabelOverlay, LabelOverlayProps } from './LabelOverlay'; interface ToggleProps { onClick: () => void; + toolTip?: string; } export interface FieldLabelProps { @@ -93,6 +94,8 @@ export class FieldLabel extends Component { inputFieldName={getFieldEnabledFieldName(column, fieldName)} active={!isDisabled ? 'on' : 'off'} onClick={toggleProps?.onClick} + disabled={!toggleProps?.onClick} + toolTip={toggleProps?.toolTip} /> diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index bda7a9dffc..459f88f850 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -25,6 +25,8 @@ import { QueryInfo } from '../../../public/QueryInfo'; import { caseInsensitive } from '../../util/utils'; +import { getContainerFilterForLookups } from '../../query/api'; + import { FormsyInput } from './input/FormsyReactComponents'; import { resolveInputRenderer } from './input/InputRenderFactory'; import { QuerySelect } from './QuerySelect'; @@ -49,6 +51,7 @@ export interface QueryFormInputsProps { componentKey?: string; // unique key to add to QuerySelect to avoid duplication w/ transpose /** A container filter that will be applied to all query-based inputs in this form */ containerFilter?: Query.ContainerFilter; + containerPath?: string; disabledFields?: List; fieldValues?: any; fireQSChangeOnInit?: boolean; @@ -59,6 +62,7 @@ export interface QueryFormInputsProps { onFieldsEnabledChange?: (numEnabled: number) => void; operation?: Operation; onSelectChange?: SelectInputChange; + preventLookupsEnable?: boolean; queryColumns?: ExtendedMap; queryFilters?: Record>; queryInfo?: QueryInfo; @@ -155,6 +159,8 @@ export class QueryFormInputs extends React.Component {this.renderLabelField(col)} ReactNode; @@ -320,7 +321,9 @@ export function resolveDetailEditRenderer( return ( { this._isMounted = false; } - toggleDisabled = (): void => { + onToggleChange = (): void => { this.setState( state => ({ isDisabled: !state.isDisabled, @@ -473,6 +474,7 @@ export class SelectInputImpl extends Component { addLabelAsterisk, renderFieldLabel, helpTipRenderer, + toggleDisabledTooltip, } = this.props; const { isDisabled } = this.state; @@ -500,14 +502,15 @@ export class SelectInputImpl extends Component { addLabelAsterisk, isFormsy: false, required, - labelClass: !allowDisable ? this.props.labelClass : undefined, + labelClass: !allowDisable ? labelClass : undefined, helpTipRenderer, }} showLabel={showLabel} showToggle={allowDisable} isDisabled={isDisabled} toggleProps={{ - onClick: this.toggleDisabled, + onClick: toggleDisabledTooltip ? undefined : this.onToggleChange, + toolTip: toggleDisabledTooltip, }} /> ); diff --git a/packages/components/src/internal/util/utils.test.ts b/packages/components/src/internal/util/utils.test.ts index 48a3dc523e..738089996a 100644 --- a/packages/components/src/internal/util/utils.test.ts +++ b/packages/components/src/internal/util/utils.test.ts @@ -40,6 +40,7 @@ import { capitalizeFirstChar, uncapitalizeFirstChar, withTransformedKeys, + getValueFromRow, } from './utils'; const emptyList = List(); @@ -1233,3 +1234,31 @@ describe('arrayEquals', () => { expect(arrayEquals(['a', 'b'], ['B', 'A'], false)).toBeFalsy(); }); }); + +describe('getValueFromRow', () => { + test('returns value', () => { + const row = { Name: 'test' }; + expect(getValueFromRow(row, 'Name')).toEqual('test'); + expect(getValueFromRow(row, 'name')).toEqual('test'); + expect(getValueFromRow(row, 'bogus')).toEqual(undefined); + }); + + test('returns value from object', () => { + const row = { Name: { value: 'test' } }; + expect(getValueFromRow(row, 'Name')).toEqual('test'); + expect(getValueFromRow(row, 'name')).toEqual('test'); + expect(getValueFromRow(row, 'bogus')).toEqual(undefined); + }); + + test('returns value from array', () => { + let row = { Name: ['test1', 'test2'] }; + expect(getValueFromRow(row, 'Name')).toEqual(undefined); + expect(getValueFromRow(row, 'name')).toEqual(undefined); + expect(getValueFromRow(row, 'bogus')).toEqual(undefined); + + row = { Name: [{ value: 'test1' }, { value: 'test2' }] }; + expect(getValueFromRow(row, 'Name')).toEqual('test1'); + expect(getValueFromRow(row, 'name')).toEqual('test1'); + expect(getValueFromRow(row, 'bogus')).toEqual(undefined); + }); +}); diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index 54e66947d2..14e2787d69 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -713,3 +713,13 @@ export function arrayEquals(a: string[], b: string[], ignoreOrder = true, caseIn return caseInsensitive ? aStr.toLowerCase() === bStr.toLowerCase() : aStr === bStr; } + +export function getValueFromRow(row: Record, col: string): string | number { + const val = caseInsensitive(row, col); + if (Utils.isArray(val)) { + return val[0]?.value; + } else if (Utils.isObject(val)) { + return val?.value; + } + return val; +} diff --git a/packages/components/src/theme/fields.scss b/packages/components/src/theme/fields.scss index f3a1dfeb78..2f91a7f053 100644 --- a/packages/components/src/theme/fields.scss +++ b/packages/components/src/theme/fields.scss @@ -138,6 +138,10 @@ color: $gray-light; } } +.toggle-group-icon.disabled i { + cursor: default; + color: $gray-lighter; +} .control-toggle-btn-group { display: flex; From 6d80e16b9eecb17c25e947d2a9311a3db29efc75 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 22 Apr 2024 15:24:33 -0500 Subject: [PATCH 12/31] null check in getValueFromRow --- .../src/internal/components/editable/LookupCell.tsx | 2 +- packages/components/src/internal/util/utils.test.ts | 5 +++++ packages/components/src/internal/util/utils.ts | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/components/src/internal/components/editable/LookupCell.tsx b/packages/components/src/internal/components/editable/LookupCell.tsx index 68f2b2abd4..31ca477fda 100644 --- a/packages/components/src/internal/components/editable/LookupCell.tsx +++ b/packages/components/src/internal/components/editable/LookupCell.tsx @@ -161,7 +161,7 @@ export const LookupCell: FC = memo(props => { } // if the column is a lookup, we need to pass the containerPath to the QuerySelect - const containerPath_ = containerPath ?? getValueFromRow(row.toJS(), 'Folder')?.toString(); + const containerPath_ = containerPath ?? getValueFromRow(row?.toJS(), 'Folder')?.toString(); return ( { }); describe('getValueFromRow', () => { + test('no row', () => { + expect(getValueFromRow(undefined, 'Name')).toEqual(undefined); + expect(getValueFromRow({}, 'Name')).toEqual(undefined); + }); + test('returns value', () => { const row = { Name: 'test' }; expect(getValueFromRow(row, 'Name')).toEqual('test'); diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index 14e2787d69..ee680d249c 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -715,6 +715,8 @@ export function arrayEquals(a: string[], b: string[], ignoreOrder = true, caseIn } export function getValueFromRow(row: Record, col: string): string | number { + if (!row) return undefined; + const val = caseInsensitive(row, col); if (Utils.isArray(val)) { return val[0]?.value; From c2627de48568ff59ab5574c7d7643675d1599d1b Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 22 Apr 2024 15:25:09 -0500 Subject: [PATCH 13/31] 3.39.4-fb-crossFolderEditInBulk.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 0287b3576a..4b1ebc505a 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.0", + "version": "3.39.4-fb-crossFolderEditInBulk.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.0", + "version": "3.39.4-fb-crossFolderEditInBulk.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 9fe0307836..a0c5355ced 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.0", + "version": "3.39.4-fb-crossFolderEditInBulk.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 69a41ef15b6fac9799c98f91b7a5812f1e9766e8 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 23 Apr 2024 11:32:20 -0500 Subject: [PATCH 14/31] Add getOperationConfirmationData to ApiWrapper --- packages/components/releaseNotes/components.md | 1 + .../src/internal/components/entities/APIWrapper.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 35e51464b0..810699dde0 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -9,6 +9,7 @@ Components, models, actions, and utility functions for LabKey applications and p - Add getSelectedIds(filterIds) to QueryModel - AppendUnitsInput fixes for grid cell rendering and enable/disable in bulk form - Edit in Grid and Bulk lookup fields to use containerPath based on selected row(s) + - Add getOperationConfirmationData to ApiWrapper ### version 3.39.4 *Released*: 19 April 2024 diff --git a/packages/components/src/internal/components/entities/APIWrapper.ts b/packages/components/src/internal/components/entities/APIWrapper.ts index c602a3de0f..978f04c3d4 100644 --- a/packages/components/src/internal/components/entities/APIWrapper.ts +++ b/packages/components/src/internal/components/entities/APIWrapper.ts @@ -14,6 +14,7 @@ import { GetDeleteConfirmationDataOptions, getDeleteConfirmationData, getMoveConfirmationData, + getOperationConfirmationData, getOperationConfirmationDataForModel, getEntityTypeData, getOriginalParentsFromLineage, @@ -65,6 +66,14 @@ export interface EntityAPIWrapper { selectionKey?: string, useSnapshotSelection?: boolean ) => Promise; + getOperationConfirmationData: ( + dataType: EntityDataType, + rowIds: string[] | number[], + selectionKey?: string, + useSnapshotSelection?: boolean, + extraParams?: Record, + containerPath?: string + ) => Promise; getOperationConfirmationDataForModel: ( model: QueryModel, dataType: EntityDataType, @@ -113,6 +122,7 @@ export class EntityServerAPIWrapper implements EntityAPIWrapper { getDeleteConfirmationData = getDeleteConfirmationData; getMoveConfirmationData = getMoveConfirmationData; getEntityTypeData = getEntityTypeData; + getOperationConfirmationData = getOperationConfirmationData; getOperationConfirmationDataForModel = getOperationConfirmationDataForModel; getOriginalParentsFromLineage = getOriginalParentsFromLineage; handleEntityFileImport = handleEntityFileImport; @@ -134,6 +144,7 @@ export function getEntityTestAPIWrapper( getDeleteConfirmationData: mockFn(), getMoveConfirmationData: mockFn(), getEntityTypeData: mockFn(), + getOperationConfirmationData: mockFn(), getOperationConfirmationDataForModel: mockFn(), getOriginalParentsFromLineage: mockFn(), handleEntityFileImport: mockFn(), From df507b641aa37f848dc7982ad35dcbe50937e188 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 23 Apr 2024 11:33:00 -0500 Subject: [PATCH 15/31] 3.39.4-fb-crossFolderEditInBulk.2 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 4b1ebc505a..772c63da86 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.1", + "version": "3.39.4-fb-crossFolderEditInBulk.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.1", + "version": "3.39.4-fb-crossFolderEditInBulk.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index a0c5355ced..abea1277cb 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.1", + "version": "3.39.4-fb-crossFolderEditInBulk.2", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From b8247bac73ff6ff14ae9c74db2041738f899007e Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 23 Apr 2024 16:13:06 -0500 Subject: [PATCH 16/31] add getParentTypeDataForLineage to APIWrapper --- .../src/internal/components/entities/APIWrapper.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/components/src/internal/components/entities/APIWrapper.ts b/packages/components/src/internal/components/entities/APIWrapper.ts index 978f04c3d4..c0d0388210 100644 --- a/packages/components/src/internal/components/entities/APIWrapper.ts +++ b/packages/components/src/internal/components/entities/APIWrapper.ts @@ -18,11 +18,13 @@ import { getOperationConfirmationDataForModel, getEntityTypeData, getOriginalParentsFromLineage, + getParentTypeDataForLineage, handleEntityFileImport, moveEntities, initParentOptionsSelects, MoveEntitiesOptions, getCrossFolderSelectionResult, + GetParentTypeDataForLineage, } from './actions'; import { DataOperation } from './constants'; import { @@ -87,6 +89,7 @@ export interface EntityAPIWrapper { originalParents: Record>; parentTypeOptions: Map>; }>; + getParentTypeDataForLineage: GetParentTypeDataForLineage; handleEntityFileImport: ( importAction: string, queryInfo: QueryInfo, @@ -125,6 +128,7 @@ export class EntityServerAPIWrapper implements EntityAPIWrapper { getOperationConfirmationData = getOperationConfirmationData; getOperationConfirmationDataForModel = getOperationConfirmationDataForModel; getOriginalParentsFromLineage = getOriginalParentsFromLineage; + getParentTypeDataForLineage = getParentTypeDataForLineage; handleEntityFileImport = handleEntityFileImport; loadNameExpressionOptions = loadNameExpressionOptions; moveEntities = moveEntities; @@ -147,6 +151,7 @@ export function getEntityTestAPIWrapper( getOperationConfirmationData: mockFn(), getOperationConfirmationDataForModel: mockFn(), getOriginalParentsFromLineage: mockFn(), + getParentTypeDataForLineage: mockFn(), handleEntityFileImport: mockFn(), loadNameExpressionOptions: mockFn(), moveEntities: mockFn(), From a94ef1d3903a957ad96c0936a6b9481eafa2de37 Mon Sep 17 00:00:00 2001 From: cnathe Date: Tue, 23 Apr 2024 16:13:34 -0500 Subject: [PATCH 17/31] 3.39.4-fb-crossFolderEditInBulk.3 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 772c63da86..7b3a2f87ba 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.2", + "version": "3.39.4-fb-crossFolderEditInBulk.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.2", + "version": "3.39.4-fb-crossFolderEditInBulk.3", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index abea1277cb..48db0e9492 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.2", + "version": "3.39.4-fb-crossFolderEditInBulk.3", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From d487fb8ce3527764bd2ecd1db88e56dbf9a26efa Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 24 Apr 2024 15:56:58 -0500 Subject: [PATCH 18/31] add updateRowsByContainer to APIWrapper to conditionally use updateRows or saveRowsByContainer --- .../components/releaseNotes/components.md | 5 +-- .../internal/components/entities/models.ts | 11 +++++++ .../src/internal/query/APIWrapper.ts | 10 ++++++ packages/components/src/internal/query/api.ts | 33 ++++++++++++++++++- 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 810699dde0..7d327d70da 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -7,9 +7,10 @@ Components, models, actions, and utility functions for LabKey applications and p - Update getOperationNotPermittedMessage to work for both Edit in Grid and Edit in Bulk scenarios - BulkUpdateForm getUpdatedData() to include Folder in updated rows, if it exists in originalData - Add getSelectedIds(filterIds) to QueryModel + - saveRowsByContainer prop for containerField to be optional since it has a default - AppendUnitsInput fixes for grid cell rendering and enable/disable in bulk form - - Edit in Grid and Bulk lookup fields to use containerPath based on selected row(s) - - Add getOperationConfirmationData to ApiWrapper + - Edit in Grid and Bulk lookup fields to use containerPath based on selected row(s) (for BulkUpdateForm, disable lookup fields toggle when more than one containerPath in selection) + - Add getOperationConfirmationData and getParentTypeDataForLineage to ApiWrapper ### version 3.39.4 *Released*: 19 April 2024 diff --git a/packages/components/src/internal/components/entities/models.ts b/packages/components/src/internal/components/entities/models.ts index 1d03bd1f75..fd7756c058 100644 --- a/packages/components/src/internal/components/entities/models.ts +++ b/packages/components/src/internal/components/entities/models.ts @@ -531,10 +531,17 @@ export interface EntityDataType { uniqueFieldKey: string; } +interface OperationContainerInfo { + id: string; + path: string; + permitted: boolean; +} + export class OperationConfirmationData { [immerable]: true; readonly allowed: any[]; + readonly containers: OperationContainerInfo[]; readonly notAllowed: any[]; readonly notPermitted: any[]; // could intersect both allowed and notAllowed readonly idMap: Record; @@ -606,6 +613,10 @@ export class OperationConfirmationData { get anyNotActionable(): boolean { return this.totalNotActionable > 0; } + + getContainerPaths(permittedOnly = true): string[] { + return this.containers?.filter(c => !permittedOnly || c.permitted).map(c => c.id) ?? []; + } } export interface CrossFolderSelectionResult { diff --git a/packages/components/src/internal/query/APIWrapper.ts b/packages/components/src/internal/query/APIWrapper.ts index b1e4ef5e9b..212fbb6cae 100644 --- a/packages/components/src/internal/query/APIWrapper.ts +++ b/packages/components/src/internal/query/APIWrapper.ts @@ -46,6 +46,7 @@ import { QueryCommandResponse, selectDistinctRows, updateRows, + updateRowsByContainer, UpdateRowsOptions, getDefaultVisibleColumns, saveRowsByContainer, @@ -131,6 +132,13 @@ export interface QueryAPIWrapper { ) => Promise; setSnapshotSelections: (key: string, ids: string[] | string, containerPath?: string) => Promise; updateRows: (options: UpdateRowsOptions) => Promise; + updateRowsByContainer: ( + schemaQuery: SchemaQuery, + rows: any[], + containerPaths: string[], + auditUserComment: string, + containerField?: string + ) => Promise; } export class QueryServerAPIWrapper implements QueryAPIWrapper { @@ -157,6 +165,7 @@ export class QueryServerAPIWrapper implements QueryAPIWrapper { setSelected = setSelected; setSnapshotSelections = setSnapshotSelections; updateRows = updateRows; + updateRowsByContainer = updateRowsByContainer; getDefaultVisibleColumns = getDefaultVisibleColumns; } @@ -191,6 +200,7 @@ export function getQueryTestAPIWrapper( setSelected: mockFn(), setSnapshotSelections: mockFn(), updateRows: mockFn(), + updateRowsByContainer: mockFn(), getDefaultVisibleColumns: mockFn(), ...overrides, }; diff --git a/packages/components/src/internal/query/api.ts b/packages/components/src/internal/query/api.ts index 3b73362dcc..6ffe0a28a5 100644 --- a/packages/components/src/internal/query/api.ts +++ b/packages/components/src/internal/query/api.ts @@ -16,7 +16,7 @@ import { fromJS, List, Map, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable'; import { immerable } from 'immer'; import { normalize, schema } from 'normalizr'; -import { Ajax, Filter, Query, QueryDOM, Utils } from '@labkey/api'; +import {Ajax, AuditBehaviorTypes, Filter, Query, QueryDOM, Utils} from '@labkey/api'; import { ExtendedMap } from '../../public/ExtendedMap'; @@ -948,6 +948,37 @@ export function updateRows(options: UpdateRowsOptions): Promise { + // if all rows are in the same container, we can use updateRows (which supports file/attachments) + if (containerPaths.length < 2) { + return updateRows({ + containerPath: containerPaths?.[0], + auditBehavior: AuditBehaviorTypes.DETAILED, + auditUserComment, + rows, + schemaQuery, + }); + } else { + const commands = []; + commands.push({ + command: 'update', + schemaName: schemaQuery.schemaName, + queryName: schemaQuery.queryName, + rows, + auditBehavior: AuditBehaviorTypes.DETAILED, + auditUserComment, + skipReselectRows: true, + }); + return saveRowsByContainer({ commands }, containerField); + } +} + export interface DeleteRowsOptions extends Omit { schemaQuery: SchemaQuery; } From f5931a7764a6042af0e8670f9b031de49598735f Mon Sep 17 00:00:00 2001 From: cnathe Date: Wed, 24 Apr 2024 15:58:34 -0500 Subject: [PATCH 19/31] 3.39.4-fb-crossFolderEditInBulk.4 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 7b3a2f87ba..6f4c762589 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.3", + "version": "3.39.4-fb-crossFolderEditInBulk.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.3", + "version": "3.39.4-fb-crossFolderEditInBulk.4", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 48db0e9492..f299da79c9 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.4-fb-crossFolderEditInBulk.3", + "version": "3.39.4-fb-crossFolderEditInBulk.4", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From be021e767959edfea909527a7ddadbc4a074e9e7 Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 25 Apr 2024 08:29:24 -0500 Subject: [PATCH 20/31] BulkUpdateForm to disable file files toggle when more than one containerPath in selection --- packages/components/releaseNotes/components.md | 2 +- .../internal/components/forms/BulkUpdateForm.tsx | 5 +++-- .../internal/components/forms/QueryFormInputs.tsx | 15 +++++++++++---- .../internal/components/forms/QueryInfoForm.tsx | 3 ++- .../internal/components/forms/input/FileInput.tsx | 5 ++++- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index 7d327d70da..87f2cec05c 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -9,7 +9,7 @@ Components, models, actions, and utility functions for LabKey applications and p - Add getSelectedIds(filterIds) to QueryModel - saveRowsByContainer prop for containerField to be optional since it has a default - AppendUnitsInput fixes for grid cell rendering and enable/disable in bulk form - - Edit in Grid and Bulk lookup fields to use containerPath based on selected row(s) (for BulkUpdateForm, disable lookup fields toggle when more than one containerPath in selection) + - Edit in Grid and Bulk lookup fields to use containerPath based on selected row(s) (for BulkUpdateForm, disable lookup fields and file files toggle when more than one containerPath in selection) - Add getOperationConfirmationData and getParentTypeDataForLineage to ApiWrapper ### version 3.39.4 diff --git a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx index 9dfa0dcd21..340caa32af 100644 --- a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx +++ b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx @@ -245,7 +245,7 @@ export class BulkUpdateForm extends PureComponent { // if all selectedIds are from the same containerPath, use that for the lookups via QueryFormInputs > QuerySelect, // otherwise, disable lookup fields const containerPath = containerPaths?.length === 1 ? containerPaths[0] : undefined; - const preventLookupsEnable = containerPaths?.length > 1; + const preventCrossFolderEnable = containerPaths?.length > 1; return ( { containerFilter={containerFilter} containerPath={containerPath} disabled={disabled} - preventLookupsEnable={preventLookupsEnable} + preventCrossFolderEnable={preventCrossFolderEnable} fieldValues={fieldValues} header={this.renderBulkUpdateHeader()} includeCommentField={true} @@ -274,6 +274,7 @@ export class BulkUpdateForm extends PureComponent { showLabelAsterisk submitForEditText="Edit with Grid" submitText={`Update ${capitalizeFirstChar(pluralNoun)}`} + pluralNoun={pluralNoun} title={this.getTitle()} onAdditionalFormDataChange={onAdditionalFormDataChange} /> diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index 459f88f850..edea2d72c3 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -62,7 +62,8 @@ export interface QueryFormInputsProps { onFieldsEnabledChange?: (numEnabled: number) => void; operation?: Operation; onSelectChange?: SelectInputChange; - preventLookupsEnable?: boolean; + pluralNoun?: string; + preventCrossFolderEnable?: boolean; queryColumns?: ExtendedMap; queryFilters?: Record>; queryInfo?: QueryInfo; @@ -160,7 +161,8 @@ export class QueryFormInputs extends React.Component ); diff --git a/packages/components/src/internal/components/forms/QueryInfoForm.tsx b/packages/components/src/internal/components/forms/QueryInfoForm.tsx index 35447b15a2..2e53149539 100644 --- a/packages/components/src/internal/components/forms/QueryInfoForm.tsx +++ b/packages/components/src/internal/components/forms/QueryInfoForm.tsx @@ -107,6 +107,7 @@ export class QueryInfoForm extends PureComponent { isSubmittingText: 'Submitting...', maxCount: MAX_EDITABLE_GRID_ROWS, creationTypeOptions: [], + pluralNoun: 'rows', }; constructor(props: QueryInfoFormProps) { @@ -439,7 +440,7 @@ export class QueryInfoForm extends PureComponent { onCountChange={this.onCountChange} /> {(header || showQuantityHeader) &&
} - + {footer} {showErrorsAtBottom && this.renderError()} {!asModal && this.renderButtons()} diff --git a/packages/components/src/internal/components/forms/input/FileInput.tsx b/packages/components/src/internal/components/forms/input/FileInput.tsx index 259eaddc08..3628b8624f 100644 --- a/packages/components/src/internal/components/forms/input/FileInput.tsx +++ b/packages/components/src/internal/components/forms/input/FileInput.tsx @@ -39,6 +39,7 @@ interface Props extends DisableableInputProps, WithFormsyProps { queryColumn?: QueryColumn; renderFieldLabel?: (queryColumn: QueryColumn, label?: string, description?: string) => ReactNode; showLabel?: boolean; + toggleDisabledTooltip?: string; } interface State extends DisableableInputState { @@ -158,6 +159,7 @@ class FileInputImpl extends DisableableInput { queryColumn, renderFieldLabel, showLabel, + toggleDisabledTooltip, } = this.props; const { data, file, isDisabled, isHover } = this.state; @@ -243,7 +245,8 @@ class FileInputImpl extends DisableableInput { column={queryColumn} isDisabled={isDisabled} toggleProps={{ - onClick: this.toggleDisabled, + onClick: toggleDisabledTooltip ? undefined : this.toggleDisabled, + toolTip: toggleDisabledTooltip, }} /> )} From 836a0c374a02baeb99365fa920e4dc24affa862c Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 25 Apr 2024 08:36:30 -0500 Subject: [PATCH 21/31] 3.39.5-fb-crossFolderEditInBulk.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 70d15c1500..df5029daef 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.5", + "version": "3.39.5-fb-crossFolderEditInBulk.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.5", + "version": "3.39.5-fb-crossFolderEditInBulk.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 8999add5c4..6a7becf5cb 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.5", + "version": "3.39.5-fb-crossFolderEditInBulk.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 329111c0a4aa09689bb4ed5ffc5e03cd309b6156 Mon Sep 17 00:00:00 2001 From: cnathe Date: Thu, 25 Apr 2024 09:18:57 -0500 Subject: [PATCH 22/31] pluralNoun.toLowerCase() --- .../src/internal/components/forms/QueryFormInputs.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index edea2d72c3..0c6fefaddc 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -257,7 +257,7 @@ export class QueryFormInputs extends React.Component Date: Thu, 25 Apr 2024 14:44:13 -0500 Subject: [PATCH 23/31] jest test updates and convert to RTL --- .../components/buttons/ToggleButtons.test.tsx | 20 +++ .../components/forms/BulkUpdateForm.tsx | 2 +- .../components/forms/FieldLabel.spec.tsx | 119 ------------- .../components/forms/FieldLabel.test.tsx | 160 ++++++++++++++++++ 4 files changed, 181 insertions(+), 120 deletions(-) delete mode 100644 packages/components/src/internal/components/forms/FieldLabel.spec.tsx create mode 100644 packages/components/src/internal/components/forms/FieldLabel.test.tsx diff --git a/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx b/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx index 5f5a53a69f..b63c524c15 100644 --- a/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx +++ b/packages/components/src/internal/components/buttons/ToggleButtons.test.tsx @@ -105,6 +105,7 @@ describe('ToggleIcon', () => { userEvent.click(document.getElementsByTagName('i')[0]); expect(onClickFn).toHaveBeenCalledTimes(1); + expect(onClickFn).toHaveBeenCalledWith('on'); }); test('active first item', () => { @@ -125,4 +126,23 @@ describe('ToggleIcon', () => { expect(document.getElementsByClassName('toggle').length).toBe(1); expect(document.getElementsByClassName('test-class').length).toBe(1); }); + + test('disabled', () => { + const onClickFn = jest.fn(); + render(); + + userEvent.click(document.getElementsByTagName('i')[0]); + expect(onClickFn).toHaveBeenCalledTimes(0); + }); + + test('tooltip', () => { + const onClickFn = jest.fn(); + render(); + + expect(document.getElementsByClassName('overlay-trigger').length).toBe(1); + + userEvent.click(document.getElementsByTagName('i')[0]); + expect(onClickFn).toHaveBeenCalledTimes(1); + expect(onClickFn).toHaveBeenCalledWith('on'); + }); }); diff --git a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx index 340caa32af..906c0374da 100644 --- a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx +++ b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx @@ -243,7 +243,7 @@ export class BulkUpdateForm extends PureComponent { isLoadingDataForSelection || !dataForSelection ? undefined : getCommonDataValues(dataForSelection); // if all selectedIds are from the same containerPath, use that for the lookups via QueryFormInputs > QuerySelect, - // otherwise, disable lookup fields + // if selections are from multiple containerPaths, disable the lookup and file field inputs const containerPath = containerPaths?.length === 1 ? containerPaths[0] : undefined; const preventCrossFolderEnable = containerPaths?.length > 1; diff --git a/packages/components/src/internal/components/forms/FieldLabel.spec.tsx b/packages/components/src/internal/components/forms/FieldLabel.spec.tsx deleted file mode 100644 index 8f00a160c3..0000000000 --- a/packages/components/src/internal/components/forms/FieldLabel.spec.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (c) 2019 LabKey Corporation. All rights reserved. No portion of this work may be reproduced in - * any form or by any electronic or mechanical means without written permission from LabKey Corporation. - */ -import React from 'react'; -import renderer from 'react-test-renderer'; -import { mount, shallow } from 'enzyme'; - -import { QueryColumn } from '../../../public/QueryColumn'; - -import { ToggleIcon } from '../buttons/ToggleButtons'; - -import { FieldLabel } from './FieldLabel'; -import { LabelOverlay } from './LabelOverlay'; - -const queryColumn = new QueryColumn({ - name: 'testColumn', - caption: 'test Column', -}); - -describe('FieldLabel', () => { - test("don't show label", () => { - const tree = renderer.create(); - expect(tree === null); - }); - - test('without overlay, with label', () => { - const label = This is the label; - const wrapper = shallow(); - expect(wrapper.find('span.label-span')).toHaveLength(1); - expect(wrapper.find(LabelOverlay)).toHaveLength(0); - }); - - test('without overlay, with column', () => { - const wrapper = shallow(); - expect(wrapper.text()).toContain(queryColumn.caption); - expect(wrapper.find(LabelOverlay)).toHaveLength(0); - }); - - test('with overlay, with label', () => { - const label = This is the label; - const wrapper = shallow(); - expect(wrapper.find(LabelOverlay)).toHaveLength(1); - }); - - test('with overlay, with column', () => { - const wrapper = mount(); - expect(wrapper.text()).toContain(queryColumn.caption); - expect(wrapper.find(LabelOverlay)).toHaveLength(1); - }); - - function verifyToggle(wrapper, classNames?: string[]) { - expect(wrapper.find(ToggleIcon)).toHaveLength(1); - if (classNames?.length > 0) { - classNames.forEach(className => expect(wrapper.find('.' + className)).toHaveLength(1)); - } - } - - test('showToggle', () => { - const wrapper = shallow(); - verifyToggle(wrapper); - }); - - test('showToggle, with labelOverlayProps, not formsy', () => { - const label = 'This is the label'; - const props = { - label, - isFormsy: false, - }; - const wrapper = shallow(); - verifyToggle(wrapper, ['control-label-toggle-input', 'control-label-toggle-input-size-fixed', 'col-xs-1']); - }); - - test('showToggle, with labelOverlayProps, formsy', () => { - const label = 'This is the label'; - const props = { - label, - isFormsy: true, - }; - const wrapper = shallow(); - verifyToggle(wrapper); - }); - - test('showToggle, with labelOverlayProps, formsy, with toggleClassName', () => { - const label = 'This is the label'; - const props = { - label, - isFormsy: true, - }; - const wrapper = shallow( - - ); - verifyToggle(wrapper, ['toggle-wrapper']); - }); - - test('showToggle, with labelOverlayProps, not formsy, with toggleClassName', () => { - const label = 'This is the label'; - const props = { - label, - isFormsy: false, - }; - const wrapper = shallow( - - ); - verifyToggle(wrapper, ['toggle-wrapper', 'col-xs-1']); - }); -}); diff --git a/packages/components/src/internal/components/forms/FieldLabel.test.tsx b/packages/components/src/internal/components/forms/FieldLabel.test.tsx new file mode 100644 index 0000000000..2edf9e4433 --- /dev/null +++ b/packages/components/src/internal/components/forms/FieldLabel.test.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import Formsy from 'formsy-react'; + +import { QueryColumn } from '../../../public/QueryColumn'; + +import { FieldLabel } from './FieldLabel'; + +const queryColumn = new QueryColumn({ + name: 'testColumn', + caption: 'test Column', +}); + +describe('FieldLabel', () => { + beforeAll(() => { + console.warn = jest.fn(); + }); + + test("don't show label", () => { + render(); + expect(document.body.textContent).toBe(''); + }); + + test('without overlay, with label', () => { + const label = This is the label; + render(); + expect(document.querySelector('span.label-span').textContent).toBe('This is the label'); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(0); + }); + + test('without overlay, with column', () => { + render(); + expect(document.body.textContent).toBe(queryColumn.caption); + expect(document.querySelectorAll('.span.label-span')).toHaveLength(0); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(0); + }); + + test('with overlay, with label', () => { + const label = This is the label; + render(); + expect(document.body.textContent).toBe(' '); + expect(document.querySelectorAll('.span.label-span')).toHaveLength(0); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); + + test('with overlay, with column', () => { + render(); + expect(document.body.textContent).toBe(queryColumn.caption + ' '); + expect(document.querySelectorAll('.span.label-span')).toHaveLength(0); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); + + test('showToggle', () => { + render( + + + + ); + expect(document.querySelectorAll('.toggle')).toHaveLength(1); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); + + test('showToggle, with labelOverlayProps, not formsy', () => { + const label = 'This is the label'; + const props = { + label, + isFormsy: false, + }; + render( + + + + ); + expect(document.querySelectorAll('.toggle')).toHaveLength(1); + expect(document.querySelectorAll('.control-label-toggle-input')).toHaveLength(1); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); + + test('showToggle, with labelOverlayProps, formsy', () => { + const label = 'This is the label'; + const props = { + label, + isFormsy: true, + }; + render( + + + + ); + expect(document.querySelectorAll('.toggle')).toHaveLength(1); + expect(document.querySelectorAll('.control-label-toggle-input')).toHaveLength(1); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); + + test('showToggle, with labelOverlayProps, formsy, with toggleClassName', () => { + const label = 'This is the label'; + const props = { + label, + isFormsy: true, + }; + render( + + + + ); + expect(document.querySelectorAll('.toggle')).toHaveLength(1); + expect(document.querySelectorAll('.toggle-wrapper')).toHaveLength(1); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); + + test('showToggle, with labelOverlayProps, not formsy, with toggleClassName', () => { + const label = 'This is the label'; + const props = { + label, + isFormsy: false, + }; + render( + + + + ); + expect(document.querySelectorAll('.toggle')).toHaveLength(1); + expect(document.querySelectorAll('.toggle-wrapper')).toHaveLength(1); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); + + test('showToggle, toggleProps disabled', () => { + render( + + + + ); + expect(document.querySelectorAll('.toggle')).toHaveLength(1); + expect(document.querySelectorAll('.disabled')).toHaveLength(1); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(2); + }); + + test('showToggle, toggleProps not disabled', () => { + render( + + + + ); + expect(document.querySelectorAll('.toggle')).toHaveLength(1); + expect(document.querySelectorAll('.disabled')).toHaveLength(0); + expect(document.querySelectorAll('.overlay-trigger')).toHaveLength(1); + }); +}); From adc5f0ec8fe2a69047cd7ad549fe9d0a7f0306bb Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 26 Apr 2024 09:55:28 -0500 Subject: [PATCH 24/31] CR feedback - check for "Container" if "Folder" not found, don't disable bulk form lookup if col.lookup.containerPath defined --- .../src/internal/components/editable/LookupCell.tsx | 5 ++++- .../src/internal/components/forms/BulkUpdateForm.tsx | 3 ++- .../src/internal/components/forms/QueryFormInputs.tsx | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/components/src/internal/components/editable/LookupCell.tsx b/packages/components/src/internal/components/editable/LookupCell.tsx index 31ca477fda..57d8a72221 100644 --- a/packages/components/src/internal/components/editable/LookupCell.tsx +++ b/packages/components/src/internal/components/editable/LookupCell.tsx @@ -161,7 +161,10 @@ export const LookupCell: FC = memo(props => { } // if the column is a lookup, we need to pass the containerPath to the QuerySelect - const containerPath_ = containerPath ?? getValueFromRow(row?.toJS(), 'Folder')?.toString(); + const containerPath_ = + containerPath ?? + getValueFromRow(row?.toJS(), 'Folder')?.toString() ?? + getValueFromRow(row?.toJS(), 'Container')?.toString(); return ( { let conflictKeys = new Set(); data.forEach((rowData, id) => { if (rowData) { - const containerPath = caseInsensitive(rowData.toJS(), 'Folder'); + const containerPath = + caseInsensitive(rowData.toJS(), 'Folder') ?? caseInsensitive(rowData.toJS(), 'Container'); if (containerPath?.value) containerPaths.add(containerPath.value); if (displayValueFields) { diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index 0c6fefaddc..6c9307c47d 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -256,7 +256,7 @@ export class QueryFormInputs extends React.Component Date: Fri, 26 Apr 2024 09:59:17 -0500 Subject: [PATCH 25/31] 3.39.6-fb-crossFolderEditInBulk.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 859047b50c..4fbc525f30 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.6", + "version": "3.39.6-fb-crossFolderEditInBulk.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.6", + "version": "3.39.6-fb-crossFolderEditInBulk.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index e1018c014a..354edcbbda 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.6", + "version": "3.39.6-fb-crossFolderEditInBulk.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 330357cd57971171fe3420fba8a140ce72e17f10 Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 26 Apr 2024 10:59:13 -0500 Subject: [PATCH 26/31] jest test updates --- .../components/entities/models.test.ts | 148 +++++++++++++++++- .../src/internal/util/utils.test.ts | 96 +++++++++++- .../components/src/internal/util/utils.ts | 2 +- .../src/public/QueryModel/QueryModel.test.ts | 14 ++ 4 files changed, 257 insertions(+), 3 deletions(-) diff --git a/packages/components/src/internal/components/entities/models.test.ts b/packages/components/src/internal/components/entities/models.test.ts index c83f3f913a..5ad300b127 100644 --- a/packages/components/src/internal/components/entities/models.test.ts +++ b/packages/components/src/internal/components/entities/models.test.ts @@ -18,7 +18,7 @@ import { List } from 'immutable'; import { SCHEMAS } from '../../schemas'; import { QueryColumn } from '../../../public/QueryColumn'; -import { EntityIdCreationModel, EntityParentType, EntityTypeOption } from './models'; +import { EntityIdCreationModel, EntityParentType, EntityTypeOption, OperationConfirmationData } from './models'; import { SampleTypeDataType } from './constants'; describe('EntityParentType', () => { @@ -212,3 +212,149 @@ describe('EntityIdCreationModel', () => { expect(sq.getSchemaQuery().queryName).toBe('a'); }); }); + +describe('OperationConfirmationData', () => { + const data = new OperationConfirmationData({ + allowed: [{ rowId: 1 }, { rowId: 2 }], + notAllowed: [{ rowId: 3 }, { rowId: 4 }, { rowId: 5 }], + notPermitted: [{ rowId: 6 }], + }); + + test('isIdActionable', () => { + expect(data.isIdActionable('1')).toBe(true); + expect(data.isIdActionable(1)).toBe(true); + expect(data.isIdActionable('3')).toBe(false); + expect(data.isIdActionable(3)).toBe(false); + expect(data.isIdActionable('6')).toBe(false); + expect(data.isIdActionable(6)).toBe(false); + }); + + test('getActionableIds', () => { + expect(data.getActionableIds()).toEqual([1, 2]); + expect(data.getActionableIds('bogus')).toEqual([]); + }); + + test('allActionable', () => { + expect(data.allActionable).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [{ rowId: 1 }, { rowId: 2 }], + notAllowed: [], + notPermitted: [], + }).allActionable + ).toBe(true); + }); + + test('noneActionable', () => { + expect(data.noneActionable).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [], + notPermitted: [], + }).noneActionable + ).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [{ rowId: 1 }, { rowId: 2 }], + notAllowed: [], + notPermitted: [], + }).noneActionable + ).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [{ rowId: 1 }, { rowId: 2 }], + notPermitted: [], + }).noneActionable + ).toBe(true); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [], + notPermitted: [{ rowId: 1 }, { rowId: 2 }], + }).noneActionable + ).toBe(true); + }); + + test('anyActionable', () => { + expect(data.anyActionable).toBe(true); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [], + notPermitted: [], + }).anyActionable + ).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [{ rowId: 1 }, { rowId: 2 }], + notAllowed: [], + notPermitted: [], + }).anyActionable + ).toBe(true); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [{ rowId: 1 }, { rowId: 2 }], + notPermitted: [], + }).anyActionable + ).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [], + notPermitted: [{ rowId: 1 }, { rowId: 2 }], + }).anyActionable + ).toBe(false); + }); + + test('anyNotActionable', () => { + expect(data.anyNotActionable).toBe(true); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [], + notPermitted: [], + }).anyNotActionable + ).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [{ rowId: 1 }, { rowId: 2 }], + notAllowed: [], + notPermitted: [], + }).anyNotActionable + ).toBe(false); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [{ rowId: 1 }, { rowId: 2 }], + notPermitted: [], + }).anyNotActionable + ).toBe(true); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [], + notPermitted: [{ rowId: 1 }, { rowId: 2 }], + }).anyNotActionable + ).toBe(true); + }); + + test('totalCount', () => { + expect(data.totalCount).toBe(5); + expect( + new OperationConfirmationData({ + allowed: [], + notAllowed: [], + notPermitted: [], + }).totalCount + ).toBe(0); + }); + + test('getContainerPaths', () => { + expect(new OperationConfirmationData({ + containers: [{ id: 'a', permitted: true }, { id: 'b', permitted: false }, { id: 'c', permitted: true }] + }).getContainerPaths()).toEqual(['a', 'c']); + }); +}); diff --git a/packages/components/src/internal/util/utils.test.ts b/packages/components/src/internal/util/utils.test.ts index 2ea0a85fb1..a4861f7160 100644 --- a/packages/components/src/internal/util/utils.test.ts +++ b/packages/components/src/internal/util/utils.test.ts @@ -711,7 +711,7 @@ describe('getUpdatedData', () => { }); }); - test('with additionalPkCols', () => { + test('with additionalCols', () => { const updatedData = getUpdatedData( originalData, { @@ -741,6 +741,100 @@ describe('getUpdatedData', () => { Data: 'data1', }); }); + + test('with folder', () => { + const originalData_ = fromJS({ + '448': { + RowId: { + value: 448, + url: '/labkey/Sample%20Management/experiment-showMaterial.view?rowId=448', + }, + Value: { + value: null, + }, + Data: { + value: 'data1', + }, + 'And/Again': { + value: 'again', + }, + Name: { + value: 'S-20190516-9042', + url: '/labkey/Sample%20Management/experiment-showMaterial.view?rowId=448', + }, + Other: { + value: 'other1', + }, + Folder: { + displayValue: 'ProjectA', + value: 'ENTITYID-A', + }, + }, + }); + + const updatedData = getUpdatedData( + originalData_, + { + Value: 'val', + And$SAgain: 'again', + Other: 'other3', + }, + List(['RowId']), + ); + expect(updatedData[0]).toStrictEqual({ + RowId: 448, + Value: 'val', + Other: 'other3', + Folder: 'ENTITYID-A', + }); + }); + + test('with container', () => { + const originalData_ = fromJS({ + '448': { + RowId: { + value: 448, + url: '/labkey/Sample%20Management/experiment-showMaterial.view?rowId=448', + }, + Value: { + value: null, + }, + Data: { + value: 'data1', + }, + 'And/Again': { + value: 'again', + }, + Name: { + value: 'S-20190516-9042', + url: '/labkey/Sample%20Management/experiment-showMaterial.view?rowId=448', + }, + Other: { + value: 'other1', + }, + Container: { + displayValue: 'ProjectA', + value: 'ENTITYID-A', + }, + }, + }); + + const updatedData = getUpdatedData( + originalData_, + { + Value: 'val', + And$SAgain: 'again', + Other: 'other3', + }, + List(['RowId']), + ); + expect(updatedData[0]).toStrictEqual({ + RowId: 448, + Value: 'val', + Other: 'other3', + Container: 'ENTITYID-A', + }); + }); }); describe('CaseInsensitive', () => { diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index ee680d249c..2db26f43c3 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -314,7 +314,7 @@ export function getUpdatedData( additionalCols?.forEach(col => pkColsLc.add(col.toLowerCase())); // if the originalData has the container/folder values, keep those as well (i.e. treat it as a primary key) - const folderKey = originalData.first().keySeq().find(key => key.toLowerCase() === 'folder'); + const folderKey = originalData.first().keySeq().find(key => key.toLowerCase() === 'folder' || key.toLowerCase() === 'container'); if (folderKey) pkColsLc.add(folderKey.toLowerCase()); const updatedData = originalData.map(originalRowMap => { diff --git a/packages/components/src/public/QueryModel/QueryModel.test.ts b/packages/components/src/public/QueryModel/QueryModel.test.ts index 13c6d73eb5..a6fb5e8264 100644 --- a/packages/components/src/public/QueryModel/QueryModel.test.ts +++ b/packages/components/src/public/QueryModel/QueryModel.test.ts @@ -235,6 +235,20 @@ describe('QueryModel', () => { expect(model.getSelectedIdsAsInts()[2]).toBe(2); }); + test('getSelectedIds', () => { + let model = new QueryModel({ schemaQuery: SCHEMA_QUERY }); + expect(model.getSelectedIds()).toBe(undefined); + model = model.mutate({ selections: new Set([]) }); + expect(model.getSelectedIds().length).toBe(0); + model = model.mutate({ selections: new Set(['1', '3', '2']) }); + expect(model.getSelectedIds().length).toBe(3); + expect(model.getSelectedIds()[0]).toBe('1'); + expect(model.getSelectedIds()[1]).toBe('3'); + expect(model.getSelectedIds()[2]).toBe('2'); + expect(model.getSelectedIds([2, 3]).length).toBe(1); + expect(model.getSelectedIds()[0]).toBe('1'); + }); + test('filters', () => { const viewName = 'TEST_VIEW'; const view = ViewInfo.fromJson({ From 1f92457dfb41142c603e6d0b91018ce635fda89a Mon Sep 17 00:00:00 2001 From: cnathe Date: Fri, 26 Apr 2024 14:58:07 -0500 Subject: [PATCH 27/31] QueryFormInputs lookup update to add check for isAllProductFoldersFilteringEnabled when toggleDisabledTooltip --- .../internal/components/forms/QueryFormInputs.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index 6c9307c47d..ab760fc37b 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -39,6 +39,7 @@ import { DatePickerInput } from './input/DatePickerInput'; import { TextChoiceInput } from './input/TextChoiceInput'; import { getQueryFormLabelFieldName, isQueryFormLabelField } from './utils'; +import {isAllProductFoldersFilteringEnabled} from "../../app/utils"; export interface QueryFormInputsProps { allowFieldDisable?: boolean; @@ -243,6 +244,12 @@ export class QueryFormInputs extends React.Component {this.renderLabelField(col)} @@ -255,11 +262,7 @@ export class QueryFormInputs extends React.Component Date: Fri, 26 Apr 2024 14:58:22 -0500 Subject: [PATCH 28/31] 3.39.6-fb-crossFolderEditInBulk.1 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 4fbc525f30..f137773fe2 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.6-fb-crossFolderEditInBulk.0", + "version": "3.39.6-fb-crossFolderEditInBulk.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.6-fb-crossFolderEditInBulk.0", + "version": "3.39.6-fb-crossFolderEditInBulk.1", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index 354edcbbda..d4dda5309f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.6-fb-crossFolderEditInBulk.0", + "version": "3.39.6-fb-crossFolderEditInBulk.1", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ From 05b04f1aaa63960c87f100cddbce56d251bd59d8 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 29 Apr 2024 09:12:35 -0500 Subject: [PATCH 29/31] Update release notes with version number and release date --- packages/components/releaseNotes/components.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index b3536c1ec8..8aceda3ef1 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,8 +1,8 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages. -### version TBD -*Released*: TBD +### version 3.40.0 +*Released*: 29 April 2024 - Support cross-folder "Edit in Bulk" - Update getOperationNotPermittedMessage to work for both Edit in Grid and Edit in Bulk scenarios - BulkUpdateForm getUpdatedData() to include Folder in updated rows, if it exists in originalData From 5ca037d107e034c9e99ab2b49ba3b29bed19b9f4 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 29 Apr 2024 09:15:38 -0500 Subject: [PATCH 30/31] npm run lint-branch-fix --- .../components/buttons/ToggleButtons.tsx | 2 +- .../components/editable/LookupCell.tsx | 3 +- .../components/entities/models.test.ts | 12 +++-- .../components/forms/BulkUpdateForm.tsx | 2 +- .../components/forms/QueryFormInputs.tsx | 15 ++++--- .../components/forms/QueryInfoForm.tsx | 45 ++++++++++++------- packages/components/src/internal/query/api.ts | 2 +- .../src/internal/util/utils.test.ts | 4 +- .../components/src/internal/util/utils.ts | 5 ++- .../src/public/QueryModel/QueryModel.ts | 3 +- 10 files changed, 61 insertions(+), 32 deletions(-) diff --git a/packages/components/src/internal/components/buttons/ToggleButtons.tsx b/packages/components/src/internal/components/buttons/ToggleButtons.tsx index 2df7310df8..11998db7d1 100644 --- a/packages/components/src/internal/components/buttons/ToggleButtons.tsx +++ b/packages/components/src/internal/components/buttons/ToggleButtons.tsx @@ -2,7 +2,7 @@ import React, { FC, memo, useCallback } from 'react'; import classNames from 'classnames'; import { FormsyInput } from '../forms/input/FormsyReactComponents'; -import { LabelHelpTip } from "../base/LabelHelpTip"; +import { LabelHelpTip } from '../base/LabelHelpTip'; interface Props { active: string; diff --git a/packages/components/src/internal/components/editable/LookupCell.tsx b/packages/components/src/internal/components/editable/LookupCell.tsx index 57d8a72221..8a8c66de4c 100644 --- a/packages/components/src/internal/components/editable/LookupCell.tsx +++ b/packages/components/src/internal/components/editable/LookupCell.tsx @@ -30,11 +30,12 @@ import { QuerySelect } from '../forms/QuerySelect'; import { getQueryColumnRenderers } from '../../global'; +import { getValueFromRow } from '../../util/utils'; + import { MODIFICATION_TYPES, SELECTION_TYPES } from './constants'; import { ValueDescriptor } from './models'; import { getLookupFilters, gridCellSelectInputProps, onCellSelectChange } from './utils'; -import { getValueFromRow } from "../../util/utils"; export interface LookupCellProps { col: QueryColumn; diff --git a/packages/components/src/internal/components/entities/models.test.ts b/packages/components/src/internal/components/entities/models.test.ts index 5ad300b127..3709099ecf 100644 --- a/packages/components/src/internal/components/entities/models.test.ts +++ b/packages/components/src/internal/components/entities/models.test.ts @@ -353,8 +353,14 @@ describe('OperationConfirmationData', () => { }); test('getContainerPaths', () => { - expect(new OperationConfirmationData({ - containers: [{ id: 'a', permitted: true }, { id: 'b', permitted: false }, { id: 'c', permitted: true }] - }).getContainerPaths()).toEqual(['a', 'c']); + expect( + new OperationConfirmationData({ + containers: [ + { id: 'a', permitted: true }, + { id: 'b', permitted: false }, + { id: 'c', permitted: true }, + ], + }).getContainerPaths() + ).toEqual(['a', 'c']); }); }); diff --git a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx index 942a26855e..c837963466 100644 --- a/packages/components/src/internal/components/forms/BulkUpdateForm.tsx +++ b/packages/components/src/internal/components/forms/BulkUpdateForm.tsx @@ -9,7 +9,7 @@ import { SchemaQuery } from '../../../public/SchemaQuery'; import { getSelectedData } from '../../actions'; -import {capitalizeFirstChar, caseInsensitive, getCommonDataValues, getUpdatedData} from '../../util/utils'; +import { capitalizeFirstChar, caseInsensitive, getCommonDataValues, getUpdatedData } from '../../util/utils'; import { QueryInfoForm } from './QueryInfoForm'; diff --git a/packages/components/src/internal/components/forms/QueryFormInputs.tsx b/packages/components/src/internal/components/forms/QueryFormInputs.tsx index ab760fc37b..00e4bc91f3 100644 --- a/packages/components/src/internal/components/forms/QueryFormInputs.tsx +++ b/packages/components/src/internal/components/forms/QueryFormInputs.tsx @@ -27,6 +27,8 @@ import { caseInsensitive } from '../../util/utils'; import { getContainerFilterForLookups } from '../../query/api'; +import { isAllProductFoldersFilteringEnabled } from '../../app/utils'; + import { FormsyInput } from './input/FormsyReactComponents'; import { resolveInputRenderer } from './input/InputRenderFactory'; import { QuerySelect } from './QuerySelect'; @@ -39,7 +41,6 @@ import { DatePickerInput } from './input/DatePickerInput'; import { TextChoiceInput } from './input/TextChoiceInput'; import { getQueryFormLabelFieldName, isQueryFormLabelField } from './utils'; -import {isAllProductFoldersFilteringEnabled} from "../../app/utils"; export interface QueryFormInputsProps { allowFieldDisable?: boolean; @@ -49,7 +50,8 @@ export interface QueryFormInputsProps { columnFilter?: (col?: QueryColumn) => boolean; // this can be used when you want to keep certain columns always filtered out (e.g., aliquot- or sample-only columns) isIncludedColumn?: (col: QueryColumn) => boolean; - componentKey?: string; // unique key to add to QuerySelect to avoid duplication w/ transpose + componentKey?: string; + // unique key to add to QuerySelect to avoid duplication w/ transpose /** A container filter that will be applied to all query-based inputs in this form */ containerFilter?: Query.ContainerFilter; containerPath?: string; @@ -246,9 +248,12 @@ export class QueryFormInputs extends React.Component diff --git a/packages/components/src/internal/components/forms/QueryInfoForm.tsx b/packages/components/src/internal/components/forms/QueryInfoForm.tsx index 2e53149539..12280af3bb 100644 --- a/packages/components/src/internal/components/forms/QueryInfoForm.tsx +++ b/packages/components/src/internal/components/forms/QueryInfoForm.tsx @@ -32,28 +32,30 @@ import { formatDate, formatDateTime } from '../../util/Date'; import { Alert } from '../base/Alert'; import { LoadingSpinner } from '../base/LoadingSpinner'; +import { getAppHomeFolderPath } from '../../app/utils'; + +import { ComponentsAPIWrapper, getDefaultAPIWrapper } from '../../APIWrapper'; + import { QueryInfoQuantity } from './QueryInfoQuantity'; import { QueryFormInputs, QueryFormInputsProps } from './QueryFormInputs'; import { getFieldEnabledFieldName } from './utils'; import { CommentTextArea } from './input/CommentTextArea'; -import { getAppHomeFolderPath } from '../../app/utils'; -import { ComponentsAPIWrapper, getDefaultAPIWrapper } from '../../APIWrapper'; export interface QueryInfoFormProps extends Omit { - api?: ComponentsAPIWrapper, + api?: ComponentsAPIWrapper; asModal?: boolean; canSubmitNotDirty?: boolean; cancelText?: string; countText?: string; - disabled?: boolean; creationTypeOptions?: SampleCreationTypeModel[]; + disabled?: boolean; errorCallback?: (error: any) => void; errorMessagePrefix?: string; footer?: ReactNode; header?: ReactNode; hideButtons?: boolean; - includeCountField?: boolean; includeCommentField?: boolean; + includeCountField?: boolean; isLoading?: boolean; isSubmittedText?: string; isSubmittingText?: string; @@ -72,22 +74,22 @@ export interface QueryInfoFormProps extends Omit { if (includeCommentField) { (async () => { try { - const {container} = getServerContext(); + const { container } = getServerContext(); const response = await api.folder.getAuditSettings(getAppHomeFolderPath(new Container(container))); - this.setState({requiresUserComment: !!response?.requireUserComments}); - } - catch { - this.setState({requiresUserComment: false}); + this.setState({ requiresUserComment: !!response?.requireUserComments }); + } catch { + this.setState({ requiresUserComment: false }); } })(); } @@ -292,7 +293,7 @@ export class QueryInfoForm extends PureComponent { onCommentChange = (comment: string): void => { this.setState({ comment }); - } + }; onFieldsEnabledChange = (fieldEnabledCount: number): void => { this.setState({ fieldEnabledCount }); @@ -317,7 +318,17 @@ export class QueryInfoForm extends PureComponent { hideButtons, } = this.props; - const { count, comment, canSubmit, fieldEnabledCount, isSubmitting, isSubmitted, submitForEdit, isDirty, requiresUserComment } = this.state; + const { + count, + comment, + canSubmit, + fieldEnabledCount, + isSubmitting, + isSubmitted, + submitForEdit, + isDirty, + requiresUserComment, + } = this.state; if (hideButtons) return null; @@ -440,7 +451,11 @@ export class QueryInfoForm extends PureComponent { onCountChange={this.onCountChange} /> {(header || showQuantityHeader) &&
} - + {footer} {showErrorsAtBottom && this.renderError()} {!asModal && this.renderButtons()} diff --git a/packages/components/src/internal/query/api.ts b/packages/components/src/internal/query/api.ts index 6ffe0a28a5..2b648d0527 100644 --- a/packages/components/src/internal/query/api.ts +++ b/packages/components/src/internal/query/api.ts @@ -16,7 +16,7 @@ import { fromJS, List, Map, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable'; import { immerable } from 'immer'; import { normalize, schema } from 'normalizr'; -import {Ajax, AuditBehaviorTypes, Filter, Query, QueryDOM, Utils} from '@labkey/api'; +import { Ajax, AuditBehaviorTypes, Filter, Query, QueryDOM, Utils } from '@labkey/api'; import { ExtendedMap } from '../../public/ExtendedMap'; diff --git a/packages/components/src/internal/util/utils.test.ts b/packages/components/src/internal/util/utils.test.ts index a4861f7160..c7a3838759 100644 --- a/packages/components/src/internal/util/utils.test.ts +++ b/packages/components/src/internal/util/utils.test.ts @@ -779,7 +779,7 @@ describe('getUpdatedData', () => { And$SAgain: 'again', Other: 'other3', }, - List(['RowId']), + List(['RowId']) ); expect(updatedData[0]).toStrictEqual({ RowId: 448, @@ -826,7 +826,7 @@ describe('getUpdatedData', () => { And$SAgain: 'again', Other: 'other3', }, - List(['RowId']), + List(['RowId']) ); expect(updatedData[0]).toStrictEqual({ RowId: 448, diff --git a/packages/components/src/internal/util/utils.ts b/packages/components/src/internal/util/utils.ts index 2db26f43c3..deae3b677c 100644 --- a/packages/components/src/internal/util/utils.ts +++ b/packages/components/src/internal/util/utils.ts @@ -314,7 +314,10 @@ export function getUpdatedData( additionalCols?.forEach(col => pkColsLc.add(col.toLowerCase())); // if the originalData has the container/folder values, keep those as well (i.e. treat it as a primary key) - const folderKey = originalData.first().keySeq().find(key => key.toLowerCase() === 'folder' || key.toLowerCase() === 'container'); + const folderKey = originalData + .first() + .keySeq() + .find(key => key.toLowerCase() === 'folder' || key.toLowerCase() === 'container'); if (folderKey) pkColsLc.add(folderKey.toLowerCase()); const updatedData = originalData.map(originalRowMap => { diff --git a/packages/components/src/public/QueryModel/QueryModel.ts b/packages/components/src/public/QueryModel/QueryModel.ts index 0958f5cde2..24cfa34699 100644 --- a/packages/components/src/public/QueryModel/QueryModel.ts +++ b/packages/components/src/public/QueryModel/QueryModel.ts @@ -603,10 +603,9 @@ export class QueryModel { if (excludeViewFilters) { // Issue 49634: LKSM: Saving search filters in default grid view has odd behavior const searchViewFilters = this.viewFilters.filter(f => f.getColumnName() === '*'); - return [...baseFilters, ...searchViewFilters] + return [...baseFilters, ...searchViewFilters]; } return [...baseFilters, ...this.viewFilters]; - } get modelFilters(): Filter.IFilter[] { From e770d3cf38d69bdca92b422bc6b6a6cf1bdfd433 Mon Sep 17 00:00:00 2001 From: cnathe Date: Mon, 29 Apr 2024 09:16:05 -0500 Subject: [PATCH 31/31] 3.40.0 --- packages/components/package-lock.json | 4 ++-- packages/components/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index f137773fe2..d5d04429e4 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "3.39.6-fb-crossFolderEditInBulk.1", + "version": "3.40.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "3.39.6-fb-crossFolderEditInBulk.1", + "version": "3.40.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@labkey/api": "1.33.0", diff --git a/packages/components/package.json b/packages/components/package.json index d4dda5309f..25f7d98b0d 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "3.39.6-fb-crossFolderEditInBulk.1", + "version": "3.40.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [