Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/components/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/components/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@labkey/components",
"version": "7.23.3",
"version": "7.23.4-fb-mv-edit-960.0",
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
"sideEffects": false,
"files": [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { fromJS } from 'immutable';
import { fromJS, List } from 'immutable';

import { QueryColumn } from '../../../../public/QueryColumn';
import { QueryInfo } from '../../../../public/QueryInfo';
Expand Down Expand Up @@ -26,47 +26,55 @@ const COLUMN_FILE_INPUT = new QueryColumn({
inputType: 'file',
jsonType: 'string',
});
const COLUMN_ARRAY_INPUT = new QueryColumn({
fieldKey: 'arrInput',
name: 'arrInput',
fieldKeyArray: ['arrInput'],
inputType: 'text',
jsonType: 'array',
});
const QUERY_INFO = QueryInfo.fromJsonForTests({
name: 'test',
schemaName: 'schema',
columns: {
strInput: COLUMN_STRING_INPUT,
arrInput: COLUMN_ARRAY_INPUT,
dtInput: COLUMN_DATE_INPUT,
fileInput: COLUMN_FILE_INPUT,
strInput: COLUMN_STRING_INPUT,
},
});

describe('extractChanges', () => {
test('file input', () => {
const FILE = new File([], 'file');
const currentData = fromJS({ fileInput: { value: FILE } });
expect(extractChanges(QUERY_INFO, currentData, {}).fileInput).toBe(undefined);
expect(extractChanges(QUERY_INFO, currentData, {}).fileInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { fileInput: undefined }).fileInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { fileInput: FILE }).fileInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { fileInput: null }).fileInput).toBe(null);
expect(extractChanges(QUERY_INFO, currentData, { fileInput: null }).fileInput).toBeNull();
expect(
extractChanges(QUERY_INFO, currentData, { fileInput: new File([], 'fileEdit') }).fileInput
).toBeDefined();
});

test('string input', () => {
const currentData = fromJS({ strInput: { value: 'abc' } });
expect(extractChanges(QUERY_INFO, currentData, {}).strInput).toBe(undefined);
expect(extractChanges(QUERY_INFO, currentData, { strInput: undefined }).strInput).toBe(null);
expect(extractChanges(QUERY_INFO, currentData, { strInput: null }).strInput).toBe(null);
expect(extractChanges(QUERY_INFO, currentData, {}).strInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { strInput: undefined }).strInput).toBeNull();
expect(extractChanges(QUERY_INFO, currentData, { strInput: null }).strInput).toBeNull();
expect(extractChanges(QUERY_INFO, currentData, { strInput: '' }).strInput).toBe('');
expect(extractChanges(QUERY_INFO, currentData, { strInput: [] }).strInput).toStrictEqual([]);
expect(extractChanges(QUERY_INFO, currentData, { strInput: 'abc' }).strInput).toBe(undefined);
expect(extractChanges(QUERY_INFO, currentData, { strInput: ' abc ' }).strInput).toBe(undefined);
expect(extractChanges(QUERY_INFO, currentData, { strInput: 'abc' }).strInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { strInput: ' abc ' }).strInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { strInput: ' abcd ' }).strInput).toBe('abcd');
});

test('date input', () => {
let currentData = fromJS({ dtInput: { value: '2022-08-30 01:02:03' } });
expect(extractChanges(QUERY_INFO, currentData, {}).dtInput).toBe(undefined);
expect(extractChanges(QUERY_INFO, currentData, { dtInput: undefined }).dtInput).toBe(null);
expect(extractChanges(QUERY_INFO, currentData, { dtInput: null }).dtInput).toBe(null);
expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:03' }).dtInput).toBe(undefined);
expect(extractChanges(QUERY_INFO, currentData, {}).dtInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { dtInput: undefined }).dtInput).toBeNull();
expect(extractChanges(QUERY_INFO, currentData, { dtInput: null }).dtInput).toBeNull();
expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:03' }).dtInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:04' }).dtInput).toBe(
'2022-08-30 01:02:04'
); // Issue 40139, 52536: date comparison only down to minute precision
Expand All @@ -81,10 +89,27 @@ describe('extractChanges', () => {
);

currentData = fromJS({ dtInput: { value: '2022-08-30' } });
expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30' }).dtInput).toBe(undefined);
expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30' }).dtInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-31' }).dtInput).toBe('2022-08-31');
expect(extractChanges(QUERY_INFO, currentData, { dtInput: '2022-08-30 01:02:03' }).dtInput).toBe(
'2022-08-30 01:02:03'
);
});

test('array input', () => {
// The existing value is an Immutable List
const currentDataList = fromJS({ arrInput: { value: List([1, 2, 3]) } });
expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: [1, 2, 3] }).arrInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: [1, 2, 4] }).arrInput).toEqual([1, 2, 4]);
expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: [1, 2] }).arrInput).toEqual([1, 2]);

// Existing value is a raw JavaScript array
const currentDataRaw = fromJS({ arrInput: { value: [10, 20] } });
expect(extractChanges(QUERY_INFO, currentDataRaw, { arrInput: [10, 20] }).arrInput).toBeUndefined();
expect(extractChanges(QUERY_INFO, currentDataRaw, { arrInput: [10, 20, 30] }).arrInput).toEqual([10, 20, 30]);

