Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5681790
Add a cot formatter filter
sharadsw Feb 18, 2025
6eff6ca
Remove unnecessary code
sharadsw Feb 24, 2025
689fa21
Merge branch 'issue-6239' into issue-6253
melton-jason Feb 26, 2025
5780ec4
Merge remote-tracking branch 'origin/production' into issue-6253
melton-jason Mar 4, 2025
98b5464
Show catalogNumber format selection when CO mapped by to-many
melton-jason Mar 4, 2025
ce52d5a
Refactor QueryLineTools
melton-jason Mar 4, 2025
0fa08aa
Extract QueryFieldFilter to its own component
melton-jason Mar 4, 2025
3e34a77
Merge branch 'issue-6239' into issue-6253
melton-jason Mar 4, 2025
aba0971
Merge remote-tracking branch 'origin/production' into issue-6253
melton-jason Mar 26, 2025
17954f5
Lint code with ESLint and Prettier
melton-jason Mar 26, 2025
986230d
Add ring to Add Uniqueness Rule
melton-jason Mar 28, 2025
03569e3
Fix bug where filter could not be added to field
melton-jason Mar 31, 2025
e707a79
Add space before (Default) in Formatter selector
melton-jason Mar 31, 2025
c51a166
Add cot filter to query when formatting catalogNumber
melton-jason Apr 1, 2025
4ea39cd
Merge remote-tracking branch 'origin/production' into issue-6253
melton-jason Apr 1, 2025
b34b627
Merge remote-tracking branch 'origin/production' into issue-6253
melton-jason Apr 2, 2025
22cec75
Lint code with ESLint and Prettier
melton-jason Apr 2, 2025
a9e56e1
Scroll to added QueryField regardless of where it's added
melton-jason Apr 2, 2025
c3cd064
Remove QueryFieldsContext and use props instead
melton-jason Apr 2, 2025
1b820b6
Merge remote-tracking branch 'origin/production' into issue-6253
melton-jason Apr 2, 2025
b0baa93
Merge remote-tracking branch 'origin/production' into issue-6253
melton-jason Apr 3, 2025
a43e6b8
Merge branch 'production' into issue-6253
melton-jason Apr 3, 2025
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const PREPARATION_GIFTED_KEY = 'preparation-isGifted';
export const PREPARATION_DISPOSED_KEY = 'preparation-isDisposed';
export const PREPARATION_EXCHANGED_OUT_KEY = 'preparation-isExchangedOut';
export const PREPARATION_EXCHANGED_IN_KEY = 'preparation-isExchangedIn';
export const COJO_PRIMARY_DELETE_KEY = 'primary-cojo-delete'
export const COJO_PRIMARY_DELETE_KEY = 'primary-cojo-delete';

