Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
df184bd
WIP add series entry button
alesan99 Jun 18, 2025
8e66d3e
Lint code with ESLint and Prettier
alesan99 Jun 18, 2025
db6f146
Increment catalog numbers on backend
alesan99 Jun 24, 2025
3265dce
Merge branch 'main' into issue-6276
alesan99 Jun 24, 2025
415f07f
Check if catalog numbers are formatted correctly
alesan99 Jun 24, 2025
e32d506
Merge branch 'issue-6276' of https://github.com/specify/specify7 into…
alesan99 Jun 24, 2025
93ddeb7
Add Carry Range Preference
alesan99 Jun 25, 2025
99bc6c9
Add existing values response placeholder
alesan99 Jun 25, 2025
9604a16
Validate range on backend
alesan99 Jun 25, 2025
0b61ce9
Show dialog if error occured when carrying forward
alesan99 Jun 25, 2025
bed8594
Show dialog for cat nums already in use
alesan99 Jun 25, 2025
d3cd67e
Make bulk range pref separate
alesan99 Jun 30, 2025
0342508
Merge branch 'main' into issue-6276
alesan99 Jun 30, 2025
2f592f8
Validate range start<end on frontend
alesan99 Jun 30, 2025
69894c3
Lint code with ESLint and Prettier
alesan99 Jun 30, 2025
e681fa5
Add "createRecordSetOnCarry" preference
alesan99 Jun 30, 2025
5dccd60
Lint code with ESLint and Prettier
alesan99 Jun 30, 2025
30e066a
Show Bulk Carry results when recordset is disabled
alesan99 Jun 30, 2025
5e89f54
Merge branch 'issue-6276' of https://github.com/specify/specify7 into…
alesan99 Jun 30, 2025
77b5ae5
Lint code with ESLint and Prettier
alesan99 Jun 30, 2025
0825d72
Add tests
alesan99 Jun 30, 2025
2cb69e7
Merge branch 'issue-6276' of https://github.com/specify/specify7 into…
alesan99 Jun 30, 2025
3848d8a
Fix not using COT's cn formatter
alesan99 Jul 1, 2025
b933011
Fix carry without record set
alesan99 Jul 1, 2025
4c2de15
Add series input to form
alesan99 Jul 1, 2025
8c697ad
Use field formatter name instead of title
alesan99 Jul 2, 2025
228cf97
Update tests
alesan99 Jul 2, 2025
4887bb8
Update tests
alesan99 Jul 2, 2025
191cff5
Fix type
alesan99 Jul 2, 2025
f80219d
Merge branch 'issue-6276' into issue-6276-form
alesan99 Jul 2, 2025
b9ee47d
Update tests
alesan99 Jul 2, 2025
c329f49
Update tests
alesan99 Jul 2, 2025
23a2463
Undo commit
alesan99 Jul 2, 2025
cc45456
Merge branch 'main' into issue-6276-form
alesan99 May 21, 2026
9a9b092
Fix merge errors
alesan99 May 21, 2026
1e40149
Fix merge conflict
alesan99 May 21, 2026
ff7910c
Potential fix for pull request finding 'CodeQL / Unused variable, imp…
alesan99 May 21, 2026
0db052f
Fix function signature
alesan99 May 21, 2026
1014063
Change handleBulkCarryForward default behavior
alesan99 May 21, 2026
7b1a565
Refactor seriesInput in field.tsx
alesan99 May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 120 additions & 28 deletions specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { resourceOn } from '../DataModel/resource';
import type { LiteralField, Relationship } from '../DataModel/specifyField';
import { raise } from '../Errors/Crash';
import { fetchPathAsString } from '../Formatters/formatters';
import { SeriesFormContext } from '../Forms/BulkCarryForward';
import { collectionPreferences } from '../Preferences/collectionPreferences';
import { userPreferences } from '../Preferences/userPreferences';

