diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index 7e5dc816a2..186355f862 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,16 +1,16 @@ { "name": "@labkey/components", - "version": "6.67.3", + "version": "6.68.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.67.3", + "version": "6.68.0", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.43.1", + "@labkey/api": "1.43.2", "@testing-library/dom": "~10.4.0", "@testing-library/jest-dom": "~6.6.3", "@testing-library/react": "~16.3.0", @@ -3492,10 +3492,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.43.1", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.43.1.tgz", - "integrity": "sha512-PkbI+OlnljpSLO7PkwfRRw/AH76pRRZbF8929ygxVZsrBXVFDSR+lSsSbJREiuMWkBTVcAOycwftwZB/O4QGHw==", - "license": "Apache-2.0" + "version": "1.43.2", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.43.2.tgz", + "integrity": "sha512-GYMwf/n6pycbjwTVshmVLUSiJ8ld/fTXlcVPCXsAANvRG0UYIE+LohNmnDOQQN0KVlAFyjSSWSV17wqvPgsVHA==" }, "node_modules/@labkey/build": { "version": "8.6.0", diff --git a/packages/components/package.json b/packages/components/package.json index 5f1ebe9e1b..7869bedd9a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.67.3", + "version": "6.68.0", "description": "Components, models, actions, and utility functions for LabKey applications and pages", "sideEffects": false, "files": [ @@ -50,7 +50,7 @@ "homepage": "https://github.com/LabKey/labkey-ui-components#readme", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.43.1", + "@labkey/api": "1.43.2", "@testing-library/dom": "~10.4.0", "@testing-library/jest-dom": "~6.6.3", "@testing-library/react": "~16.3.0", diff --git a/packages/components/releaseNotes/components.md b/packages/components/releaseNotes/components.md index e50f83f44e..34b4cf0e96 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,12 +1,17 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 6.68.0 +*Released* 2 November 2025 +- Add auditing of what method was used for CRUD + - Modified api.query.insertRows/updateRows/deleteRows/saveRows and moveEntities to accept and process editMethod parameter and record request hash in 'requestSource` + ### version 6.67.3 *Released*: 1 November 2025 - Issue 54160: Non US date parsing in the app - - Added `getAltNonUSParseFormats` date utility function to provide alternative parse formats for common non-US date/datetime formats - - Update DatePickerInput to use alternative parse formats when server date format is non-US - - Update parseDate utility function to use alternative non-US parse formats + - Added `getAltNonUSParseFormats` date utility function to provide alternative parse formats for common non-US date/datetime formats + - Update DatePickerInput to use alternative parse formats when server date format is non-US + - Update parseDate utility function to use alternative non-US parse formats ### version 6.67.2 *Released* 31 October 2025 diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index d011265baf..f56ddd2092 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -51,6 +51,7 @@ import { applyDevTools, arrayEquals, blurActiveElement, + camelCaseToTitleCase, capitalizeFirstChar, caseInsensitive, debounce, @@ -218,6 +219,7 @@ import { getContainerFilterForFolder, getContainerFilterForLookups, getQueryDetails, + getRequestAuditDetail, getVerbForInsertOption, importData, InsertFormats, @@ -228,6 +230,7 @@ import { loadQueries, loadQueriesFromTable, QueryCommandResponse, + saveRows, selectDistinctRows, selectRowsDeprecated, updateRows, @@ -252,6 +255,7 @@ import { flattenBrowseDataTreeResponse, loadReports } from './internal/query/rep import { AssayUploadTabs, DataViewInfoTypes, + EDIT_METHOD, EXPORT_TYPES, GRID_CHECKBOX_OPTIONS, IMPORT_DATA_FORM_TYPES, @@ -1189,6 +1193,7 @@ export { BreadcrumbCreate, buildURL, BulkUpdateForm, + camelCaseToTitleCase, cancelEvent, capitalizeFirstChar, Cards, @@ -1282,6 +1287,7 @@ export { DropdownButton, DropdownMenu, DropdownSection, + EDIT_METHOD, EditableDetailPanel, EditableGrid, EditableGridEvent, @@ -1420,6 +1426,7 @@ export { getQueryFormLabelFieldName, getQueryModelExportParams, getQueryParams, + getRequestAuditDetail, getRolesByUniqueName, getSampleDomainDefaultSystemFields, getSampleFinderLocalStorageKey, @@ -1701,6 +1708,7 @@ export { SampleTypeModel, saveDomain, SavedSettings, + saveRows, SchemaDetails, SchemaQuery, SCHEMAS, diff --git a/packages/components/src/internal/components/assay/actions.ts b/packages/components/src/internal/components/assay/actions.ts index d5fe2ef35f..3d18ad21e7 100644 --- a/packages/components/src/internal/components/assay/actions.ts +++ b/packages/components/src/internal/components/assay/actions.ts @@ -22,6 +22,8 @@ import { handleRequestFailure } from '../../request'; import { AssayProtocolModel } from '../domainproperties/assay/models'; import { AssayUploadResultModel } from './models'; +import { getRequestAuditDetail } from '../../query/api'; +import { EDIT_METHOD } from '../../constants'; let assayDefinitionCache: { [key: string]: Promise } = {}; let protocolCache: Record> = {}; @@ -94,12 +96,16 @@ export function getProtocol(options: GetProtocolOptions): Promise; +export interface ImportAssayRunOptions extends Omit { + editMethod?: EDIT_METHOD; +} export function importAssayRun(config: ImportAssayRunOptions): Promise { return new Promise((resolve, reject) => { + const { editMethod, ...importConfig } = config; + importConfig['auditDetails'] = getRequestAuditDetail(editMethod); AssayDOM.importRun({ - ...config, + ...importConfig, success: rawModel => { resolve(new AssayUploadResultModel(rawModel)); }, diff --git a/packages/components/src/internal/components/entities/actions.ts b/packages/components/src/internal/components/entities/actions.ts index 17a59a5d0f..8fca72d705 100644 --- a/packages/components/src/internal/components/entities/actions.ts +++ b/packages/components/src/internal/components/entities/actions.ts @@ -6,7 +6,7 @@ import { getSelected, getSelectedDataDeprecated } from '../../actions'; import { SampleOperation } from '../samples/constants'; import { SchemaQuery } from '../../../public/SchemaQuery'; import { getFilterForSampleOperation, isSamplesSchema } from '../samples/utils'; -import { importData, InsertOptions } from '../../query/api'; +import { getRequestAuditDetail, importData, InsertOptions } from '../../query/api'; import { caseInsensitive, generateId } from '../../util/utils'; import { request } from '../../request'; import { EntityCreationType } from '../samples/models'; @@ -982,6 +982,7 @@ export function moveEntities(options: MoveEntitiesOptions): Promise { if (response.success) { resolve(response); diff --git a/packages/components/src/internal/components/samples/actions.ts b/packages/components/src/internal/components/samples/actions.ts index e802a77613..3b657e1f19 100644 --- a/packages/components/src/internal/components/samples/actions.ts +++ b/packages/components/src/internal/components/samples/actions.ts @@ -35,6 +35,7 @@ import { SCHEMAS } from '../../schemas'; import { getQueryDetails, + getRequestAuditDetail, invalidateFullQueryDetailsCache, ISelectRowsResult, selectDistinctRows, @@ -53,6 +54,7 @@ import { QueryInfo } from '../../../public/QueryInfo'; import { ALL_AMOUNT_AND_UNITS_COLUMNS_LC, SAMPLE_STORAGE_COLUMNS_LC, STORED_AMOUNT_FIELDS } from './constants'; import { FindField, GroupedSampleFields, SampleState, SampleStateType } from './models'; import { executeSql, ExecuteSqlResponseWithSession } from '../../query/executeSql'; +import { EDIT_METHOD } from '../../constants'; export async function getSampleSet(config: IEntityTypeDetails): Promise { const response = await request({ @@ -556,7 +558,8 @@ export function updateSampleStorageData( sampleStorageData: SampleStorageData[], containerPath?: string, userComment?: string, - isDiscard = false + isDiscard = false, + editMethod?: EDIT_METHOD ): Promise { if (sampleStorageData.length === 0) { return Promise.resolve(); @@ -568,6 +571,7 @@ export function updateSampleStorageData( jsonData: { sampleRows: sampleStorageData, [STORED_AMOUNT_FIELDS.AUDIT_COMMENT]: userComment, + ...getRequestAuditDetail(editMethod), isDiscard, }, success: Utils.getCallbackWrapper(response => { diff --git a/packages/components/src/internal/constants.ts b/packages/components/src/internal/constants.ts index 6602cdbe7f..7a0cbf2acc 100644 --- a/packages/components/src/internal/constants.ts +++ b/packages/components/src/internal/constants.ts @@ -34,8 +34,8 @@ export const MAX_EDITABLE_GRID_ROWS = MAX_SELECTION_ACTION_ROWS; export const LOOKUP_DEFAULT_SIZE = 25; export enum AssayUploadTabs { - Grid = 1, Files = 2, + Grid = 1, } export enum EXPORT_TYPES { @@ -49,6 +49,17 @@ export enum EXPORT_TYPES { LABEL_TEMPLATE, } +export enum EDIT_METHOD { + BULK_EDIT = 'BulkEdit', + BULK_EDIT_LINEAGE = 'BulkEditLineage', + DETAIL_EDIT = 'DetailEdit', + DETAIL_EDIT_LINEAGE = 'DetailEditLineage', + FORM_INSERT = 'FormInsert', + GRID_EDIT = 'GridEdit', + GRID_INSERT = 'GridInsert', + STORAGE_VIEW_ACTION = 'StorageViewAction', +} + export enum KEYS { Backspace = 8, Tab = 9, diff --git a/packages/components/src/internal/query/APIWrapper.ts b/packages/components/src/internal/query/APIWrapper.ts index ccc54f785e..9738da1d70 100644 --- a/packages/components/src/internal/query/APIWrapper.ts +++ b/packages/components/src/internal/query/APIWrapper.ts @@ -55,6 +55,7 @@ import { insertRows, InsertRowsOptions, QueryCommandResponse, + saveRows, saveRowsByContainer, SaveRowsOptions, SelectDistinctOptions, @@ -65,6 +66,7 @@ import { } from './api'; import { executeSql, ExecuteSqlOptions, ExecuteSqlResponse } from './executeSql'; import { selectRows, SelectRowsOptions, SelectRowsResponse } from './selectRows'; +import { EDIT_METHOD } from '../constants'; export interface QueryAPIWrapper { clearSelected: (options: ClearSelectedOptions) => Promise; @@ -125,6 +127,7 @@ export interface QueryAPIWrapper { inherit: boolean, shared: boolean ) => Promise; + saveRows: (options: SaveRowsOptions) => Promise; saveRowsByContainer: (options: SaveRowsOptions, containerField?: string) => Promise; saveSessionView: ( schemaQuery: SchemaQuery, @@ -159,6 +162,7 @@ export interface QueryAPIWrapper { rows: any[], containerPaths: string[], auditUserComment: string, + editMethod?: EDIT_METHOD, containerField?: string ) => Promise; } @@ -182,6 +186,7 @@ export class QueryServerAPIWrapper implements QueryAPIWrapper { incrementClientSideMetricCount = incrementClientSideMetricCount; incrementRowCountMetric = incrementRowCountMetric; insertRows = insertRows; + saveRows = saveRows; renameGridView = renameGridView; replaceSelected = replaceSelected; saveGridView = saveGridView; @@ -225,6 +230,7 @@ export function getQueryTestAPIWrapper( renameGridView: mockFn(), replaceSelected: mockFn(), saveGridView: mockFn(), + saveRows: mockFn(), saveRowsByContainer: mockFn(), saveSessionView: mockFn(), selectRows: mockFn(), diff --git a/packages/components/src/internal/query/api.ts b/packages/components/src/internal/query/api.ts index ee4606c7ef..47a8768d0a 100644 --- a/packages/components/src/internal/query/api.ts +++ b/packages/components/src/internal/query/api.ts @@ -36,6 +36,7 @@ import { ViewInfo, ViewInfoJson } from '../ViewInfo'; import { URLResolver } from '../url/URLResolver'; import { ModuleContext } from '../components/base/ServerContext'; import { handleRequestFailure, RequestHandler } from '../request'; +import { EDIT_METHOD } from '../constants'; let queryDetailsCache: Record> = {}; @@ -741,8 +742,12 @@ export class InsertRowsErrorResponse extends ImmutableRecord({ } } +export interface QueryRequestOptionsBase { + editMethod?: EDIT_METHOD; +} export interface InsertRowsOptions - extends Omit { + extends Omit, + QueryRequestOptionsBase { fillEmptyFields?: boolean; rows: List; // TODO: convert to Array> schemaQuery: SchemaQuery; @@ -779,11 +784,24 @@ export class QueryCommandResponse { } } +export function getRequestAuditDetail(editMethod?: EDIT_METHOD): Record { + const auditDetails = {}; + if (editMethod) { + auditDetails['editMethod'] = editMethod; + } + const requestLocation = window.location.hash; + if (requestLocation) auditDetails['requestSource'] = requestLocation; + + return auditDetails; +} + export function insertRows(options: InsertRowsOptions): Promise { return new Promise((resolve, reject) => { - const { fillEmptyFields, rows, schemaQuery, ...insertRowsOptions } = options; + const { fillEmptyFields, editMethod, rows, schemaQuery, ...insertRowsOptions } = options; const _rows = fillEmptyFields === true ? ensureAllFieldsInAllRows(rows) : rows; + insertRowsOptions['auditDetails'] = getRequestAuditDetail(editMethod); + Query.insertRows({ autoFormFileData: true, ...insertRowsOptions, @@ -860,13 +878,16 @@ function ensureNullForUndefined(row: Map): Map { return row.reduce((map, v, k) => map.set(k, v === undefined ? null : v), Map()); } -export interface UpdateRowsOptions extends Omit { +export interface UpdateRowsOptions + extends Omit, + QueryRequestOptionsBase { schemaQuery: SchemaQuery; } export function updateRows(options: UpdateRowsOptions): Promise { return new Promise((resolve, reject) => { - const { schemaQuery, ...updateRowOptions } = options; + const { schemaQuery, editMethod, ...updateRowOptions } = options; + updateRowOptions['auditDetails'] = getRequestAuditDetail(editMethod); Query.updateRows({ autoFormFileData: true, ...updateRowOptions, @@ -907,6 +928,7 @@ export function updateRowsByContainer( rows: any[], containerPaths: string[], auditUserComment: string, + editMethod?: EDIT_METHOD, containerField = 'Folder' ): Promise { // if all rows are in the same container, we can use updateRows (which supports file/attachments) @@ -917,6 +939,7 @@ export function updateRowsByContainer( auditUserComment, rows, schemaQuery, + editMethod, }); } else { const commands: Query.Command[] = []; @@ -929,17 +952,26 @@ export function updateRowsByContainer( auditUserComment, skipReselectRows: true, }); - return saveRowsByContainer({ commands }, containerField); + return saveRowsByContainer( + { + commands, + editMethod, + }, + containerField + ); } } -export interface DeleteRowsOptions extends Omit { +export interface DeleteRowsOptions + extends Omit, + QueryRequestOptionsBase { schemaQuery: SchemaQuery; } export function deleteRows(options: DeleteRowsOptions): Promise { return new Promise((resolve, reject) => { - const { schemaQuery, ...deleteRowsOptions } = options; + const { schemaQuery, editMethod, ...deleteRowsOptions } = options; + deleteRowsOptions['auditDetails'] = getRequestAuditDetail(editMethod); Query.deleteRows({ apiVersion: 13.2, ...deleteRowsOptions, @@ -964,13 +996,15 @@ export function deleteRows(options: DeleteRowsOptions): Promise; +export interface SaveRowsOptions extends Omit, QueryRequestOptionsBase {} export function saveRows(options: SaveRowsOptions): Promise { return new Promise((resolve, reject) => { + const { editMethod, ...saveOptions } = options; + saveOptions['auditDetails'] = getRequestAuditDetail(editMethod); Query.saveRows({ apiVersion: 13.2, - ...options, + ...saveOptions, success: response => { resolve(response); }, @@ -1044,10 +1078,11 @@ export function deleteRowsByContainer( }); } + const { editMethod, ...deleteOptions } = options; Object.keys(containerRows).forEach(containerPath => { const rows = containerRows[containerPath]; commands.push({ - ...options, + ...deleteOptions, command: 'delete', schemaName: options.schemaQuery.schemaName, queryName: options.schemaQuery.queryName, @@ -1058,6 +1093,8 @@ export function deleteRowsByContainer( return new Promise((resolve, reject) => { saveRows({ + ...deleteOptions, + editMethod, commands, }) .then(response => { @@ -1123,8 +1160,9 @@ export interface IImportData { export function importData(config: IImportData): Promise { return new Promise((resolve, reject) => { + const auditDetails = getRequestAuditDetail(); QueryDOM.importData( - Object.assign({}, config, { + Object.assign({auditDetails}, config, { success: response => { if (response && response.exception) { reject(response); diff --git a/packages/components/src/public/QueryModel/EditableDetailPanel.tsx b/packages/components/src/public/QueryModel/EditableDetailPanel.tsx index 2fa3ca6875..205e48c126 100644 --- a/packages/components/src/public/QueryModel/EditableDetailPanel.tsx +++ b/packages/components/src/public/QueryModel/EditableDetailPanel.tsx @@ -21,6 +21,7 @@ import { useAppContext } from '../../internal/AppContext'; import { QueryModel } from './QueryModel'; import { DetailPanel, DetailPanelWithModel } from './DetailPanel'; +import { EDIT_METHOD } from '../../internal/constants'; export interface EditableDetailPanelProps { appEditable?: boolean; @@ -110,7 +111,6 @@ export const EditableDetailPanel: FC = props => { return ; }, []); - // eslint-disable-next-line @typescript-eslint/no-explicit-any const handleSubmit = useCallback( async (values: Record): Promise => { const { queryInfo } = model; @@ -139,6 +139,7 @@ export const EditableDetailPanel: FC = props => { onBeforeUpdate?.(updatedValues); await api.query.updateRows({ + editMethod: EDIT_METHOD.DETAIL_EDIT, auditBehavior: AuditBehaviorTypes.DETAILED, containerPath, rows: [updatedValues], @@ -147,8 +148,8 @@ export const EditableDetailPanel: FC = props => { }); setEditing(false); - onUpdate?.(); // eslint-disable-line no-unused-expressions - onEditToggle?.(false); // eslint-disable-line no-unused-expressions + onUpdate?.(); + onEditToggle?.(false); } catch (e) { setError(resolveErrorMessage(e, 'data', undefined, 'update')); setWarning(undefined); @@ -162,10 +163,10 @@ export const EditableDetailPanel: FC = props => { const panel = (
@@ -216,27 +217,27 @@ export const EditableDetailPanel: FC = props => { return ( {panel} -