/**
*
Expand Down
51 changes: 32 additions & 19 deletions specifyweb/frontend/js_src/lib/components/FormFields/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,33 +126,42 @@ function Field({
'rightAlignNumberFields'
);

const isNew = resource?.isNew()
const isCO = resource?.specifyTable.name === "CollectionObject"
const isPartOfCOG = isCO ? resource?.get('cojo') !== null && resource?.get('cojo') !== undefined : false;
const isCatNumberField = field?.name === "catalogNumber"
const isNew = resource?.isNew();
const isCO = resource?.specifyTable.name === 'CollectionObject';
const isPartOfCOG = isCO
? resource?.get('cojo') !== null && resource?.get('cojo') !== undefined
: false;
const isCatNumberField = field?.name === 'catalogNumber';
// Check if collection pref wants to inherit primary cat num for empty CO cat num sibilings inside of a COG
const [displayPrimaryCatNumberPref] = collectionPreferences.use(
'catalogNumberInheritance',
'behavior',
'inheritance'
);
const displayCatNumberPlaceHolder = isNew === false && isCO && isPartOfCOG && isCatNumberField && displayPrimaryCatNumberPref

const [primaryCatalogNumber, setPrimaryCatalogNumber] = React.useState<string | null>(null);
'catalogNumberInheritance',
'behavior',
'inheritance'
);
const displayCatNumberPlaceHolder =
isNew === false &&
isCO &&
isPartOfCOG &&
isCatNumberField &&
displayPrimaryCatNumberPref;

const [primaryCatalogNumber, setPrimaryCatalogNumber] = React.useState<
string | null
>(null);

React.useEffect(() => {
if (resource && displayCatNumberPlaceHolder) {
ajax<string | null>('/api/specify/catalog_number_for_sibling/', {
method: 'POST',
headers: { Accept: 'application/json' },
body: resource
body: resource,
})
.then((response) => {
setPrimaryCatalogNumber(response.data);
})
.catch((error) => {
console.error("Error fetching catalog number:", error);
});
.then((response) => {
setPrimaryCatalogNumber(response.data);
})
.catch((error) => {
console.error('Error fetching catalog number:', error);
});
}
}, [resource, displayCatNumberPlaceHolder]);

Expand All @@ -161,7 +170,11 @@ function Field({
forwardRef={validationRef}
key={parser.title}
name={name}
placeholder={displayCatNumberPlaceHolder && typeof primaryCatalogNumber === 'string' ? primaryCatalogNumber : undefined}
placeholder={
displayCatNumberPlaceHolder && typeof primaryCatalogNumber === 'string'
? primaryCatalogNumber
: undefined
}
{...validationAttributes}
className={
/*
Expand Down
5 changes: 3 additions & 2 deletions specifyweb/frontend/js_src/lib/components/FormMeta/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function MetaDialog({
readonly onClose: () => void;
}): JSX.Element {
const subView = React.useContext(SubViewContext);
const canMergeTable = canMerge(resource.specifyTable)
const canMergeTable = canMerge(resource.specifyTable);
return (
<Dialog
buttons={commonText.close()}
Expand Down Expand Up @@ -221,7 +221,8 @@ function MetaDialog({
{resource.specifyTable.name in recordMergingTableSpec &&
hasPermission('/record/merge', 'update') &&
hasPermission('/record/merge', 'delete') &&
hasTablePermission(resource.specifyTable.name, 'update') && canMergeTable ? (
hasTablePermission(resource.specifyTable.name, 'update') &&
canMergeTable ? (
<MergeRecord resource={resource} />
) : undefined}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ export function ResourceMapping({
showHiddenFields: true,
generateFieldData: 'all',
spec: navigatorSpecs.formatterEditor,
}).map(line => ({
}).map((line) => ({
...line,
fieldsData: Object.fromEntries(
Object.entries(line.fieldsData).filter(([key]) => key !== 'age')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type QueryFieldType =
| 'id'
| 'number'
| 'text';
export type QueryFieldFilter =
export type QueryFieldFilterType =
| 'ageName'
| 'ageRange'
| 'any'
Expand All @@ -65,7 +65,7 @@ export type QueryFieldFilter =
| 'startsWith'
| 'true'
| 'trueOrNull';
export const filtersWithDefaultValue = new Set<QueryFieldFilter>([
export const filtersWithDefaultValue = new Set<QueryFieldFilterType>([
'equal',
'in',
]);
Expand Down Expand Up @@ -401,7 +401,7 @@ function In({
}

export const queryFieldFilters: RR<
QueryFieldFilter,
QueryFieldFilterType,
{
readonly id: number;
readonly label: LocalizedString;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,82 +2,68 @@ import React from 'react';

import { commonText } from '../../localization/common';
import { queryText } from '../../localization/query';
import type { RA } from '../../utils/types';
import { RA } from '../../utils/types';
import { Button } from '../Atoms/Button';
import { className } from '../Atoms/className';
import { iconClassName, icons } from '../Atoms/Icons';
import {
mappingElementDivider,
mappingElementDividerClassName,
} from '../WbPlanView/LineComponents';
import type { QueryFieldFilter } from './FieldFilter';
import type { QueryField } from './helpers';

type FieldFilterToolProps = {
readonly fieldFilters: RA<{
readonly type: QueryFieldFilter;
readonly startValue: string;
readonly isNot: boolean;
/**
* When 'isStrict' is True
* each CO's age_range must be fully contained by
* the age range provided by start_time and end_time filter (complete overlap).
* When 'isStrict' is False, only a partial overlap
* between a CO's age range and the provided
* start_time and end_time filter are needed.
*/
readonly isStrict: boolean;
}>;
readonly index: number;
readonly isBasic: boolean;
readonly hasAny: boolean;
readonly isFieldComplete: boolean;
readonly fieldName: string;
readonly handleChange: ((newField: QueryField) => void) | undefined;
readonly handleFilterChange: (
index: number,
filter: QueryField['filters'][number] | undefined
) => void;
};
import { QueryFieldFilterType } from './FieldFilter';
import { QueryFieldFilterProps } from './QueryLineFieldFilter';