Expand All @@ -24,10 +25,13 @@ export function UiField({
readonly name: string | undefined;
readonly resource: SpecifyResource<AnySchema> | undefined;
readonly field: LiteralField | Relationship | undefined;
readonly isSeries: boolean | undefined;
readonly parser?: Parser;
}): JSX.Element {
return field?.isRelationship === true ? (
<RelationshipField field={field} {...props} />
) : props.isSeries === true ? (
<SeriesField field={field} {...props} />
) : (
<Field field={field} {...props} />
);
Expand Down Expand Up @@ -90,18 +94,26 @@ function RelationshipField({
);
}

type FieldSeriesInput = (props: {
readonly name: string | undefined;
readonly placeholder: string | undefined;
readonly validationAttributes: ReturnType<typeof getValidationAttributes>;
}) => React.ReactNode;

function Field({
resource,
id,
name,
field,
parser: defaultParser,
seriesInput,
}: {
readonly resource: SpecifyResource<AnySchema> | undefined;
readonly id: string | undefined;
readonly name: string | undefined;
readonly field: LiteralField | undefined;
readonly parser?: Parser;
readonly seriesInput?: FieldSeriesInput;
}): JSX.Element {
const { value, updateValue, validationRef, parser } = useResourceValue(
resource,
Expand Down Expand Up @@ -217,36 +229,116 @@ function Field({
validationAttributes;

return (
<Input.Generic
forwardRef={validationRef}
key={parser.title}
max={Number.MAX_SAFE_INTEGER}
name={name}
placeholder={customPlaceholder ?? parserPlaceholder}
{...restValidationAttributes}
className={rightAlignClassName}
id={id}
isReadOnly={isReadOnly}
required={'required' in validationAttributes && !isInSearchDialog}
tabIndex={isReadOnly ? -1 : undefined}
value={value?.toString() ?? ''}
onBlur={
isReadOnly ? undefined : ({ target }): void => updateValue(target.value)
}
onValueChange={(value): void => updateValue(value, false)}
/*
* Update data model value before onBlur, as onBlur fires after onSubmit
* if form is submitted using the ENTER key
*/
onChange={(event): void => {
const input = event.target as HTMLInputElement;
<>
<Input.Generic
forwardRef={validationRef}
key={parser.title}
max={Number.MAX_SAFE_INTEGER}
name={name}
placeholder={customPlaceholder ?? parserPlaceholder}
{...restValidationAttributes}
className={rightAlignClassName}
id={id}
isReadOnly={isReadOnly}
required={'required' in validationAttributes && !isInSearchDialog}
tabIndex={isReadOnly ? -1 : undefined}
value={value?.toString() ?? ''}
onBlur={
isReadOnly
? undefined
: ({ target }): void => updateValue(target.value)
}
onValueChange={(value): void => updateValue(value, false)}
/*
* Don't show validation errors on value change for input fields until
* field is blurred, unless user tried to paste a date (see definition
* of Input.Generic)
* Update data model value before onBlur, as onBlur fires after onSubmit
* if form is submitted using the ENTER key
*/
updateValue(input.value, event.type === 'paste');
}}
onChange={(event): void => {
const input = event.target as HTMLInputElement;
/*
* Don't show validation errors on value change for input fields until
* field is blurred, unless user tried to paste a date (see definition
* of Input.Generic)
*/
updateValue(input.value, event.type === 'paste');
}}
/>
{seriesInput?.({
name,
placeholder: customPlaceholder,
validationAttributes,
})}
</>
);
}

function SeriesField({
resource,
field,
id,
name,
isSeries,
parser,
}: {
readonly id: string | undefined;
readonly name: string | undefined;
readonly resource: SpecifyResource<AnySchema> | undefined;
readonly field: LiteralField | undefined;
readonly isSeries: boolean | undefined;
readonly parser?: Parser;
}): JSX.Element {
const {
seriesEnd: seriesRangeEnd,
setSeriesEnd: setSeriesRangeEnd,
setUsingSeries,
} = React.useContext(SeriesFormContext);
const tableName = resource?.specifyTable.name;
const [enableCarryForward] = userPreferences.use(
'form',
'preferences',
'enableCarryForward'
);
const [enableBulkCarryForwardRange] = userPreferences.use(
'form',
'preferences',
'enableBulkCarryForwardRange'
);
const showSeriesInput =
isSeries === true &&
tableName !== undefined &&
enableCarryForward.includes(tableName) &&
enableBulkCarryForwardRange.includes(tableName) &&
resource?.isNew() === true;
const handleSeriesRangeEndChange = (
event: React.ChangeEvent<HTMLInputElement>
): void => {
const input = event.target as HTMLInputElement;
setSeriesRangeEnd(input.value);
setUsingSeries(true);
};

return (
<Field
field={field}
id={id}
name={name}
parser={parser}
resource={resource}
seriesInput={({ name, placeholder, validationAttributes }) =>
showSeriesInput ? (
<Input.Generic
id="formSeriesRangeEnd"
key="formSeriesRangeEnd"
max={Number.MAX_SAFE_INTEGER}
name={name}
placeholder={placeholder}
value={seriesRangeEnd}
onChange={handleSeriesRangeEndChange}
{...validationAttributes}
required={false}
/>
) : null
}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ const fieldRenderers: {
maxLength,
minLength,
whiteSpaceSensitive,
isSeries,
},
}) {
const parser = React.useMemo<Parser>(
Expand Down Expand Up @@ -218,6 +219,7 @@ const fieldRenderers: {
name={name}
parser={parser}
resource={resource}
isSeries={isSeries}
/>
);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ describe('parseFormCell', () => {
fieldDefinition: {
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
max: undefined,
min: undefined,
step: undefined,
Expand Down Expand Up @@ -145,6 +146,7 @@ describe('parseFormCell', () => {
fieldDefinition: {
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
max: undefined,
min: undefined,
step: undefined,
Expand All @@ -170,6 +172,7 @@ describe('parseFormCell', () => {
fieldDefinition: {
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
max: undefined,
maxLength: undefined,
min: undefined,
Expand Down Expand Up @@ -203,6 +206,7 @@ describe('parseFormCell', () => {
fieldDefinition: {
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
max: undefined,
maxLength: undefined,
min: undefined,
Expand Down Expand Up @@ -253,6 +257,7 @@ describe('parseFormCell', () => {
fieldDefinition: {
defaultValue: 'A',
isReadOnly: false,
isSeries: false,
max: undefined,
min: undefined,
step: undefined,
Expand Down Expand Up @@ -281,6 +286,7 @@ describe('parseFormCell', () => {
fieldDefinition: {
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
max: undefined,
min: undefined,
step: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ describe('parseFormField', () => {
expect(parse('<cell uiType="text" />', {})).toEqual({
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
max: undefined,
min: undefined,
step: undefined,
Expand All @@ -46,6 +47,7 @@ describe('parseFormField', () => {
).toEqual({
defaultValue: 'a',
isReadOnly: true,
isSeries: false,
max: -10,
min: 4,
step: 3.2,
Expand All @@ -61,6 +63,7 @@ describe('parseFormField', () => {
).toEqual({
defaultValue: 'abc',
isReadOnly: true,
isSeries: false,
max: -10,
min: 4,
step: 3.2,
Expand All @@ -72,6 +75,7 @@ describe('parseFormField', () => {
expect(parse('<cell uiType="formattedtext" default="abc" />', {})).toEqual({
defaultValue: 'abc',
isReadOnly: false,
isSeries: false,
max: undefined,
min: undefined,
step: undefined,
Expand All @@ -83,6 +87,7 @@ describe('parseFormField', () => {
expect(parse('<cell uiType="label" default="abc" />', {})).toEqual({
defaultValue: 'abc',
isReadOnly: false,
isSeries: false,
max: undefined,
min: undefined,
step: undefined,
Expand Down Expand Up @@ -197,6 +202,7 @@ describe('parseFormField', () => {
expect(parse('<cell uiType="combobox"/>', {})).toEqual({
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
type: 'Text',
max: undefined,
min: undefined,
Expand Down Expand Up @@ -299,6 +305,7 @@ describe('parseFormField', () => {
expect(parse('<cell uiType="browse" initialize="min=3" />', {})).toEqual({
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
type: 'Text',
max: undefined,
maxLength: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ const parsedFormView = {
minLength: undefined,
step: undefined,
isReadOnly: false,
isSeries: false,
defaultValue: undefined,
whiteSpaceSensitive: false,
},
Expand Down Expand Up @@ -561,6 +562,7 @@ test('parseRows', async () => {
fieldDefinition: {
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
max: undefined,
maxLength: undefined,
min: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ const missingLabelTextField = ensure<FormCellDefinition>()({
fieldDefinition: {
defaultValue: undefined,
isReadOnly: false,
isSeries: false,
min: undefined,
max: undefined,
step: undefined,
Expand Down
2 changes: 2 additions & 0 deletions specifyweb/frontend/js_src/lib/components/FormParse/fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export type FieldTypes = {
readonly minLength: number | undefined;
readonly maxLength: number | undefined;
readonly whiteSpaceSensitive: boolean | undefined;
readonly isSeries: boolean | undefined;
}
>;
readonly Plugin: State<
Expand Down Expand Up @@ -218,6 +219,7 @@ const processFieldType: {
minLength: f.parseInt(getProperty('minLength')),
maxLength: f.parseInt(getProperty('maxLength')),
whiteSpaceSensitive,
isSeries: getProperty('series')?.toLowerCase() === 'true',
};
},
QueryComboBox({ getProperty, fields }) {
Expand Down
Loading
Loading