Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open interaction dialog when adding prep in ExchangeOut #4805

Merged
merged 14 commits into from May 21, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -9,20 +9,20 @@ import { Button } from '../Atoms/Button';
import { DataEntry } from '../Atoms/DataEntry';
import { ReadOnlyContext } from '../Core/Contexts';
import { DependentCollection } from '../DataModel/collectionApi';
import type { AnySchema } from '../DataModel/helperTypes';
import type {
AnyInteractionPreparation,
AnySchema,
} from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { useAllSaveBlockers } from '../DataModel/saveBlockers';
import type { Collection } from '../DataModel/specifyTable';
import type {
DisposalPreparation,
GiftPreparation,
LoanPreparation,
} from '../DataModel/types';
import type { Collection, SpecifyTable } from '../DataModel/specifyTable';
import { FormTableCollection } from '../FormCells/FormTableCollection';
import type { FormType } from '../FormParse';
import type { SubViewSortField } from '../FormParse/cells';
import { augmentMode, ResourceView } from '../Forms/ResourceView';
import { useFirstFocus } from '../Forms/SpecifyForm';
import type { InteractionWithPreps } from '../Interactions/helpers';
import { interactionPrepTables } from '../Interactions/helpers';
import { InteractionDialog } from '../Interactions/InteractionDialog';
import { hasTablePermission } from '../Permissions/helpers';
import { relationshipIsToMany } from '../WbPlanView/mappingHelpers';
Expand Down Expand Up @@ -101,8 +101,9 @@ export function IntegratedRecordSelector({
typeof relationship === 'object' &&
relationshipIsToMany(relationship) &&
typeof collection.related === 'object' &&
['LoanPreparation', 'GiftPreparation', 'DisposalPreparation'].includes(
relationship.relatedTable.name
interactionPrepTables.includes(
(relationship.relatedTable as SpecifyTable<AnyInteractionPreparation>)
.name
);

const [isDialogOpen, handleOpenDialog, handleCloseDialog] = useBooleanState();
Expand Down Expand Up @@ -145,11 +146,12 @@ export function IntegratedRecordSelector({
typeof collection.related === 'object' &&
isDialogOpen ? (
<InteractionDialog
actionTable={collection.related.specifyTable}
actionTable={
collection.related
.specifyTable as SpecifyTable<InteractionWithPreps>
}
itemCollection={
collection as Collection<
DisposalPreparation | GiftPreparation | LoanPreparation
>
collection as Collection<AnyInteractionPreparation>
}
onClose={handleCloseDialog}
/>
Expand Down
Expand Up @@ -25,23 +25,24 @@ import { H3 } from '../Atoms';
import { Button } from '../Atoms/Button';
import { Link } from '../Atoms/Link';
import { LoadingContext, ReadOnlyContext } from '../Core/Contexts';
import type { SerializedResource } from '../DataModel/helperTypes';
import type {
AnyInteractionPreparation,
SerializedResource,
} from '../DataModel/helperTypes';
import { getResourceViewUrl } from '../DataModel/resource';
import type { LiteralField } from '../DataModel/specifyField';
import type { Collection, SpecifyTable } from '../DataModel/specifyTable';
import { tables } from '../DataModel/tables';
import type {
DisposalPreparation,
Gift,
GiftPreparation,
LoanPreparation,
RecordSet,
} from '../DataModel/types';
import type { RecordSet } from '../DataModel/types';
import { AutoGrowTextArea } from '../Molecules/AutoGrowTextArea';
import { Dialog } from '../Molecules/Dialog';
import { userPreferences } from '../Preferences/userPreferences';
import { RecordSetsDialog } from '../Toolbar/RecordSets';
import type { PreparationData, PreparationRow } from './helpers';
import type {
InteractionWithPreps,
PreparationData,
PreparationRow,
} from './helpers';
import {
getPrepsAvailableForLoanCoIds,
getPrepsAvailableForLoanRs,
Expand All @@ -55,11 +56,9 @@ export function InteractionDialog({
itemCollection,
}: {
readonly onClose: () => void;
readonly actionTable: SpecifyTable;
readonly actionTable: SpecifyTable<InteractionWithPreps>;
readonly isLoanReturn?: boolean;
readonly itemCollection?: Collection<
DisposalPreparation | GiftPreparation | LoanPreparation
>;
readonly itemCollection?: Collection<AnyInteractionPreparation>;
}): JSX.Element {
const itemTable = isLoanReturn ? tables.Loan : tables.CollectionObject;
const searchField = itemTable.strictGetLiteralField(
Expand Down Expand Up @@ -231,7 +230,7 @@ export function InteractionDialog({
// BUG: make this readOnly if don't have necessary permissions
itemCollection={itemCollection}
preparations={state.entries}
table={actionTable as SpecifyTable<Gift>}
table={actionTable}
onClose={handleClose}
/>
) : (
Expand Down
Expand Up @@ -4,7 +4,7 @@ import { useParams } from 'react-router-dom';
import { useBooleanState } from '../../hooks/useBooleanState';
import { commonText } from '../../localization/common';
import { interactionsText } from '../../localization/interactions';
import type { RA, RR } from '../../utils/types';
import type { RA } from '../../utils/types';
import { Ul } from '../Atoms';
import { DataEntry } from '../Atoms/DataEntry';
import { icons } from '../Atoms/Icons';
Expand All @@ -14,31 +14,23 @@ import { useDataEntryTables } from '../DataEntryTables/fetchTables';
import { getResourceViewUrl } from '../DataModel/resource';
import type { SpecifyTable } from '../DataModel/specifyTable';
import { getTable, tables } from '../DataModel/tables';
import type { Tables } from '../DataModel/types';
import { Dialog, dialogClassNames } from '../Molecules/Dialog';
import { TableIcon } from '../Molecules/TableIcon';
import { hasTablePermission } from '../Permissions/helpers';
import { ProtectedTable } from '../Permissions/PermissionDenied';
import { Redirect } from '../Router/Redirect';
import { OverlayContext } from '../Router/Router';
import { fetchLegacyInteractions } from './fetch';
import type { InteractionWithPreps } from './helpers';
import { interactionsWithPrepTables } from './helpers';
import { InteractionDialog } from './InteractionDialog';

export const interactionWorkflowTables: Partial<
RR<keyof Tables, 'CollectionObject' | 'Preparation'>
> = {
/*
* Accession: 'CollectionObject',
* Appraisal: 'CollectionObject',
*/
Loan: 'CollectionObject',
Gift: 'CollectionObject',
Disposal: 'CollectionObject',
/*
* ExchangeOut: 'Preparation',
* InfoRequest: 'CollectionObject',
*/
};
/**
* FEATURE: If needed, implement a dialog for:
* Accession -> CollectionObjects
* Appraisal -> CollectionObjects
* InfoRequest -> CollectionObjects
*/

export function InteractionsOverlay(): JSX.Element | null {
const tables = useDataEntryTables('interactions');
Expand Down Expand Up @@ -79,7 +71,9 @@ function Interactions({
href={
table.name === 'LoanReturnPreparation'
? `/specify/overlay/interactions/return-loan/`
: table.name in interactionWorkflowTables
: interactionsWithPrepTables.includes(
(table as SpecifyTable<InteractionWithPreps>).name
)
? `/specify/overlay/interactions/create/${table.name}/`
: getResourceViewUrl(table.name)
}
Expand All @@ -103,8 +97,11 @@ export function InteractionAction(): JSX.Element | null {
const { tableName = '' } = useParams();
const rawTable = React.useMemo(() => getTable(tableName), [tableName]);
const table =
typeof rawTable === 'object' && rawTable.name in interactionWorkflowTables
? rawTable
typeof rawTable === 'object' &&
interactionsWithPrepTables.includes(
(rawTable as SpecifyTable<InteractionWithPreps>).name
)
? (rawTable as SpecifyTable<InteractionWithPreps>)
: undefined;
return table === undefined ? (
<Redirect to="/specify/overlay/interactions/" />
Expand Down
Expand Up @@ -6,28 +6,23 @@ import { useLiveState } from '../../hooks/useLiveState';
import { commonText } from '../../localization/common';
import { interactionsText } from '../../localization/interactions';
import type { RA } from '../../utils/types';
import { filterArray } from '../../utils/types';
import { defined, filterArray } from '../../utils/types';
import { group, replaceItem } from '../../utils/utils';
import { Button } from '../Atoms/Button';
import { Form, Input, Label } from '../Atoms/Form';
import { Submit } from '../Atoms/Submit';
import { ReadOnlyContext } from '../Core/Contexts';
import { getField, toTable } from '../DataModel/helpers';
import type { AnyInteractionPreparation } from '../DataModel/helperTypes';
import type { SpecifyResource } from '../DataModel/legacyTypes';
import { getResourceApiUrl, getResourceViewUrl } from '../DataModel/resource';
import { serializeResource } from '../DataModel/serializers';
import type { Collection, SpecifyTable } from '../DataModel/specifyTable';
import { strictGetTable, tables } from '../DataModel/tables';
import type {
Disposal,
DisposalPreparation,
Gift,
GiftPreparation,
Loan,
LoanPreparation,
} from '../DataModel/types';
import { tables } from '../DataModel/tables';
import type { ExchangeOut, ExchangeOutPrep } from '../DataModel/types';
import { Dialog } from '../Molecules/Dialog';
import type { PreparationData } from './helpers';
import type { InteractionWithPreps, PreparationData } from './helpers';
import { interactionPrepTables } from './helpers';
import { PrepDialogRow } from './PrepDialogRow';

export function PrepDialog({
Expand All @@ -38,10 +33,8 @@ export function PrepDialog({
}: {
readonly onClose: () => void;
readonly preparations: RA<PreparationData>;
readonly table: SpecifyTable<Disposal | Gift | Loan>;
readonly itemCollection?: Collection<
DisposalPreparation | GiftPreparation | LoanPreparation
>;
readonly table: SpecifyTable<InteractionWithPreps>;
readonly itemCollection?: Collection<AnyInteractionPreparation>;
}): JSX.Element {
const preparations = React.useMemo(() => {
if (itemCollection === undefined) return rawPreparations;
Expand Down Expand Up @@ -152,11 +145,16 @@ export function PrepDialog({
<Form
id={id('form')}
onSubmit={(): void => {
const itemTable = strictGetTable(
`${table.name}Preparation`
) as SpecifyTable<
DisposalPreparation | GiftPreparation | LoanPreparation
>;
const itemTable = defined(
table.relationships.find((relationship) =>
interactionPrepTables.includes(
(
relationship.relatedTable as SpecifyTable<AnyInteractionPreparation>
).name
)
)?.table
) as SpecifyTable<AnyInteractionPreparation>;

const items = filterArray(
preparations.map((preparation, index) => {
if (selected[index] === 0) return undefined;
Expand All @@ -178,20 +176,10 @@ export function PrepDialog({
handleClose();
} else {
const interaction = new table.Resource();
setPreparationItems(interaction, items);

const loan = toTable(interaction, 'Loan');
loan?.set(
'loanPreparations',
items as RA<SpecifyResource<LoanPreparation>>
);
loan?.set('isClosed', false);
toTable(interaction, 'Gift')?.set(
'giftPreparations',
items as RA<SpecifyResource<GiftPreparation>>
);
toTable(interaction, 'Disposal')?.set(
'disposalPreparations',
items as RA<SpecifyResource<DisposalPreparation>>
);
navigate(getResourceViewUrl(table.name, undefined), {
state: {
type: 'RecordSet',
Expand Down Expand Up @@ -238,3 +226,23 @@ export function PrepDialog({
</Dialog>
);
}

function setPreparationItems(
interaction: SpecifyResource<InteractionWithPreps>,
items: RA<SpecifyResource<AnyInteractionPreparation>>
): void {
const preparationRelationship = defined(
interaction.specifyTable.relationships.find((relationship) =>
interactionPrepTables.includes(
(relationship.relatedTable as SpecifyTable<AnyInteractionPreparation>)
.name
)
)
);

// Typecast as a single case because the relatiships do not exist in the union type.
(interaction as SpecifyResource<ExchangeOut>).set(
preparationRelationship.name as 'exchangeOutPreps',
items as RA<SpecifyResource<ExchangeOutPrep>>
);
}
48 changes: 36 additions & 12 deletions specifyweb/frontend/js_src/lib/components/Interactions/helpers.ts
@@ -1,6 +1,29 @@
import { ajax } from '../../utils/ajax';
import { formData } from '../../utils/ajax/helpers';
import type { RA } from '../../utils/types';
import type { RA, RestrictedTuple } from '../../utils/types';
import type { AnyInteractionPreparation } from '../DataModel/helperTypes';
import type { Tables } from '../DataModel/types';

export const interactionPrepTables: RestrictedTuple<
AnyInteractionPreparation['tableName']
> = [
'DisposalPreparation',
'ExchangeInPrep',
'ExchangeOutPrep',
'GiftPreparation',
'LoanPreparation',
];

type ExtractInteraction<T extends string> =
T extends `${infer Prefix}Prep${string}` ? Prefix : never;

export type InteractionWithPreps = Tables[ExtractInteraction<
AnyInteractionPreparation['tableName']
>];

export const interactionsWithPrepTables: RestrictedTuple<
InteractionWithPreps['tableName']
> = ['Disposal', 'ExchangeIn', 'ExchangeOut', 'Gift', 'Loan'];

export type PreparationData = {
readonly catalogNumber: string;
Expand All @@ -17,18 +40,19 @@ export type PreparationData = {
};

export type PreparationRow = readonly [
string,
number,
string,
number,
number,
string,
number,
string | null,
string | null,
string | null,
string
catalogNumber: string,
collectionObjectId: number,
taxonFullName: string,
taxonId: number,
preparationId: number,
prepType: string,
preparationCountAmt: number,
amountLoaned: string | null,
amountedGifted: string | null,
amountExchanged: string | null,
amountAvailable: string
];

export const getPrepsAvailableForLoanRs = async (recordSetId: number) =>
ajax<RA<PreparationRow>>(
`/interactions/preparations_available_rs/${recordSetId}/`,
Expand Down