// Nulls and Undefined
expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: null }).arrInput).toBeNull();
expect(extractChanges(QUERY_INFO, currentDataList, { arrInput: undefined }).arrInput).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { List, Map } from 'immutable';
import { Utils } from '@labkey/api';

import { QueryInfo } from '../../../../public/QueryInfo';
import { isSetEqual } from '../../../util/utils';

function arrayListIsEqual(valueArr: Array<string | number>, nestedModelList: List<Map<string, any>>): boolean {
let matched = 0;
Expand Down Expand Up @@ -92,6 +93,14 @@ export function extractChanges(
if (existingValue === newValue) {
return false;
}
} else if (column?.jsonType === 'array') {
if (Array.isArray(newValue)) {
const existingArray = List.isList(existingValue) ? existingValue.toJS() : existingValue;

if (Array.isArray(existingArray) && isSetEqual(newValue, existingArray)) {
return false;
}
}
}

changedValues[col.name] = newValue === undefined ? null : newValue;
Expand Down
37 changes: 29 additions & 8 deletions packages/components/src/internal/util/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,7 @@ describe('arrayEquals', () => {
test('ignore order, case sensitive', () => {
expect(arrayEquals(undefined, undefined)).toBeTruthy();
expect(arrayEquals(undefined, null)).toBeTruthy();
expect(arrayEquals(null, undefined)).toBeTruthy();
expect(arrayEquals([], [])).toBeTruthy();
expect(arrayEquals(null, [])).toBeFalsy();
expect(arrayEquals(['a'], null)).toBeFalsy();
Expand Down Expand Up @@ -1621,38 +1622,58 @@ describe('arrayEquals', () => {
expect(arrayEquals(['a', 'b'], ['A', 'b'], false)).toBeFalsy();
expect(arrayEquals(['a', 'b'], ['B', 'A'], false)).toBeFalsy();
});

test('does not mutate original arrays', () => {
const arrA = ['b', 'a'];
const arrB = ['a', 'b'];
arrayEquals(arrA, arrB, true);
expect(arrA[0]).toBe('b');
expect(arrB[0]).toBe('a');
});

test('handles delimiter collision', () => {
expect(arrayEquals(['a;b', 'c'], ['a', 'b;c'])).toBeFalsy();
});

test('handles numeric-string collisions', () => {
expect(arrayEquals(['1', '23'], ['12', '3'])).toBeFalsy();
});

test('handles duplicate elements correctly with ignoreOrder', () => {
expect(arrayEquals(['a', 'a', 'b'], ['a', 'b', 'b'], true)).toBeFalsy();
});
});

describe('getValueFromRow', () => {
test('no row', () => {
expect(getValueFromRow(undefined, 'Name')).toEqual(undefined);
expect(getValueFromRow({}, 'Name')).toEqual(undefined);
expect(getValueFromRow(undefined, 'Name')).toBeUndefined();
expect(getValueFromRow({}, 'Name')).toBeUndefined();
});

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);
expect(getValueFromRow(row, 'bogus')).toBeUndefined();
});

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);
expect(getValueFromRow(row, 'bogus')).toBeUndefined();
});

test('returns value from array', () => {
const flatRow = { Name: ['test1', 'test2'] };
expect(getValueFromRow(flatRow, 'Name')).toEqual(undefined);
expect(getValueFromRow(flatRow, 'name')).toEqual(undefined);
expect(getValueFromRow(flatRow, 'bogus')).toEqual(undefined);
expect(getValueFromRow(flatRow, 'Name')).toBeUndefined();
expect(getValueFromRow(flatRow, 'name')).toBeUndefined();
expect(getValueFromRow(flatRow, 'bogus')).toBeUndefined();

const nestedRow = { Name: [{ value: 'test1' }, { value: 'test2' }] };
expect(getValueFromRow(nestedRow, 'Name')).toEqual('test1');
expect(getValueFromRow(nestedRow, 'name')).toEqual('test1');
expect(getValueFromRow(nestedRow, 'bogus')).toEqual(undefined);
expect(getValueFromRow(nestedRow, 'bogus')).toBeUndefined();
});
});

Expand Down
19 changes: 15 additions & 4 deletions packages/components/src/internal/util/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -748,16 +748,27 @@ export function isQuotedWithDelimiters(value: any, delimiter: string): boolean {
return strVal.startsWith('"') && strVal.endsWith('"');
}

export function arrayEquals(a: string[], b: string[], ignoreOrder = true, caseInsensitive?: boolean): boolean {
export function arrayEquals(
a: null | string[] | undefined,
b: null | string[] | undefined,
ignoreOrder = true,
caseInsensitive = false
): boolean {
if (a === b) return true;
if (a == null && b == null) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;

const aStr = ignoreOrder ? a.sort().join(';') : a.join(';');
const bStr = ignoreOrder ? b.sort().join(';') : b.join(';');
const normalize = (s: string) => (caseInsensitive ? s.toLowerCase() : s);

return caseInsensitive ? aStr.toLowerCase() === bStr.toLowerCase() : aStr === bStr;
if (ignoreOrder) {
// Use a copy to avoid mutating the original arrays
const aSorted = [...a].map(normalize).sort();
const bSorted = [...b].map(normalize).sort();
return aSorted.every((val, index) => val === bSorted[index]);
}

return a.every((val, index) => normalize(val) === normalize(b[index]));
}

export function getValueFromRow(row: Record<string, any>, col: string): number | string {
Expand Down
Loading
Loading