Skip to content

Commit e948859

Browse files
authored
GitHub Issue 779: Cannot Edit Relative Dates in Sample Finder (#1932)
1 parent 2933121 commit e948859

6 files changed

Lines changed: 105 additions & 11 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": "7.16.1",
3+
"version": "7.16.2",
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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# @labkey/components
22
Components, models, actions, and utility functions for LabKey applications and pages
33

4+
### version 7.16.2
5+
*Released*: 11 February 2026
6+
- GitHub Issue 779: Cannot Edit Relative Dates in Sample Finder
7+
- Allow intermediate editing state for relative date values in DatePickerInput
8+
49
### version 7.16.1
510
*Released*: 11 February 2026
611
- Query: sort columns where name starts with '+'

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

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -166,11 +166,18 @@ export class DatePickerInputImpl extends DisableableInput<DatePickerInputImplPro
166166

167167
onChange = (date: Date, event?: any): void => {
168168
const { onChange, formsy, inlineEdit, queryColumn } = this.props;
169+
const { relativeInputValue } = this.state;
169170

170-
this.setState({ selectedDate: date, invalid: false, invalidStart: false });
171+
let validSelect = date;
171172

172-
if (this.state.relativeInputValue) {
173-
onChange?.(this.state.relativeInputValue);
173+
if (relativeInputValue) {
174+
if (isRelativeDateFilterValue(relativeInputValue, false)) {
175+
onChange?.(relativeInputValue);
176+
}
177+
else {
178+
validSelect = undefined;
179+
onChange?.(undefined);
180+
}
174181
} else {
175182
const formatted = getDateTimeDisplayValue(date, queryColumn);
176183
onChange?.(queryColumn.isTimeColumn ? formatted : date, formatted);
@@ -180,6 +187,8 @@ export class DatePickerInputImpl extends DisableableInput<DatePickerInputImplPro
180187
}
181188
}
182189

190+
this.setState({ selectedDate: validSelect, invalid: false, invalidStart: false });
191+
183192
// event is null when selecting time picker
184193
if (!event && inlineEdit) this.input.current.setFocus();
185194
};
@@ -191,14 +200,27 @@ export class DatePickerInputImpl extends DisableableInput<DatePickerInputImplPro
191200
if (queryColumn.isTimeColumn) {
192201
// Issue 50010: Time picker enters the wrong time if a time field has a format set
193202
this.onChange(parseTime(value), undefined);
194-
} else if (isRelativeDateFilterValue(value)) {
203+
} else if (isRelativeDateFilterValue(value, true)) {
195204
this.setState({ relativeInputValue: value });
196205
this.props.onChange?.(value);
197206
} else {
198207
this.setState({ relativeInputValue: undefined });
199208
}
200209
};
201210

211+
handleCalendarClose = (): void => {
212+
const { onCalendarClose, onChange } = this.props;
213+
const { relativeInputValue } = this.state;
214+
onCalendarClose?.();
215+
216+
if (relativeInputValue) {
217+
if (isRelativeDateFilterValue(relativeInputValue, true) && !isRelativeDateFilterValue(relativeInputValue, false)) {
218+
// clear out invalid relative date input on calendar close
219+
onChange?.(undefined);
220+
}
221+
}
222+
}
223+
202224
onIconClick = (): void => {
203225
this.input.current?.setFocus();
204226
};
@@ -275,7 +297,7 @@ export class DatePickerInputImpl extends DisableableInput<DatePickerInputImplPro
275297
isClearable={isClearable}
276298
name={name ? name : queryColumn.fieldKey}
277299
onBlur={inlineEdit ? onBlur : undefined}
278-
onCalendarClose={onCalendarClose}
300+
onCalendarClose={this.handleCalendarClose}
279301
onChange={this.onChange}
280302
onChangeRaw={allowRelativeInput || isTimeOnly ? this.onChangeRaw : undefined}
281303
onKeyDown={onKeyDown}

