diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 2fd26626f7..e87d0622bd 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,12 +1,12 @@ { "name": "@labkey/components", - "version": "7.54.3", + "version": "7.55.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "7.54.3", + "version": "7.55.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", @@ -2563,9 +2563,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2587,9 +2584,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2611,9 +2605,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2635,9 +2626,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2659,9 +2647,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2683,9 +2668,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2838,9 +2820,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2855,9 +2834,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2872,9 +2848,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2889,9 +2862,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2906,9 +2876,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2923,9 +2890,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3861,9 +3825,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3878,9 +3839,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3895,9 +3853,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3912,9 +3867,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3929,9 +3881,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3946,9 +3895,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3963,9 +3909,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -3980,9 +3923,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -3997,9 +3937,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -4014,9 +3951,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ diff --git a/packages/components/package.json b/packages/components/package.json index b5a677f8aa..de4ea125ca 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "7.54.3", + "version": "7.55.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index ea1d53eba8..c0000e2339 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -80,6 +80,7 @@ import { } from './internal/util/messaging'; import { WHERE_FILTER_TYPE } from './internal/url/WhereFilterType'; import { InternalSpacesWarning } from './internal/components/forms/InternalSpacesWarning'; +import { RadioGroupInput } from './internal/components/forms/input/RadioGroupInput'; import { AddEntityButton, AddEntityElement } from './internal/components/buttons/AddEntityButton'; import { RemoveEntityButton } from './internal/components/buttons/RemoveEntityButton'; import { Alert } from './internal/components/base/Alert'; @@ -1634,6 +1635,7 @@ export { QuerySelect, QuerySort, quoteValueWithDelimiters, + RadioGroupInput, RANGE_URIS, redirect, registerDefaultURLMappers, diff --git a/packages/components/src/internal/components/base/models/User.test.ts b/packages/components/src/internal/components/base/models/User.test.ts index 557f889071..ccb6fa9d22 100644 --- a/packages/components/src/internal/components/base/models/User.test.ts +++ b/packages/components/src/internal/components/base/models/User.test.ts @@ -182,16 +182,16 @@ describe('User permissions', () => { expect(TEST_USER_APP_ADMIN.hasAddUsersPermission()).toBeTruthy(); }); - test('hasSampleWorkflowDeletePermission', () => { - expect(TEST_USER_GUEST.hasSampleWorkflowDeletePermission()).toBeFalsy(); - expect(TEST_USER_READER.hasSampleWorkflowDeletePermission()).toBeFalsy(); - expect(TEST_USER_AUTHOR.hasSampleWorkflowDeletePermission()).toBeFalsy(); - expect(TEST_USER_EDITOR.hasSampleWorkflowDeletePermission()).toBeTruthy(); - expect(TEST_USER_EDITOR_WITHOUT_DELETE.hasSampleWorkflowDeletePermission()).toBeFalsy(); - expect(TEST_USER_WORKFLOW_EDITOR.hasSampleWorkflowDeletePermission()).toBeTruthy(); - expect(TEST_USER_ASSAY_DESIGNER.hasSampleWorkflowDeletePermission()).toBeFalsy(); - expect(TEST_USER_FOLDER_ADMIN.hasSampleWorkflowDeletePermission()).toBeTruthy(); - expect(TEST_USER_PROJECT_ADMIN.hasSampleWorkflowDeletePermission()).toBeTruthy(); - expect(TEST_USER_APP_ADMIN.hasSampleWorkflowDeletePermission()).toBeTruthy(); + test('hasWorkflowDeletePermission', () => { + expect(TEST_USER_GUEST.hasWorkflowDeletePermission()).toBeFalsy(); + expect(TEST_USER_READER.hasWorkflowDeletePermission()).toBeFalsy(); + expect(TEST_USER_AUTHOR.hasWorkflowDeletePermission()).toBeFalsy(); + expect(TEST_USER_EDITOR.hasWorkflowDeletePermission()).toBeTruthy(); + expect(TEST_USER_EDITOR_WITHOUT_DELETE.hasWorkflowDeletePermission()).toBeFalsy(); + expect(TEST_USER_WORKFLOW_EDITOR.hasWorkflowDeletePermission()).toBeTruthy(); + expect(TEST_USER_ASSAY_DESIGNER.hasWorkflowDeletePermission()).toBeFalsy(); + expect(TEST_USER_FOLDER_ADMIN.hasWorkflowDeletePermission()).toBeTruthy(); + expect(TEST_USER_PROJECT_ADMIN.hasWorkflowDeletePermission()).toBeTruthy(); + expect(TEST_USER_APP_ADMIN.hasWorkflowDeletePermission()).toBeTruthy(); }); }); diff --git a/packages/components/src/internal/components/base/models/User.ts b/packages/components/src/internal/components/base/models/User.ts index 74bdd45e1c..b896103fa7 100644 --- a/packages/components/src/internal/components/base/models/User.ts +++ b/packages/components/src/internal/components/base/models/User.ts @@ -113,7 +113,7 @@ export class User implements IUserProps { return hasAllPermissions(this, [PermissionTypes.AddUser], false); } - hasSampleWorkflowDeletePermission(): boolean { + hasWorkflowDeletePermission(): boolean { return hasAllPermissions(this, [PermissionTypes.SampleWorkflowDelete]); } } diff --git a/packages/components/src/internal/components/entities/actions.ts b/packages/components/src/internal/components/entities/actions.ts index 9c79186983..b0cd493d51 100644 --- a/packages/components/src/internal/components/entities/actions.ts +++ b/packages/components/src/internal/components/entities/actions.ts @@ -24,7 +24,12 @@ import { import { SampleOperation } from '../samples/constants'; import { SchemaQuery } from '../../../public/SchemaQuery'; -import { getFilterForSampleOperation, isSamplesSchema, isWorkflowInputSamplesSchema } from '../samples/utils'; +import { + getFilterForSampleOperation, + isSamplesSchema, + isWorkflowInputSamplesSchema, + isWorkflowInputSourcesSchema +} from '../samples/utils'; import { getQueryDetails, getRequestAuditDetail, importData, InsertOptions, selectDistinctRows } from '../../query/api'; import { caseInsensitive, generateId } from '../../util/utils'; import { request } from '../../request'; @@ -274,17 +279,19 @@ export async function getSelectedParents( // GitHub Issue 1357: Resolve selected parents from details view to avoid filters applied to default view const response = await selectRows({ columns, filterArray, schemaQuery: schemaQuery.detailView }); - if (isSampleParent) { - return resolveSampleParentTypes(response, isAliquotParent, orderedRowIds); - } - - return resolveEntityParentTypeFromIds(schemaQuery, response, isAliquotParent, orderedRowIds); + return resolveEntityParentTypes( + response, + isAliquotParent, + orderedRowIds, + isSampleParent ? SCHEMAS.SAMPLE_SETS.SCHEMA : SCHEMAS.DATA_CLASSES.SCHEMA + ); } -function resolveSampleParentTypes( +function resolveEntityParentTypes( response: SelectRowsResponse, isAliquotParent?: boolean, - orderedRowIds?: string[] + orderedRowIds?: string[], + entitySchema = SCHEMAS.SAMPLE_SETS.SCHEMA ): List { const groups: Record = {}; const results = []; @@ -292,24 +299,27 @@ function resolveSampleParentTypes( // The transformation done here makes the entities compatible with the editable grid response.rows.forEach(row => { const displayValue = caseInsensitive(row, 'Name')?.value; - const sampleType = caseInsensitive(row, 'SampleSet')?.displayValue; + const entityType = + entitySchema === SCHEMAS.SAMPLE_SETS.SCHEMA + ? caseInsensitive(row, 'SampleSet')?.displayValue + : caseInsensitive(row, 'DataClass')?.displayValue; const value = caseInsensitive(row, 'RowId')?.value; - if (!groups.hasOwnProperty(sampleType)) { - groups[sampleType] = []; + if (!groups.hasOwnProperty(entityType)) { + groups[entityType] = []; } - groups[sampleType].push({ displayValue, value }); + groups[entityType].push({ displayValue, value }); }); let index = 1; - for (const [sampleType, data] of Object.entries(groups)) { + for (const [entityType, data] of Object.entries(groups)) { results.push( EntityParentType.create({ index, - schema: 'samples', - query: sampleType, - label: sampleType, + schema: entitySchema, + query: entityType, + label: entityType, value: orderedRowIds ? List(data.sort(_getEntitySort(orderedRowIds))) : List(data), @@ -364,12 +374,14 @@ async function initParents( Filter.create('RowId', selectionResponse.selected, Filter.Types.IN), Filter.create('Container', insertPermissionContainers, Filter.Types.IN), ]; - if (isWorkflowInputSamplesSchema(schemaQuery) && jobId) { + if (jobId && (isWorkflowInputSamplesSchema(schemaQuery) || isWorkflowInputSourcesSchema(schemaQuery))) { filterArray.push(Filter.create('JobId', jobId, Filter.Types.EQUAL)); } - const opFilter = getFilterForSampleOperation(SampleOperation.EditLineage); - if (opFilter) { - filterArray.push(opFilter); + if (isSamplesSchema(schemaQuery)) { + const opFilter = getFilterForSampleOperation(SampleOperation.EditLineage); + if (opFilter) { + filterArray.push(opFilter); + } } // Issue 48751 -- Always pass the selectionResponse.selected as the orderedRowIds because we use @@ -424,35 +436,6 @@ function _getEntitySort(orderedIds: string[]) { }; } -function resolveEntityParentTypeFromIds( - schemaQuery: SchemaQuery, - response: SelectRowsResponse, - isAliquotParent?: boolean, - orderedRowIds?: string[] -): List { - // The transformation done here makes the entities compatible with the editable grid - let data: DisplayObject[] = response.rows - .map(row => extractEntityTypeOptionFromRow(row)) - .map(({ label, rowId }) => ({ displayValue: label, value: rowId })); - if (orderedRowIds?.length > 1) data = data.sort(_getEntitySort(orderedRowIds)); - - // Issue 50389: use the data class display name if available - const dataClass = - response.rows.length > 0 ? caseInsensitive(response.rows[0], 'DataClass')?.displayValue : undefined; - - return List([ - EntityParentType.create({ - index: 1, - schema: schemaQuery.schemaName, - query: dataClass ?? schemaQuery.queryName, - label: dataClass, - value: List(data), - isAliquotParent, - required: isAliquotParent, - }), - ]); -} - // export for jest export function extractEntityTypeOptionFromRow( row: Row, diff --git a/packages/components/src/internal/components/entities/constants.ts b/packages/components/src/internal/components/entities/constants.ts index 0633bf4522..594f115acf 100644 --- a/packages/components/src/internal/components/entities/constants.ts +++ b/packages/components/src/internal/components/entities/constants.ts @@ -89,7 +89,7 @@ export const SamplePropertyDataType: EntityDataType = { instanceSchemaName: undefined, getInstanceSchemaQuery: (queryName: string) => { if (queryName === SAMPLE_PROPERTY_ALL_SAMPLE_TYPE.query) return SCHEMAS.EXP_TABLES.MATERIALS; - return new SchemaQuery('samples', queryName); + return new SchemaQuery(SCHEMAS.SAMPLE_SETS.SCHEMA, queryName); }, getInstanceDataType: (schemaQuery: SchemaQuery, altQueryName?: string) => { return altQueryName ?? schemaQuery.queryName; diff --git a/packages/components/src/internal/components/menus/SelectionMenuItem.tsx b/packages/components/src/internal/components/menus/SelectionMenuItem.tsx index ea5187574b..3863050250 100644 --- a/packages/components/src/internal/components/menus/SelectionMenuItem.tsx +++ b/packages/components/src/internal/components/menus/SelectionMenuItem.tsx @@ -11,6 +11,7 @@ import { MenuItem } from '../../dropdowns'; import { useOverlayTriggerState } from '../../OverlayTrigger'; import { Popover } from '../../Popover'; import { AppURL } from '../../url/AppURL'; +import { Placement } from '../../useOverlayPositioning'; interface Props { href?: string | AppURL; @@ -18,16 +19,22 @@ interface Props { maxSelectionDisabledMsg?: string; nounPlural: string; // always used, doesn't need default value onClick?: () => void; + popoverPlacement?: Placement; queryModel: QueryModel; text: React.ReactNode; } interface DisabledSelectionMenuItemProps { message: string; + popoverPlacement?: Placement; text: React.ReactNode; } -export const DisabledSelectionMenuItem: FC = ({ message, text }) => { +export const DisabledSelectionMenuItem: FC = ({ + message, + text, + popoverPlacement = 'right', +}) => { const { onMouseEnter, onMouseLeave, portalEl, show, targetRef } = useOverlayTriggerState( 'disabled-selection-menu-item', true, @@ -35,11 +42,11 @@ export const DisabledSelectionMenuItem: FC = ({ ); const overlay = useMemo( () => ( - + {message} ), - [message, targetRef] + [message, popoverPlacement, targetRef] ); return ( @@ -51,7 +58,7 @@ export const DisabledSelectionMenuItem: FC = ({ DisabledSelectionMenuItem.displayName = 'DisabledSelectionMenuItem'; export const SelectionMenuItem: FC = props => { - const { href, maxSelection, maxSelectionDisabledMsg, nounPlural, onClick, queryModel, text } = props; + const { href, maxSelection, maxSelectionDisabledMsg, nounPlural, onClick, popoverPlacement, queryModel, text } = props; const selectionSize = queryModel?.selections?.size; const { tooFewSelected, tooManySelected } = useMemo( () => ({ @@ -66,7 +73,7 @@ export const SelectionMenuItem: FC = props => { const message = tooFewSelected ? `Select one or more ${nounPlural}.` : maxSelectionDisabledMsg || `At most ${maxSelection?.toLocaleString()} ${nounPlural} can be selected.`; - return ; + return ; } return ( diff --git a/packages/components/src/internal/components/samples/utils.tsx b/packages/components/src/internal/components/samples/utils.tsx index 504468f28d..cc22a015bd 100644 --- a/packages/components/src/internal/components/samples/utils.tsx +++ b/packages/components/src/internal/components/samples/utils.tsx @@ -266,14 +266,22 @@ export function isFindBySampleSchema(schemaQuery: SchemaQuery): boolean { return schemaQuery?.hasSchema(SCHEMAS.EXP_TABLES.SCHEMA) && schemaQuery.queryStartsWith('exp_temp_'); } -export function isSamplesSchema(schemaQuery: SchemaQuery): boolean { - return schemaQuery?.hasSchema(SCHEMAS.SAMPLE_SETS.SCHEMA) || isAllSamplesSchema(schemaQuery); -} - export function isWorkflowInputSamplesSchema(schemaQuery: SchemaQuery): boolean { return SCHEMAS.WORKFLOW.JOB_INPUT_SAMPLES.isEqual(schemaQuery, false); } +export function isWorkflowInputSourcesSchema(schemaQuery: SchemaQuery): boolean { + return SCHEMAS.WORKFLOW.JOB_INPUT_SOURCES.isEqual(schemaQuery, false); +} + +export function isSamplesSchema(schemaQuery: SchemaQuery): boolean { + return ( + schemaQuery?.hasSchema(SCHEMAS.SAMPLE_SETS.SCHEMA) || + isAllSamplesSchema(schemaQuery) || + isWorkflowInputSamplesSchema(schemaQuery) + ); +} + export function isAllSamplesSchema(schemaQuery: SchemaQuery): boolean { if (!schemaQuery) return false; if (SCHEMAS.EXP_TABLES.MATERIALS.isEqual(schemaQuery, false)) return true;