diff --git a/packages/components/package-lock.json b/packages/components/package-lock.json index b0d87d0393..02704005b4 100644 --- a/packages/components/package-lock.json +++ b/packages/components/package-lock.json @@ -1,16 +1,16 @@ { "name": "@labkey/components", - "version": "6.43.1", + "version": "6.43.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@labkey/components", - "version": "6.43.1", + "version": "6.43.2", "license": "SEE LICENSE IN LICENSE.txt", "dependencies": { "@hello-pangea/dnd": "18.0.1", - "@labkey/api": "1.41.0", + "@labkey/api": "1.41.1", "@testing-library/dom": "~10.4.0", "@testing-library/jest-dom": "~6.6.3", "@testing-library/react": "~16.3.0", @@ -3018,9 +3018,9 @@ } }, "node_modules/@labkey/api": { - "version": "1.41.0", - "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.41.0.tgz", - "integrity": "sha512-FWhELnNLkTVNNGXOnPHFoLb/CvPKVnibvVnINGETh1rBUmp8UdGrSV2OuhhPEkuGoi/SGp2zI3dkkSYjRR20Eg==" + "version": "1.41.1", + "resolved": "https://labkey.jfrog.io/artifactory/api/npm/libs-client/@labkey/api/-/@labkey/api-1.41.1.tgz", + "integrity": "sha512-5CaGXA0Z2m/D7lDf8F8gZugQVxcWURpRsEdU+Xc/xn4Vz5ZdLjfgzyD87WsEtwNEi0Ed8Lw/BpsToFh80g+3IA==" }, "node_modules/@labkey/build": { "version": "8.5.0", diff --git a/packages/components/package.json b/packages/components/package.json index 0e81de5f49..60bce62482 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@labkey/components", - "version": "6.43.1", + "version": "6.43.2", "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.41.0", + "@labkey/api": "1.41.1", "@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 f8700aa615..4c2ae20355 100644 --- a/packages/components/releaseNotes/components.md +++ b/packages/components/releaseNotes/components.md @@ -1,8 +1,14 @@ # @labkey/components Components, models, actions, and utility functions for LabKey applications and pages +### version 6.43.2 +*Released*: 23 May 2025 +- Add detailed auditing of domain changes & comment ability + - include `CommentTextArea` in `BaseDomainDesigner` for supplying user provided comment for domain updates + - wire up `auditUserComment` for `SampleTypeDesigner`, `DataClassDesigner` and `AssayDesignerPanels` + ### version 6.43.1 -*Released*: 22 May 2025 +*Released*: 23 May 2025 - Issue 53055: Check for multiple values in single value column ### version 6.43.0 diff --git a/packages/components/src/internal/components/domainproperties/BaseDomainDesigner.tsx b/packages/components/src/internal/components/domainproperties/BaseDomainDesigner.tsx index 03c2ad713c..53b5c94f9d 100644 --- a/packages/components/src/internal/components/domainproperties/BaseDomainDesigner.tsx +++ b/packages/components/src/internal/components/domainproperties/BaseDomainDesigner.tsx @@ -1,4 +1,13 @@ -import React, { PureComponent, ComponentType, FC, memo, PropsWithChildren } from 'react'; +import React, { + PureComponent, + ComponentType, + FC, + memo, + PropsWithChildren, + useState, + useCallback, + useMemo, +} from 'react'; import { List } from 'immutable'; import { getSubmitButtonClass, isApp } from '../../app/utils'; @@ -6,6 +15,10 @@ import { FormButtons } from '../../FormButtons'; import { Alert } from '../base/Alert'; +import { CommentTextArea } from '../forms/input/CommentTextArea'; + +import { useDataChangeCommentsRequired } from '../forms/input/useDataChangeCommentsRequired'; + import { getDomainBottomErrorMessage, getDomainHeaderName, getUpdatedVisitedPanelsList } from './actions'; import { DOMAIN_ERROR_ID, SEVERITY_LEVEL_ERROR } from './constants'; @@ -119,8 +132,9 @@ interface BaseDomainDesignerProps extends PropsWithChildren { hasValidProperties: boolean; name: string; onCancel: () => void; - onFinish: () => void; + onFinish: (reason?: string) => void; saveBtnText?: string; + showUserComment?: boolean; submitting: boolean; visitedPanels: List; } @@ -137,7 +151,11 @@ export const BaseDomainDesigner: FC = memo(props => { onCancel, hasValidProperties, saveBtnText = 'Save', + showUserComment, } = props; + const [userComment, setUserComment] = useState(undefined); + // skip useDataChangeCommentsRequired hook for LKS pages with showUserComment=false + const requiresUserComment = showUserComment ? useDataChangeCommentsRequired().requiresUserComment : false; // get a list of the domain names that have errors const errorDomains = domains @@ -147,6 +165,15 @@ export const BaseDomainDesigner: FC = memo(props => { const bottomErrorMsg = getDomainBottomErrorMessage(exception, errorDomains, hasValidProperties, visitedPanels); const submitClassname = `save-button btn btn-${getSubmitButtonClass()}`; + const onSave = useCallback(() => { + onFinish(userComment); + }, [userComment, onFinish]); + + const canSubmit = useMemo(() => { + if (submitting) return false; + return !requiresUserComment || userComment?.trim()?.length > 0; + }, [requiresUserComment, userComment, submitting]); + return (
{children} @@ -159,7 +186,16 @@ export const BaseDomainDesigner: FC = memo(props => { - diff --git a/packages/components/src/internal/components/domainproperties/actions.ts b/packages/components/src/internal/components/domainproperties/actions.ts index aa328c8422..134ce52046 100644 --- a/packages/components/src/internal/components/domainproperties/actions.ts +++ b/packages/components/src/internal/components/domainproperties/actions.ts @@ -530,6 +530,7 @@ export function _parseCalculatedColumn( export interface SaveDomainOptions { /** Boolean indicating if rowIndices should be added to the error message objects */ addRowIndexes?: boolean; + auditUserComment?: string; /** Container path where requests are made. Defaults to domain.container for updates. */ containerPath?: string; /** DomainDesign to save */ @@ -576,6 +577,7 @@ export function saveDomain(options: SaveDomainOptions): Promise { domainDesign: DomainDesign.serialize(domain), includeWarnings, options: options.options, + auditUserComment: options.auditUserComment, success: successHandler, failure: failureHandler, }); @@ -902,7 +904,7 @@ export function updateDataType(field: DomainField, value: any): DomainField { }) as DomainField; } else { if (PropDescType.isUser(value)) { - field = field.merge({lookupValidator: LOOKUP_VALIDATOR}) as DomainField; + field = field.merge({ lookupValidator: LOOKUP_VALIDATOR }) as DomainField; } } } diff --git a/packages/components/src/internal/components/domainproperties/assay/AssayDesignerPanels.tsx b/packages/components/src/internal/components/domainproperties/assay/AssayDesignerPanels.tsx index 1882f1e192..81af223700 100644 --- a/packages/components/src/internal/components/domainproperties/assay/AssayDesignerPanels.tsx +++ b/packages/components/src/internal/components/domainproperties/assay/AssayDesignerPanels.tsx @@ -232,14 +232,14 @@ export class AssayDesignerPanelsImpl extends React.PureComponent { ); } - onFinish = (): void => { + onFinish = (reasonForUpdate?: string): void => { const { setSubmitting } = this.props; const { protocolModel } = this.state; const appIsValidMsg = this.getAppIsValidMsg(); const textChoiceValidMsg = this.getTextChoiceUpdatesValidMsg(); const isValid = protocolModel.isValid() && textChoiceValidMsg === undefined && appIsValidMsg === undefined; - this.props.onFinish(isValid, this.saveDomain); + this.props.onFinish(isValid, () => this.saveDomain(reasonForUpdate)); if (!isValid) { const exception = @@ -258,13 +258,13 @@ export class AssayDesignerPanelsImpl extends React.PureComponent { } }; - saveDomain = (): void => { + saveDomain = (auditUserComment?: string): void => { const { beforeFinish, setSubmitting } = this.props; const { protocolModel } = this.state; beforeFinish?.(protocolModel); - saveAssayDesign(protocolModel) + saveAssayDesign(protocolModel, auditUserComment) .then(response => { this.setState(() => ({ protocolModel })); setSubmitting(false, () => { @@ -418,6 +418,7 @@ export class AssayDesignerPanelsImpl extends React.PureComponent { onCancel={onCancel} onFinish={this.onFinish} saveBtnText={saveBtnText} + showUserComment={!initModel.isNew() && appPropertiesOnly} > { +export function saveAssayDesign(model: AssayProtocolModel, auditUserComment?: string): Promise { return new Promise((resolve, reject) => { Ajax.request({ url: ActionURL.buildURL('assay', 'saveProtocol.api', model.container), - jsonData: AssayProtocolModel.serialize(model), + jsonData: AssayProtocolModel.serialize(model, auditUserComment), success: Utils.getCallbackWrapper(response => { resolve(AssayProtocolModel.create(response.data)); }), diff --git a/packages/components/src/internal/components/domainproperties/assay/models.ts b/packages/components/src/internal/components/domainproperties/assay/models.ts index 29c81ecfcc..1016c89669 100644 --- a/packages/components/src/internal/components/domainproperties/assay/models.ts +++ b/packages/components/src/internal/components/domainproperties/assay/models.ts @@ -135,7 +135,7 @@ export class AssayProtocolModel extends ImmutableRecord({ return new AssayProtocolModel({ ...raw, name, domains }); } - static serialize(model: AssayProtocolModel): any { + static serialize(model: AssayProtocolModel, auditUserComment?: string): any { // need to serialize the DomainDesign objects to remove the unrecognized fields const domains = model.domains.map(domain => { return DomainDesign.serialize(domain); @@ -143,6 +143,8 @@ export class AssayProtocolModel extends ImmutableRecord({ const json = model.merge({ domains }).toJS(); + if (auditUserComment) json.auditUserComment = auditUserComment; + // only need to serialize the id and not the autoCopyTargetContainer object delete json.autoCopyTargetContainer; delete json.exception; diff --git a/packages/components/src/internal/components/domainproperties/dataclasses/DataClassDesigner.tsx b/packages/components/src/internal/components/domainproperties/dataclasses/DataClassDesigner.tsx index 149fc8df4f..0f1d911410 100644 --- a/packages/components/src/internal/components/domainproperties/dataclasses/DataClassDesigner.tsx +++ b/packages/components/src/internal/components/domainproperties/dataclasses/DataClassDesigner.tsx @@ -48,6 +48,7 @@ interface Props { headerText?: string; helpTopic?: string; initModel?: DataClassModel; + isUpdate?: boolean; isValidParentOptionsFn?: (row: any, isDataClass: boolean) => boolean; // loadNameExpressionOptions is a prop for testing purposes only, see default implementation below loadNameExpressionOptions?: ( @@ -61,11 +62,11 @@ interface Props { onChange?: (model: DataClassModel) => void; onComplete: (model: DataClassModel) => void; saveBtnText?: string; - showGenIdBanner?: boolean; validateNameExpressions?: boolean; } interface State { + auditUserComment?: string; model: DataClassModel; nameExpressionWarnings: string[]; namePreviews: string[]; @@ -156,12 +157,12 @@ export class DataClassDesignerImpl extends PureComponent { + onFinish = (auditUserComment?: string): void => { const { defaultNameFieldConfig, setSubmitting, nounSingular } = this.props; const { model } = this.state; const isValid = model.isValid(defaultNameFieldConfig); - this.props.onFinish(isValid, this.saveDomain); + this.props.onFinish(isValid, () => this.saveDomain(false, auditUserComment)); if (!isValid) { let exception: string; @@ -207,7 +208,7 @@ export class DataClassDesignerImpl extends PureComponent => { + saveDomain = async (hasConfirmedNameExpression?: boolean, auditUserComment?: string): Promise => { const { api, beforeFinish, onComplete, setSubmitting, validateNameExpressions } = this.props; const { model } = this.state; const { name, domain } = model; @@ -239,6 +240,7 @@ export class DataClassDesignerImpl extends PureComponent { this.setState({ nameExpressionWarnings: undefined, + auditUserComment: undefined, }); }); }; @@ -349,7 +353,7 @@ export class DataClassDesignerImpl extends PureComponent ({ nameExpressionWarnings: undefined, }), - () => this.saveDomain(true) + () => this.saveDomain(true, this.state.auditUserComment) ); }; @@ -461,7 +465,7 @@ export class DataClassDesignerImpl extends PureComponent boolean; metricUnitProps?: MetricUnitProps; nameExpressionInfoUrl?: string; @@ -113,11 +114,10 @@ interface Props { onCancel: () => void; onChange?: (model: SampleTypeModel) => void; onComplete: (response: DomainDesign) => void; - sampleAliasCaption?: string; sampleTypeCaption?: string; saveBtnText?: string; showAliquotOptions?: boolean; - showGenIdBanner?: boolean; + sampleAliasCaption?: string; showLinkToStudy?: boolean; showParentLabelPrefix?: boolean; useSeparateDataClassesAliasMenu?: boolean; @@ -126,6 +126,7 @@ interface Props { } interface State { + auditUserComment?: string; error: React.ReactNode; model: SampleTypeModel; nameExpressionWarnings: string[]; @@ -327,7 +328,11 @@ export class SampleTypeDesignerImpl extends React.PureComponent { - this.setState({ showUniqueIdConfirmation: false, uniqueIdsConfirmed: false }); + this.setState({ + showUniqueIdConfirmation: false, + uniqueIdsConfirmed: false, + auditUserComment: undefined, + }); }; onUniqueIdConfirm = (): void => { @@ -338,7 +343,10 @@ export class SampleTypeDesignerImpl extends React.PureComponent { - this.setState({ nameExpressionWarnings: undefined }); + this.setState({ + nameExpressionWarnings: undefined, + auditUserComment: undefined, + }); }); }; @@ -348,19 +356,22 @@ export class SampleTypeDesignerImpl extends React.PureComponent { + onFinish = (auditUserComment?: string): void => { const { defaultSampleFieldConfig, setSubmitting, metricUnitProps } = this.props; const { model, uniqueIdsConfirmed } = this.state; if (!model.isNew() && this.getNumNewUniqueIdFields() > 0 && !uniqueIdsConfirmed) { - this.setState({ showUniqueIdConfirmation: true }); + this.setState({ + showUniqueIdConfirmation: true, + auditUserComment + }); return; } const metricUnitRequired = metricUnitProps?.metricUnitRequired; const isValid = model.isValid(defaultSampleFieldConfig, metricUnitRequired); - this.props.onFinish(isValid, this.saveDomain); + this.props.onFinish(isValid, () => this.saveDomain(false, auditUserComment)); if (isValid) return; @@ -382,15 +393,21 @@ export class SampleTypeDesignerImpl extends React.PureComponent { - this.setState({ model: updatedModel }, () => { - scrollDomainErrorIntoView(); - }); + this.setState( + { + model: updatedModel, + auditUserComment, + }, + () => { + scrollDomainErrorIntoView(); + } + ); }); }; - saveDomain = async (hasConfirmedNameExpression?: boolean): Promise => { + saveDomain = async (hasConfirmedNameExpression?: boolean, comment?: string): Promise => { const { api, beforeFinish, setSubmitting } = this.props; - const { model } = this.state; + const { model, auditUserComment } = this.state; const { name, domain, description } = model; if (!hasConfirmedNameExpression) { beforeFinish?.(model); @@ -492,6 +509,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent { this.props.onComplete(response); @@ -621,7 +639,7 @@ export class SampleTypeDesignerImpl extends React.PureComponent