/**
* Buttons relating to the management of the filter or change the behavior
* of the filter
*
* Examples of these buttons are:
*
* Add Filter Button, Negate Filter Button, Remove Filter Button,
* Toggle IsStrict Button (for CollectionObject -> Age queries)
*/
export function FieldFilterTool({
fieldFilters,
index,
fieldFilter,
isFirst,
isBasic,
hasAny,
hasMultipleFilters,
isFieldComplete,
fieldName,
handleChange,
handleFilterChange,
}: FieldFilterToolProps): JSX.Element {
onChange: handleChange,
onAddFieldFilter: handleAddFieldFilter,
onRemoveFieldFilter: handleRemoveFieldFilter,
}: QueryFieldFilterProps): JSX.Element {
return (
<>
{index === 0 ? (
{/* REFACTOR: Extract this to separate component */}
{isFirst ? (
<>
{isBasic ? null : mappingElementDivider}
{!hasAny && (
<Button.Small
aria-label={queryText.or()}
aria-pressed={fieldFilters.length > 1}
aria-pressed={hasMultipleFilters}
className={`
print:hidden
${className.ariaHandled}
${isFieldComplete ? '' : 'invisible'}
`}
disabled={handleChange === undefined}
title={queryText.or()}
variant={
fieldFilters.length > 1
hasMultipleFilters
? className.infoButton
: className.secondaryLightButton
}
onClick={(): void =>
handleFilterChange(fieldFilters.length, {
type: 'any',
isNot: false,
startValue: '',
isStrict: false,
})
onClick={
handleAddFieldFilter === undefined
? undefined
: (): void =>
handleAddFieldFilter({
type: 'any',
isNot: false,
startValue: '',
isStrict: false,
})
}
>
{icons.plus}
Expand All @@ -99,63 +85,82 @@ export function FieldFilterTool({
<Button.Small
aria-label={commonText.remove()}
className="print:hidden"
disabled={handleChange === undefined}
title={commonText.remove()}
variant={className.dangerButton}
onClick={(): void => handleFilterChange(index, undefined)}
onClick={
handleRemoveFieldFilter === undefined
? undefined
: (): void => handleRemoveFieldFilter()
}
>
{icons.trash}
</Button.Small>
</>
)}
{fieldFilters[index].type !== 'any' && (
{fieldFilter.type !== 'any' && (
<Button.Small
aria-label={queryText.negate()}
aria-pressed={fieldFilters[index].isNot}
aria-pressed={fieldFilter.isNot}
className={className.ariaHandled}
disabled={handleChange === undefined}
title={queryText.negate()}
variant={
fieldFilters[index].isNot
fieldFilter.isNot
? className.dangerButton
: className.secondaryLightButton
}
onClick={(): void =>
handleFilterChange(index, {
...fieldFilters[index],
isNot: !fieldFilters[index].isNot,
})
onClick={
handleChange === undefined
? undefined
: (): void =>
handleChange({
...fieldFilter,
isNot: !fieldFilter.isNot,
})
}
>
{icons.ban}
</Button.Small>
)}
{fieldName === 'age' && index === 0 ? (
<Button.Small
aria-label={
fieldFilters[index].isStrict
? queryText.strict()
: queryText.nonStrict()
}
aria-pressed={fieldFilters[index].isStrict}
className={className.ariaHandled}
disabled={handleChange === undefined}
title={
fieldFilters[index].isStrict
? queryText.strict()
: queryText.nonStrict()
}
variant={className.secondaryLightButton}
onClick={(): void =>
handleFilterChange(index, {
...fieldFilters[index],
isStrict: !fieldFilters[index].isStrict,
})
}
>
{fieldFilters[index].isStrict ? icons.strict : icons.nonStrict}
</Button.Small>
{/**
* REFACTOR: Add a property to queryFieldFilters to determine
* FieldFilterTool component based on type
*/}
{(['ageRange', 'ageName'] as RA<QueryFieldFilterType>).includes(
fieldFilter.type
) ? (
<AgeQueryFieldFilter
fieldFilter={fieldFilter}
onChange={handleChange}
/>
) : undefined}
</>
);
}

function AgeQueryFieldFilter({
fieldFilter,
onChange: handleChange,
}: Pick<QueryFieldFilterProps, 'fieldFilter' | 'onChange'>): JSX.Element {
return (
<Button.Small
aria-label={
fieldFilter.isStrict ? queryText.strict() : queryText.nonStrict()
}
aria-pressed={fieldFilter.isStrict}
className={className.ariaHandled}
title={fieldFilter.isStrict ? queryText.strict() : queryText.nonStrict()}
variant={className.secondaryLightButton}
onClick={
handleChange === undefined
? undefined
: () =>
handleChange({
...fieldFilter,
isStrict: !fieldFilter.isStrict,
})
}
>
{fieldFilter.isStrict ? icons.strict : icons.nonStrict}
</Button.Small>
);
}
Loading