Skip to content

Commit 7bddb03

Browse files
authored
Workflow: Change Required Template Fields to be Required at the start of a Job (#1854)
1 parent 4f043f6 commit 7bddb03

13 files changed

Lines changed: 216 additions & 53 deletions

File tree

packages/components/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/components/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@labkey/components",
3-
"version": "6.62.2",
3+
"version": "6.62.3",
44
"description": "Components, models, actions, and utility functions for LabKey applications and pages",
55
"sideEffects": false,
66
"files": [

packages/components/releaseNotes/components.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 6.62.3
5+
*Released*: 18 September 2025
6+
- Workflow: Change Required Template Fields to be Required at the start of a Job
7+
- Update required field label display to show overlay before asterisk
8+
- Fix CheckBoxInput required field display on Formsy forms
9+
- Add optional `colFieldKeyMap` param to `flattenValuesFromRow` to convert field names to field keys
10+
411
### version 6.62.2
512
*Released*: 16 September 2025
613
- Issue 53926: Move custom assay queries to assay schema

packages/components/src/internal/components/EditInlineField.tsx

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import {
55
formatDate,
66
getColDateFormat,
77
getDateFNSDateFormat,
8-
getJsonDateTimeFormatString,
9-
getJsonDateFormatString,
108
getDateTimeDisplayValueFromStr,
9+
getJsonDateFormatString,
10+
getJsonDateTimeFormatString,
1111
} from '../util/Date';
1212
import { Key, useEnterEscape } from '../../public/useEnterEscape';
1313

@@ -20,6 +20,7 @@ import { useServerContext } from './base/ServerContext';
2020
import { resolveDetailEditRenderer } from './forms/detail/DetailDisplay';
2121
import { UserLink } from './user/UserLink';
2222
import { DatePickerInput } from './forms/input/DatePickerInput';
23+
import { isBlankValue } from '../util/utils';
2324

2425
interface Props {
2526
allowBlank?: boolean;
@@ -99,7 +100,7 @@ export const EditInlineField: FC<Props> = memo(props => {
99100
// Issue 48196: if the domain column has been setup with a "url" prop, use it in the EditInlineField value display
100101
if (value_ !== undefined && value?.url) {
101102
value_ = (
102-
<a href={value.url} target="_blank" rel="noopener noreferrer">
103+
<a href={value.url} rel="noopener noreferrer" target="_blank">
103104
{value_}
104105
</a>
105106
);
@@ -128,7 +129,7 @@ export const EditInlineField: FC<Props> = memo(props => {
128129
const saveEdit = useCallback(() => {
129130
const inputValue = getInputValue();
130131

131-
if (allowBlank === false && !isDate && inputValue.trim() === '') {
132+
if (allowBlank === false && !isDate && isBlankValue(inputValue)) {
132133
return;
133134
}
134135

@@ -140,7 +141,7 @@ export const EditInlineField: FC<Props> = memo(props => {
140141

141142
const onBlur = useCallback((): void => {
142143
if (!state.ignoreBlur) {
143-
if (allowBlank === false && !isDate && getInputValue().trim() === '') {
144+
if (allowBlank === false && !isDate && isBlankValue(getInputValue())) {
144145
onCancel();
145146
} else {
146147
saveEdit();
@@ -154,6 +155,11 @@ export const EditInlineField: FC<Props> = memo(props => {
154155
if (date instanceof Array) throw new Error('Unsupported date/time type');
155156

156157
if (!date) {
158+
if (allowBlank === false) {
159+
onCancel();
160+
return;
161+
}
162+
157163
if (isDate) setDateValue(undefined);
158164
else setTimeJsonValue(undefined);
159165
} else if (typeof date === 'string') {
@@ -162,7 +168,7 @@ export const EditInlineField: FC<Props> = memo(props => {
162168
if (isDate) setDateValue(date);
163169
}
164170
},
165-
[isDate]
171+
[isDate, allowBlank]
166172
);
167173

168174
const onFormsyColumnChange = useCallback(
@@ -214,38 +220,38 @@ export const EditInlineField: FC<Props> = memo(props => {
214220
{state.editing && isDateOrTime && !!column && (
215221
<DatePickerInput
216222
autoFocus
217-
name={name}
218223
formsy={false}
219-
queryColumn={column}
220-
showLabel={false}
221-
value={isDate ? dateValue : _value}
224+
inlineEdit
222225
inputWrapperClassName="form-control"
226+
name={name}
223227
onBlur={onBlur}
224-
onKeyDown={onKeyDown}
225228
onChange={onDateChange}
229+
onKeyDown={onKeyDown}
226230
placeholderText={placeholder}
227-
inlineEdit
231+
queryColumn={column}
232+
showLabel={false}
233+
value={isDate ? dateValue : _value}
228234
/>
229235
)}
230236
{state.editing && isDateOrTime && !column && (
231237
<DateInput
232238
autoFocus
233239
container={container}
240+
dateFormat={dateInputDateFormat}
234241
endDate={endDate}
235-
minDate={startDate}
236242
maxDate={endDate}
243+
minDate={startDate}
237244
name={name}
238245
onBlur={onBlur}
239-
onKeyDown={onKeyDown}
240246
onChange={onDateChange}
247+
onKeyDown={onKeyDown}
241248
onMonthChange={onDateChange}
242249
placeholderText={placeholder}
243250
selected={dateValue}
244251
selectsEnd={startDate !== undefined}
245252
selectsStart={endDate !== undefined}
246-
startDate={startDate}
247253
showTimeSelect={!!column}
248-
dateFormat={dateInputDateFormat}
254+
startDate={startDate}
249255
/>
250256
)}
251257
{state.editing && isTextArea && (
@@ -255,10 +261,10 @@ export const EditInlineField: FC<Props> = memo(props => {
255261
className="form-control"
256262
cols={100}
257263
defaultValue={_value}
264+
name={name}
258265
onBlur={onBlur}
259266
onFocus={onTextAreaFocus}
260267
onKeyDown={onKeyDown}
261-
name={name}
262268
placeholder={placeholder}
263269
ref={inputRef}
264270
rows={5}
@@ -278,14 +284,14 @@ export const EditInlineField: FC<Props> = memo(props => {
278284
autoFocus
279285
className="form-control"
280286
defaultValue={_value}
287+
name={name}
281288
onBlur={onBlur}
289+
onInput={onInputChange}
282290
onKeyDown={onKeyDown}
283-
name={name}
284291
placeholder={placeholder}
285292
ref={inputRef}
286-
type={inputType}
287293
size={Math.max(_value?.length ?? 0, 20)}
288-
onInput={onInputChange}
294+
type={inputType}
289295
/>
290296
</span>
291297
)}
@@ -294,13 +300,13 @@ export const EditInlineField: FC<Props> = memo(props => {
294300
{label && (
295301
<span
296302
className="edit-inline-field__label"
297-
unselectable="on"
298303
title={allowEdit ? tooltip : undefined}
304+
unselectable="on"
299305
>
300306
{label}
301307
</span>
302308
)}
303-
{isUser && <UserLink userId={value?.value} userDisplayValue={value?.displayValue} />}
309+
{isUser && <UserLink userDisplayValue={value?.displayValue} userId={value?.value} />}
304310
<span
305311
className={classNames({ 'edit-inline-field__toggle': allowEdit, 'ws-pre-wrap': isTextArea })}
306312
onClick={toggleEdit}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { render } from '@testing-library/react';
3+
import { LabelOverlay } from './LabelOverlay';
4+
5+
describe('LabelOverlay', () => {
6+
it('renders label with overlay, not formsy', () => {
7+
render(<LabelOverlay isFormsy={false} label="Test Label" />);
8+
expect(document.querySelector('label')?.textContent).toBe('Test Label ');
9+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
10+
});
11+
12+
it('renders label with overlay and required symbol when required, not formsy', () => {
13+
render(<LabelOverlay isFormsy={false} label="Test Label" required={true} />);
14+
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
15+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
16+
});
17+
18+
it('renders label with overlay and required, but addLabelAsterisk = false, not formsy', () => {
19+
render(<LabelOverlay addLabelAsterisk={false} isFormsy={false} label="Test Label" required={true} />);
20+
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
21+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
22+
});
23+
24+
it('renders label with overlay, required = false, and addLabelAsterisk = true, not formsy', () => {
25+
render(<LabelOverlay addLabelAsterisk={true} isFormsy={false} label="Test Label" required={false} />);
26+
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
27+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
28+
});
29+
30+
it('renders label with no overlay and required symbol when required, not formsy', () => {
31+
render(<LabelOverlay helpTipRenderer="NONE" isFormsy={false} label="Test Label" required={true} />);
32+
expect(document.querySelector('label')?.textContent).toBe('Test Label * ');
33+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(0);
34+
});
35+
36+
it('renders label with overlay, isFormsy = true (default)', () => {
37+
render(<LabelOverlay label="Test Label" />);
38+
expect(document.querySelector('label')).toBeNull();
39+
expect(document.querySelector('span')?.textContent).toBe('Test Label ');
40+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
41+
});
42+
43+
it('renders label with overlay and required', () => {
44+
render(<LabelOverlay isFormsy={true} label="Test Label" required={true} />);
45+
expect(document.querySelector('label')).toBeNull();
46+
expect(document.querySelector('span')?.textContent).toBe('Test Label * ');
47+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
48+
});
49+
50+
it('renders label with overlay and addLabelAsterisk', () => {
51+
render(<LabelOverlay addLabelAsterisk={true} isFormsy={true} label="Test Label" />);
52+
expect(document.querySelector('label')).toBeNull();
53+
expect(document.querySelector('span')?.textContent).toBe('Test Label * ');
54+
expect(document.querySelectorAll('.fa-question-circle')).toHaveLength(1);
55+
});
56+
});

packages/components/src/internal/components/forms/LabelOverlay.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,14 @@ export class LabelOverlay extends React.Component<LabelOverlayProps> {
6161

6262
if (column?.helpTipRenderer || helpTipRenderer) {
6363
return (
64-
<HelpTipRenderer type={column?.helpTipRenderer || helpTipRenderer} column={column}>
64+
<HelpTipRenderer column={column} type={column?.helpTipRenderer || helpTipRenderer}>
6565
{children}
6666
</HelpTipRenderer>
6767
);
6868
}
6969

7070
return (
71-
<DomainFieldHelpTipContents column={column} required={required} description={description} type={type}>
71+
<DomainFieldHelpTipContents column={column} description={description} required={required} type={type}>
7272
{children}
7373
</DomainFieldHelpTipContents>
7474
);
@@ -84,7 +84,7 @@ export class LabelOverlay extends React.Component<LabelOverlayProps> {
8484
: undefined;
8585
const body = this.overlayBody();
8686
return (
87-
<Popover id={this._popoverId} title={label} placement={placement} className={popoverClassName}>
87+
<Popover className={popoverClassName} id={this._popoverId} placement={placement} title={label}>
8888
{body}
8989
</Popover>
9090
);
@@ -111,20 +111,22 @@ export class LabelOverlay extends React.Component<LabelOverlayProps> {
111111
if (isFormsy) {
112112
// when being used as a label for a formsy component directly this will use just a span without the
113113
// classes applied as well as not needing to handle 'required' display
114+
// TODO: remove space for required-symbol after *
114115
return (
115116
<span>
116117
{label}&nbsp;
117-
{required || addLabelAsterisk ? <span className="required-symbol">* </span> : null}
118118
{overlay}
119+
{required || addLabelAsterisk ? <span className="required-symbol">* </span> : null}
119120
</span>
120121
);
121122
}
122123

124+
// TODO: remove space for required-symbol after *
123125
return (
124126
<label className={(labelClass ? labelClass + ' ' : '') + 'text__truncate-and-wrap'} htmlFor={inputId}>
125127
<span>{label}</span>&nbsp;
126-
{required || addLabelAsterisk ? <span className="required-symbol">* </span> : null}
127128
{overlay}
129+
{required || addLabelAsterisk ? <span className="required-symbol">* </span> : null}
128130
</label>
129131
);
130132
}

packages/components/src/internal/components/forms/detail/DetailDisplay.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { List, fromJS } from 'immutable';
1+
import { fromJS, List } from 'immutable';
22
import React from 'react';
33
import { render } from '@testing-library/react';
44

@@ -11,7 +11,7 @@ import { AssayRunReferenceRenderer } from '../../../renderers/AssayRunReferenceR
1111
import { LabelColorRenderer } from '../../../renderers/LabelColorRenderer';
1212
import { FileColumnRenderer } from '../../../renderers/FileColumnRenderer';
1313

14-
import { DetailDisplay, resolveDetailRenderer, defaultTitleRenderer, Renderer } from './DetailDisplay';
14+
import { defaultTitleRenderer, DetailDisplay, Renderer, resolveDetailRenderer } from './DetailDisplay';
1515

1616
describe('DetailDisplay', () => {
1717
const namePatternCol = new QueryColumn({
@@ -132,9 +132,9 @@ describe('DetailDisplay', () => {
132132
render(
133133
<DetailDisplay
134134
asPanel={true}
135-
editingMode={false}
136135
data={data}
137136
displayColumns={cols}
137+
editingMode={false}
138138
fieldHelpTexts={fieldHelpText}
139139
/>
140140
);
@@ -155,7 +155,7 @@ describe('defaultTitleRenderer', () => {
155155
});
156156
render(<div>{defaultTitleRenderer(col)}</div>);
157157
expect(document.querySelector('span').innerHTML).toEqual(
158-
'test&nbsp;<span class="required-symbol">* </span><div class="overlay-trigger"><i class="fa fa-question-circle"></i></div>'
158+
'test&nbsp;<div class="overlay-trigger"><i class="fa fa-question-circle"></i></div><span class="required-symbol">* </span>'
159159
);
160160
});
161161

packages/components/src/internal/components/forms/input/CheckboxInput.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,33 +115,34 @@ class CheckboxInputImpl extends DisableableInput<CheckboxInputImplProps, Checkbo
115115
</label>
116116
) : (
117117
<FieldLabel
118+
column={queryColumn}
119+
isDisabled={isDisabled}
118120
label={label}
119121
labelOverlayProps={{
120122
isFormsy: false,
121123
inputId: queryColumn.fieldKey,
124+
required: queryColumn?.required,
122125
addLabelAsterisk,
123126
}}
124127
showLabel={showLabel}
125128
showToggle={allowDisable}
126-
column={queryColumn}
127-
isDisabled={isDisabled}
128129
toggleProps={{
129130
onClick: this.toggleDisabled,
130131
}}
131132
/>
132133
)}
133134
<div className={wrapperClassName}>
134135
<input
136+
checked={checked}
135137
disabled={isDisabled}
136138
id={queryColumn.fieldKey}
137139
name={queryColumn.fieldKey}
140+
onChange={this.onChange}
138141
// Issue 43299: Ignore "required" property for boolean columns as this will
139142
// cause any false value (i.e. unchecked) to prevent submission.
140143
// required={queryColumn.required}
141144
type="checkbox"
142145
value={formsy ? value : checked}
143-
checked={checked}
144-
onChange={this.onChange}
145146
/>
146147
</div>
147148
</div>

packages/components/src/internal/util/messaging.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ export function resolveErrorMessage(
197197
} else if (errorMsg.indexOf('null value in column "name"') > -1) {
198198
const noun = errorMsg.indexOf('material') > -1 ? 'Sample ID' : 'ID';
199199
return noun + ' cannot be blank.';
200+
} else if (noun === 'job' && errorMsg.indexOf('when it contains rows with blank values') > -1) {
201+
return errorMsg.replace('it contains rows with blank values', 'there are already jobs using this template');
200202
}
201203
}
202204
return errorMsg;
@@ -234,7 +236,7 @@ export function getPermissionRestrictionMessage(
234236
}
235237

236238
export function lookupValidationErrorMessage(
237-
value: string | number | boolean,
239+
value: boolean | number | string,
238240
fromPaste?: boolean,
239241
displayValue?: any
240242
): string {

0 commit comments

Comments
 (0)