packages/components/src/internal/util/Date.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,61 @@ describe('Date Utilities', () => {
10471047
expect(isRelativeDateFilterValue('-3d')).toBeTruthy();
10481048
expect(isRelativeDateFilterValue('-0d')).toBeTruthy();
10491049
});
1050+
1051+
test('relaxedMatch=true, bad input', () => {
1052+
expect(isRelativeDateFilterValue('', true)).toBeFalsy();
1053+
expect(isRelativeDateFilterValue(' ', true)).toBeFalsy();
1054+
expect(isRelativeDateFilterValue('-D', true)).toBeFalsy();
1055+
expect(isRelativeDateFilterValue('ad', true)).toBeFalsy();
1056+
expect(isRelativeDateFilterValue('a1', true)).toBeFalsy();
1057+
expect(isRelativeDateFilterValue('1dd', true)).toBeFalsy();
1058+
});
1059+
1060+
test('relaxedMatch=true allows incomplete values ending with d', () => {
1061+
expect(isRelativeDateFilterValue('3d', true)).toBeTruthy();
1062+
expect(isRelativeDateFilterValue('0d', true)).toBeTruthy();
1063+
expect(isRelativeDateFilterValue('300d', true)).toBeTruthy();
1064+
expect(isRelativeDateFilterValue('d', true)).toBeTruthy();
1065+
});
1066+
1067+
test('relaxedMatch=true allows values starting with sign', () => {
1068+
expect(isRelativeDateFilterValue('+3', true)).toBeTruthy();
1069+
expect(isRelativeDateFilterValue('-3', true)).toBeTruthy();
1070+
expect(isRelativeDateFilterValue('+0', true)).toBeTruthy();
1071+
expect(isRelativeDateFilterValue('-0', true)).toBeTruthy();
1072+
expect(isRelativeDateFilterValue('+300', true)).toBeTruthy();
1073+
expect(isRelativeDateFilterValue('-300', true)).toBeTruthy();
1074+
expect(isRelativeDateFilterValue('+d', true)).toBeTruthy();
1075+
expect(isRelativeDateFilterValue('-d', true)).toBeTruthy();
1076+
});
1077+
1078+
test('relaxedMatch=true allows numbers with optional d', () => {
1079+
expect(isRelativeDateFilterValue('3', true)).toBeTruthy();
1080+
expect(isRelativeDateFilterValue('0', true)).toBeTruthy();
1081+
expect(isRelativeDateFilterValue('300', true)).toBeTruthy();
1082+
});
1083+
1084+
test('relaxedMatch=true still accepts strict format', () => {
1085+
expect(isRelativeDateFilterValue('+3d', true)).toBeTruthy();
1086+
expect(isRelativeDateFilterValue('+300d', true)).toBeTruthy();
1087+
expect(isRelativeDateFilterValue('-3d', true)).toBeTruthy();
1088+
expect(isRelativeDateFilterValue('-0d', true)).toBeTruthy();
1089+
});
1090+
1091+
test('relaxedMatch=true still rejects invalid values', () => {
1092+
expect(isRelativeDateFilterValue('++3d', true)).toBeFalsy();
1093+
expect(isRelativeDateFilterValue('2022-04-19', true)).toBeFalsy();
1094+
expect(isRelativeDateFilterValue('abc', true)).toBeFalsy();
1095+
expect(isRelativeDateFilterValue('d3', true)).toBeFalsy();
1096+
});
1097+
1098+
test('relaxedMatch=false uses strict validation', () => {
1099+
expect(isRelativeDateFilterValue('3d', false)).toBeFalsy();
1100+
expect(isRelativeDateFilterValue('+3', false)).toBeFalsy();
1101+
expect(isRelativeDateFilterValue('3', false)).toBeFalsy();
1102+
expect(isRelativeDateFilterValue('+3d', false)).toBeTruthy();
1103+
expect(isRelativeDateFilterValue('-3d', false)).toBeTruthy();
1104+
});
10501105
});
10511106

10521107
describe('getParsedRelativeDateStr', () => {

packages/components/src/internal/util/Date.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -796,9 +796,21 @@ export function isDateBetween(date: Date, start: Date, end: Date, dateOnlyCompar
796796
return time >= start.getTime() && time <= end.getTime();
797797
}
798798

799-
const RELATIVE_DAYS_REGEX = /^[+-]\d+d$/;
800-
export function isRelativeDateFilterValue(val: string): boolean {
801-
return typeof val === 'string' && RELATIVE_DAYS_REGEX.test(val);
799+
// Matches strict format: +5d, -10d
800+
const STRICT_RELATIVE_DAYS = /^[+-]\d+d$/;
801+
802+
// Matches partials: "+", "5", "+5", "5d", "+d", etc.
803+
const RELAXED_RELATIVE_DAYS = /^[+-]?\d*d?$/;
804+
805+
/**
806+
* Returns true if the given string is a valid relative date filter value. Ex: +5d, -1d
807+
*/
808+
export function isRelativeDateFilterValue(val: string, relaxedMatch?: boolean): boolean {
809+
if (typeof val !== 'string' || val === '') return false;
810+
811+
if (relaxedMatch) return RELAXED_RELATIVE_DAYS.test(val);
812+
813+
return STRICT_RELATIVE_DAYS.test(val);
802814
}
803815

804816
export function getParsedRelativeDateStr(dateVal: string): { days: number; positive: boolean } {

0 commit comments

Comments
 (0)