Skip to content

Commit 23c316b

Browse files
Merge pull request #4805 from specify/issue-922
Open interaction dialog when adding prep in ExchangeOut
2 parents 7873a51 + a062d6f commit 23c316b

File tree

6 files changed

+146
-100
lines changed

6 files changed

+146
-100
lines changed

specifyweb/frontend/js_src/lib/components/FormSliders/IntegratedRecordSelector.tsx

+11-12
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,20 @@ import { Button } from '../Atoms/Button';
99
import { DataEntry } from '../Atoms/DataEntry';
1010
import { ReadOnlyContext } from '../Core/Contexts';
1111
import { DependentCollection } from '../DataModel/collectionApi';
12-
import type { AnySchema } from '../DataModel/helperTypes';
12+
import type {
13+
AnyInteractionPreparation,
14+
AnySchema,
15+
} from '../DataModel/helperTypes';
1316
import type { SpecifyResource } from '../DataModel/legacyTypes';
1417
import { useAllSaveBlockers } from '../DataModel/saveBlockers';
15-
import type { Collection } from '../DataModel/specifyTable';
16-
import type {
17-
DisposalPreparation,
18-
GiftPreparation,
19-
LoanPreparation,
20-
} from '../DataModel/types';
18+
import type { Collection, SpecifyTable } from '../DataModel/specifyTable';
2119
import { FormTableCollection } from '../FormCells/FormTableCollection';
2220
import type { FormType } from '../FormParse';
2321
import type { SubViewSortField } from '../FormParse/cells';
2422
import { augmentMode, ResourceView } from '../Forms/ResourceView';
2523
import { useFirstFocus } from '../Forms/SpecifyForm';
24+
import type { InteractionWithPreps } from '../Interactions/helpers';
25+
import { interactionPrepTables } from '../Interactions/helpers';
2626
import { InteractionDialog } from '../Interactions/InteractionDialog';
2727
import { hasTablePermission } from '../Permissions/helpers';
2828
import { relationshipIsToMany } from '../WbPlanView/mappingHelpers';
@@ -104,8 +104,9 @@ export function IntegratedRecordSelector({
104104
typeof relationship === 'object' &&
105105
relationshipIsToMany(relationship) &&
106106
typeof collection.related === 'object' &&
107-
['LoanPreparation', 'GiftPreparation', 'DisposalPreparation'].includes(
108-
relationship.relatedTable.name
107+
interactionPrepTables.includes(
108+
(relationship.relatedTable as SpecifyTable<AnyInteractionPreparation>)
109+
.name
109110
);
110111

111112
const [isDialogOpen, handleOpenDialog, handleCloseDialog] = useBooleanState();
@@ -154,9 +155,7 @@ export function IntegratedRecordSelector({
154155
actionTable={collection.related.specifyTable}
155156
interactionResource={interactionResource}
156157
itemCollection={
157-
collection as Collection<
158-
DisposalPreparation | GiftPreparation | LoanPreparation
159-
>
158+
collection as Collection<AnyInteractionPreparation>
160159
}
161160
onClose={handleCloseDialog}
162161
/>

specifyweb/frontend/js_src/lib/components/Interactions/InteractionDialog.tsx

+14-24
Original file line numberDiff line numberDiff line change
@@ -25,24 +25,24 @@ import { H3 } from '../Atoms';
2525
import { Button } from '../Atoms/Button';
2626
import { Link } from '../Atoms/Link';
2727
import { LoadingContext, ReadOnlyContext } from '../Core/Contexts';
28-
import type { AnySchema, SerializedResource } from '../DataModel/helperTypes';
28+
import type { AnySchema, SerializedResource, AnyInteractionPreparation,
29+
SerializedResource, } from '../DataModel/helperTypes';
2930
import type { SpecifyResource } from '../DataModel/legacyTypes';
3031
import { getResourceViewUrl } from '../DataModel/resource';
3132
import type { LiteralField } from '../DataModel/specifyField';
3233
import type { Collection, SpecifyTable } from '../DataModel/specifyTable';
3334
import { tables } from '../DataModel/tables';
34-
import type {
35-
DisposalPreparation,
36-
Gift,
37-
GiftPreparation,
38-
LoanPreparation,
39-
RecordSet,
40-
} from '../DataModel/types';
35+
import type { RecordSet } from '../DataModel/types';
4136
import { AutoGrowTextArea } from '../Molecules/AutoGrowTextArea';
4237
import { Dialog } from '../Molecules/Dialog';
4338
import { userPreferences } from '../Preferences/userPreferences';
4439
import { RecordSetsDialog } from '../Toolbar/RecordSets';
45-
import type { PreparationData, PreparationRow } from './helpers';
40+
import type {
41+
InteractionWithPreps,
42+
PreparationData,
43+
PreparationRow,
44+
} from './helpers';
45+
import { interactionsWithPrepTables } from './helpers';
4646
import {
4747
getPrepsAvailableForLoanCoIds,
4848
getPrepsAvailableForLoanRs,
@@ -57,11 +57,9 @@ export function InteractionDialog({
5757
interactionResource,
5858
}: {
5959
readonly onClose: () => void;
60-
readonly actionTable: SpecifyTable;
60+
readonly actionTable: SpecifyTable<InteractionWithPreps>;
6161
readonly isLoanReturn?: boolean;
62-
readonly itemCollection?: Collection<
63-
DisposalPreparation | GiftPreparation | LoanPreparation
64-
>;
62+
readonly itemCollection?: Collection<AnyInteractionPreparation>;
6563
readonly interactionResource?: SpecifyResource<AnySchema>;
6664
}): JSX.Element {
6765
const itemTable = isLoanReturn ? tables.Loan : tables.CollectionObject;
@@ -245,7 +243,7 @@ export function InteractionDialog({
245243
// BUG: make this readOnly if don't have necessary permissions
246244
itemCollection={itemCollection}
247245
preparations={state.entries}
248-
table={actionTable as SpecifyTable<Gift>}
246+
table={actionTable}
249247
onClose={handleClose}
250248
/>
251249
) : (
@@ -299,19 +297,11 @@ export function InteractionDialog({
299297
>
300298
{interactionsText.addUnassociated()}
301299
</Button.Info>
302-
) : actionTable.name === 'Loan' &&
303-
state.type === 'MissingState' &&
304-
prepsData?.length === 0 ? (
305-
<Link.Info href={getResourceViewUrl('Loan')}>
300+
) : interactionsWithPrepTables.includes(actionTable.name) ? (
301+
<Link.Info href={getResourceViewUrl(actionTable.name)}>
306302
{interactionsText.withoutPreparations()}
307303
</Link.Info>
308304
) : undefined}
309-
{actionTable.name === 'Gift' &&
310-
itemCollection === undefined && (
311-
<Link.Info href={getResourceViewUrl('Gift')}>
312-
{interactionsText.withoutPreparations()}
313-
</Link.Info>
314-
)}
315305
{state.type === 'MissingState' &&
316306
prepsData?.length !== 0 &&
317307
prepsData ? (

specifyweb/frontend/js_src/lib/components/Interactions/InteractionsDialog.tsx

+17-20
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom';
44
import { useBooleanState } from '../../hooks/useBooleanState';
55
import { commonText } from '../../localization/common';
66
import { interactionsText } from '../../localization/interactions';
7-
import type { RA, RR } from '../../utils/types';
7+
import type { RA } from '../../utils/types';
88
import { Ul } from '../Atoms';
99
import { DataEntry } from '../Atoms/DataEntry';
1010
import { icons } from '../Atoms/Icons';
@@ -14,31 +14,23 @@ import { useDataEntryTables } from '../DataEntryTables/fetchTables';
1414
import { getResourceViewUrl } from '../DataModel/resource';
1515
import type { SpecifyTable } from '../DataModel/specifyTable';
1616
import { getTable, tables } from '../DataModel/tables';
17-
import type { Tables } from '../DataModel/types';
1817
import { Dialog, dialogClassNames } from '../Molecules/Dialog';
1918
import { TableIcon } from '../Molecules/TableIcon';
2019
import { hasTablePermission } from '../Permissions/helpers';
2120
import { ProtectedTable } from '../Permissions/PermissionDenied';
2221
import { Redirect } from '../Router/Redirect';
2322
import { OverlayContext } from '../Router/Router';
2423
import { fetchLegacyInteractions } from './fetch';
24+
import type { InteractionWithPreps } from './helpers';
25+
import { interactionsWithPrepTables } from './helpers';
2526
import { InteractionDialog } from './InteractionDialog';
2627

27-
export const interactionWorkflowTables: Partial<
28-
RR<keyof Tables, 'CollectionObject' | 'Preparation'>
29-
> = {
30-
/*
31-
* Accession: 'CollectionObject',
32-
* Appraisal: 'CollectionObject',
33-
*/
34-
Loan: 'CollectionObject',
35-
Gift: 'CollectionObject',
36-
Disposal: 'CollectionObject',
37-
/*
38-
* ExchangeOut: 'Preparation',
39-
* InfoRequest: 'CollectionObject',
40-
*/
41-
};
28+
/**
29+
* FEATURE: If needed, implement a dialog for:
30+
* Accession -> CollectionObjects
31+
* Appraisal -> CollectionObjects
32+
* InfoRequest -> CollectionObjects
33+
*/
4234

4335
export function InteractionsOverlay(): JSX.Element | null {
4436
const tables = useDataEntryTables('interactions');
@@ -79,7 +71,9 @@ function Interactions({
7971
href={
8072
table.name === 'LoanReturnPreparation'
8173
? `/specify/overlay/interactions/return-loan/`
82-
: table.name in interactionWorkflowTables
74+
: interactionsWithPrepTables.includes(
75+
(table as SpecifyTable<InteractionWithPreps>).name
76+
)
8377
? `/specify/overlay/interactions/create/${table.name}/`
8478
: getResourceViewUrl(table.name)
8579
}
@@ -103,8 +97,11 @@ export function InteractionAction(): JSX.Element | null {
10397
const { tableName = '' } = useParams();
10498
const rawTable = React.useMemo(() => getTable(tableName), [tableName]);
10599
const table =
106-
typeof rawTable === 'object' && rawTable.name in interactionWorkflowTables
107-
? rawTable
100+
typeof rawTable === 'object' &&
101+
interactionsWithPrepTables.includes(
102+
(rawTable as SpecifyTable<InteractionWithPreps>).name
103+
)
104+
? (rawTable as SpecifyTable<InteractionWithPreps>)
108105
: undefined;
109106
return table === undefined ? (
110107
<Redirect to="/specify/overlay/interactions/" />

specifyweb/frontend/js_src/lib/components/Interactions/PrepDialog.tsx

+40-32
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,23 @@ import { useLiveState } from '../../hooks/useLiveState';
66
import { commonText } from '../../localization/common';
77
import { interactionsText } from '../../localization/interactions';
88
import type { RA } from '../../utils/types';
9-
import { filterArray } from '../../utils/types';
9+
import { defined, filterArray } from '../../utils/types';
1010
import { group, replaceItem } from '../../utils/utils';
1111
import { Button } from '../Atoms/Button';
1212
import { Form, Input, Label } from '../Atoms/Form';
1313
import { Submit } from '../Atoms/Submit';
1414
import { ReadOnlyContext } from '../Core/Contexts';
1515
import { getField, toTable } from '../DataModel/helpers';
16+
import type { AnyInteractionPreparation } from '../DataModel/helperTypes';
1617
import type { SpecifyResource } from '../DataModel/legacyTypes';
1718
import { getResourceApiUrl, getResourceViewUrl } from '../DataModel/resource';
1819
import { serializeResource } from '../DataModel/serializers';
1920
import type { Collection, SpecifyTable } from '../DataModel/specifyTable';
20-
import { strictGetTable, tables } from '../DataModel/tables';
21-
import type {
22-
Disposal,
23-
DisposalPreparation,
24-
Gift,
25-
GiftPreparation,
26-
Loan,
27-
LoanPreparation,
28-
} from '../DataModel/types';
21+
import { tables } from '../DataModel/tables';
22+
import type { ExchangeOut, ExchangeOutPrep } from '../DataModel/types';
2923
import { Dialog } from '../Molecules/Dialog';
30-
import type { PreparationData } from './helpers';
24+
import type { InteractionWithPreps, PreparationData } from './helpers';
25+
import { interactionPrepTables } from './helpers';
3126
import { PrepDialogRow } from './PrepDialogRow';
3227

3328
export function PrepDialog({
@@ -38,10 +33,8 @@ export function PrepDialog({
3833
}: {
3934
readonly onClose: () => void;
4035
readonly preparations: RA<PreparationData>;
41-
readonly table: SpecifyTable<Disposal | Gift | Loan>;
42-
readonly itemCollection?: Collection<
43-
DisposalPreparation | GiftPreparation | LoanPreparation
44-
>;
36+
readonly table: SpecifyTable<InteractionWithPreps>;
37+
readonly itemCollection?: Collection<AnyInteractionPreparation>;
4538
}): JSX.Element {
4639
const preparations = React.useMemo(() => {
4740
if (itemCollection === undefined) return rawPreparations;
@@ -152,11 +145,16 @@ export function PrepDialog({
152145
<Form
153146
id={id('form')}
154147
onSubmit={(): void => {
155-
const itemTable = strictGetTable(
156-
`${table.name}Preparation`
157-
) as SpecifyTable<
158-
DisposalPreparation | GiftPreparation | LoanPreparation
159-
>;
148+
const itemTable = defined(
149+
table.relationships.find((relationship) =>
150+
interactionPrepTables.includes(
151+
(
152+
relationship.relatedTable as SpecifyTable<AnyInteractionPreparation>
153+
).name
154+
)
155+
)?.relatedTable
156+
) as SpecifyTable<AnyInteractionPreparation>;
157+
160158
const items = filterArray(
161159
preparations.map((preparation, index) => {
162160
if (selected[index] === 0) return undefined;
@@ -178,20 +176,10 @@ export function PrepDialog({
178176
handleClose();
179177
} else {
180178
const interaction = new table.Resource();
179+
setPreparationItems(interaction, items);
180+
181181
const loan = toTable(interaction, 'Loan');
182-
loan?.set(
183-
'loanPreparations',
184-
items as RA<SpecifyResource<LoanPreparation>>
185-
);
186182
loan?.set('isClosed', false);
187-
toTable(interaction, 'Gift')?.set(
188-
'giftPreparations',
189-
items as RA<SpecifyResource<GiftPreparation>>
190-
);
191-
toTable(interaction, 'Disposal')?.set(
192-
'disposalPreparations',
193-
items as RA<SpecifyResource<DisposalPreparation>>
194-
);
195183
navigate(getResourceViewUrl(table.name, undefined), {
196184
state: {
197185
type: 'RecordSet',
@@ -238,3 +226,23 @@ export function PrepDialog({
238226
</Dialog>
239227
);
240228
}
229+
230+
function setPreparationItems(
231+
interaction: SpecifyResource<InteractionWithPreps>,
232+
items: RA<SpecifyResource<AnyInteractionPreparation>>
233+
): void {
234+
const preparationRelationship = defined(
235+
interaction.specifyTable.relationships.find((relationship) =>
236+
interactionPrepTables.includes(
237+
(relationship.relatedTable as SpecifyTable<AnyInteractionPreparation>)
238+
.name
239+
)
240+
)
241+
);
242+
243+
// Typecast as a single case because the relatiships do not exist in the union type.
244+
(interaction as SpecifyResource<ExchangeOut>).set(
245+
preparationRelationship.name as 'exchangeOutPreps',
246+
items as RA<SpecifyResource<ExchangeOutPrep>>
247+
);
248+
}

specifyweb/frontend/js_src/lib/components/Interactions/helpers.ts

+36-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,29 @@
11
import { ajax } from '../../utils/ajax';
22
import { formData } from '../../utils/ajax/helpers';
3-
import type { RA } from '../../utils/types';
3+
import type { RA, RestrictedTuple } from '../../utils/types';
4+
import type { AnyInteractionPreparation } from '../DataModel/helperTypes';
5+
import type { Tables } from '../DataModel/types';
6+
7+
export const interactionPrepTables: RestrictedTuple<
8+
AnyInteractionPreparation['tableName']
9+
> = [
10+
'DisposalPreparation',
11+
'ExchangeInPrep',
12+
'ExchangeOutPrep',
13+
'GiftPreparation',
14+
'LoanPreparation',
15+
];
16+
17+
type ExtractInteraction<T extends string> =
18+
T extends `${infer Prefix}Prep${string}` ? Prefix : never;
19+
20+
export type InteractionWithPreps = Tables[ExtractInteraction<
21+
AnyInteractionPreparation['tableName']
22+
>];
23+
24+
export const interactionsWithPrepTables: RestrictedTuple<
25+
InteractionWithPreps['tableName']
26+
> = ['Disposal', 'ExchangeIn', 'ExchangeOut', 'Gift', 'Loan'];
427

528
export type PreparationData = {
629
readonly catalogNumber: string;
@@ -17,18 +40,19 @@ export type PreparationData = {
1740
};
1841

1942
export type PreparationRow = readonly [
20-
string,
21-
number,
22-
string,
23-
number,
24-
number,
25-
string,
26-
number,
27-
string | null,
28-
string | null,
29-
string | null,
30-
string
43+
catalogNumber: string,
44+
collectionObjectId: number,
45+
taxonFullName: string,
46+
taxonId: number,
47+
preparationId: number,
48+
prepType: string,
49+
preparationCountAmt: number,
50+
amountLoaned: string | null,
51+
amountedGifted: string | null,
52+
amountExchanged: string | null,
53+
amountAvailable: string
3154
];
55+
3256
export const getPrepsAvailableForLoanRs = async (recordSetId: number) =>
3357
ajax<RA<PreparationRow>>(
3458
`/interactions/preparations_available_rs/${recordSetId}/`,

0 commit comments

Comments
 (0)