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

Batch edit for relationships #6283

Draft
wants to merge 17 commits into
base: issue-2331
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
19 changes: 12 additions & 7 deletions specifyweb/frontend/js_src/css/workbench.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,16 @@
}

/* CONTENT styles */
.wbs-form.wb-show-upload-results .wb-no-match-cell
.wbs-form.wb-show-upload-results .wb-updated-cell
.wbs-form.wb-show-upload-results .wb-deleted-cell
.wbs-form.wb-show-upload-results .wb-matched-and-changed-cell
.wbs-form.wb-focus-coordinates .wb-coordinate-cell {
.wbs-form.wb-show-upload-results
.wb-no-match-cell
.wbs-form.wb-show-upload-results
.wb-updated-cell
.wbs-form.wb-show-upload-results
.wb-deleted-cell
.wbs-form.wb-show-upload-results
.wb-matched-and-changed-cell
.wbs-form.wb-focus-coordinates
.wb-coordinate-cell {
@apply text-black dark:text-white;
}

Expand All @@ -58,8 +63,8 @@
.wb-modified-cell,
.htCommentCell,
.wb-search-match-cell,
.wb-updated-cell,
.wb-deleted-cell,
.wb-updated-cell,
.wb-deleted-cell,
.wb-matched-and-changed-cell
),
.wb-navigation-section {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function AttachmentDatasetMeta({
return (
<DataSetMeta
dataset={dataset}
datasetVariant='bulkAttachment'
datasetVariant="bulkAttachment"
deleteDescription={attachmentsText.deleteAttachmentDataSetDescription()}
permissionResource="/attachment_import/dataset"
onChange={(changed) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,11 @@ function containsFaultyNestedToMany(queryFieldSpec: QueryFieldSpec): boolean {
relationship.isRelationship && relationshipIsToMany(relationship)
);

const allowedToMany = isTreeTable(queryFieldSpec.table.name) ? 0 : 1;
const allowedToMany =
isTreeTable(queryFieldSpec.baseTable.name) &&
isTreeTable(queryFieldSpec.table.name)
? 0
: 1;
return nestedToManyCount.length > allowedToMany;
}

Expand Down
10 changes: 6 additions & 4 deletions specifyweb/frontend/js_src/lib/components/DataModel/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type {
import type { SpecifyResource } from './legacyTypes';
import { schema } from './schema';
import { serializeResource } from './serializers';
import { SpecifyTable } from './specifyTable';
import type { SpecifyTable } from './specifyTable';
import { genericTables, getTable, tables } from './tables';
import type { Tables } from './types';
import { getUniquenessRules } from './uniquenessRules';
Expand Down Expand Up @@ -343,9 +343,11 @@ export const exportsForTests = {
};

setDevelopmentGlobal('_getUniqueFields', (): void => {
// Batch-editor clones records in independent-to-one no-match cases. It needs to be aware of the fields to not clone. It's fine if it doesn't respect user preferences (for now), but needs to be replicate
// front-end logic. So, the "fields to not clone" must be identical. This is done by storing them as a static file, which frontend and backend both access + a unit test to make sure the file is up-to-date.
// In the case where the user is really doesn't want to carry-over some fields, they can simply add those fields in batch-edit query (and then set them to null) so it handles general use case pretty well.
/*
* Batch-editor clones records in independent-to-one no-match cases. It needs to be aware of the fields to not clone. It's fine if it doesn't respect user preferences (for now), but needs to be replicate
* front-end logic. So, the "fields to not clone" must be identical. This is done by storing them as a static file, which frontend and backend both access + a unit test to make sure the file is up-to-date.
* In the case where the user is really doesn't want to carry-over some fields, they can simply add those fields in batch-edit query (and then set them to null) so it handles general use case pretty well.
*/
const allTablesResult = Object.fromEntries(
Object.values(tables).map((table) => [
table.name.toLowerCase(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const rawMenuItems = ensure<IR<Omit<MenuItem, 'name'>>>()({
url: '/specify/overlay/batch-edit',
title: batchEditText.batchEdit(),
icon: icons.table,
}
},
} as const);

export type MenuItemName = keyof typeof rawMenuItems | 'search';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';

import { attachmentsText } from '../../localization/attachments';
import { batchEditText } from '../../localization/batchEdit';
import { commonText } from '../../localization/common';
import { headerText } from '../../localization/header';
import { interactionsText } from '../../localization/interactions';
Expand All @@ -15,7 +16,6 @@ import { wbText } from '../../localization/workbench';
import type { RA } from '../../utils/types';
import { Redirect } from './Redirect';
import type { EnhancedRoute } from './RouterUtils';
import { batchEditText } from '../../localization/batchEdit';

/* eslint-disable @typescript-eslint/promise-function-async */
/**
Expand Down
8 changes: 4 additions & 4 deletions specifyweb/frontend/js_src/lib/components/WbActions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,16 +222,16 @@ export function WbActions({
mode === 'validate'
? wbText.validationCanceled()
: mode === 'unupload'
? wbText.rollbackCanceled()
: viewerLocalization.doCancelled
? wbText.rollbackCanceled()
: viewerLocalization.doCancelled
}
onClose={closeAbortedMessage}
>
{mode === 'validate'
? wbText.validationCanceledDescription()
: mode === 'unupload'
? wbText.rollbackCanceledDescription()
: viewerLocalization.doCancelledDescription}
? wbText.rollbackCanceledDescription()
: viewerLocalization.doCancelledDescription}
</Dialog>
)}
</>
Expand Down
128 changes: 92 additions & 36 deletions specifyweb/frontend/js_src/lib/components/WbPlanView/Mapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import type { MappingElementProps } from './LineComponents';
import { getMappingLineProps, MappingLineComponent } from './LineComponents';
import { columnOptionsAreDefault } from './linesGetter';
import {
BatchEditPrefsView,
ChangeBaseTable,
EmptyDataSetDialog,
mappingOptionsMenu,
Expand Down Expand Up @@ -102,17 +103,36 @@ export type MappingState = State<
readonly autoMapperSuggestions?: RA<AutoMapperSuggestion>;
readonly openSelectElement?: SelectElementPosition;
readonly validationResults: RA<MappingPath>;
readonly batchEditPrefs?: BatchEditPrefs;
}
>;

export type ReadonlySpec = {
readonly mustMatch: boolean;
readonly columnOptions: boolean;
readonly batchEditPrefs: boolean;
};

export type BatchEditPrefs = {
readonly deferForNullCheck: boolean;
readonly deferForMatch: boolean;
};

export const DEFAULT_BATCH_EDIT_PREFS: BatchEditPrefs = {
deferForMatch: true,
deferForNullCheck: false,
} as const;

export const getDefaultMappingState = ({
changesMade,
lines,
mustMatchPreferences,
batchEditPrefs,
}: {
readonly changesMade: boolean;
readonly lines: RA<MappingLine>;
readonly mustMatchPreferences: IR<boolean>;
readonly batchEditPrefs?: BatchEditPrefs;
}): MappingState => ({
type: 'MappingState',
showHiddenFields: getCache('wbPlanViewUi', 'showHiddenFields') ?? false,
Expand All @@ -124,6 +144,7 @@ export const getDefaultMappingState = ({
focusedLine: 0,
changesMade,
mustMatchPreferences,
batchEditPrefs,
});

// REFACTOR: split component into smaller components
Expand All @@ -133,19 +154,23 @@ export function Mapper(props: {
readonly onChangeBaseTable: () => void;
readonly onSave: (
lines: RA<MappingLine>,
mustMatchPreferences: IR<boolean>
mustMatchPreferences: IR<boolean>,
batchEditPrefs?: BatchEditPrefs
) => Promise<void>;
// Initial values for the state:
readonly changesMade: boolean;
readonly lines: RA<MappingLine>;
readonly mustMatchPreferences: IR<boolean>;
readonly readonlySpec?: ReadonlySpec;
readonly batchEditPrefs?: BatchEditPrefs;
}): JSX.Element {
const [state, dispatch] = React.useReducer(
reducer,
{
changesMade: props.changesMade,
lines: props.lines,
mustMatchPreferences: props.mustMatchPreferences,
batchEditPrefs: props.batchEditPrefs,
},
getDefaultMappingState
);
Expand Down Expand Up @@ -264,7 +289,13 @@ export function Mapper(props: {
const validationResults = ignoreValidation ? [] : validate();
if (validationResults.length === 0) {
unsetUnloadProtect();
loading(props.onSave(state.lines, state.mustMatchPreferences));
loading(
props.onSave(
state.lines,
state.mustMatchPreferences,
state.batchEditPrefs
)
);
} else
dispatch({
type: 'ValidationAction',
Expand Down Expand Up @@ -294,6 +325,11 @@ export function Mapper(props: {
mappingPathIsComplete(state.mappingView) &&
getMappedFieldsBind(state.mappingView).length === 0;

const disableSave =
props.readonlySpec === undefined
? isReadOnly
: Object.values(props.readonlySpec).every(Boolean);

return (
<Layout
buttonsLeft={
Expand Down Expand Up @@ -337,38 +373,57 @@ export function Mapper(props: {
})
}
/>
<MustMatch
getMustMatchPreferences={(): IR<boolean> =>
getMustMatchTables({
baseTableName: props.baseTableName,
lines: state.lines,
mustMatchPreferences: state.mustMatchPreferences,
})
}
onChange={(mustMatchPreferences): void =>
dispatch({
type: 'MustMatchPrefChangeAction',
mustMatchPreferences,
})
}
onClose={(): void => {
/*
* Since setting table as must match causes all of its fields to
* be optional, we may have to rerun validation on
* mustMatchPreferences changes
*/
if (
state.validationResults.length > 0 &&
state.lines.some(({ mappingPath }) =>
mappingPathIsComplete(mappingPath)
)
)
{typeof props.batchEditPrefs === 'object' ? (
<ReadOnlyContext.Provider
value={props.readonlySpec?.batchEditPrefs ?? isReadOnly}
>
<BatchEditPrefsView
prefs={props.batchEditPrefs}
onChange={(prefs) =>
dispatch({
type: 'ChangeBatchEditPrefs',
prefs,
})
}
/>
</ReadOnlyContext.Provider>
) : null}
<ReadOnlyContext.Provider
value={props.readonlySpec?.mustMatch ?? isReadOnly}
>
<MustMatch
getMustMatchPreferences={(): IR<boolean> =>
getMustMatchTables({
baseTableName: props.baseTableName,
lines: state.lines,
mustMatchPreferences: state.mustMatchPreferences,
})
}
onChange={(mustMatchPreferences): void =>
dispatch({
type: 'ValidationAction',
validationResults: validate(),
});
}}
/>
type: 'ChangeMustMatchPrefAction',
mustMatchPreferences,
})
}
onClose={(): void => {
/*
* Since setting table as must match causes all of its fields to
* be optional, we may have to rerun validation on
* mustMatchPreferences changes
*/
if (
state.validationResults.length > 0 &&
state.lines.some(({ mappingPath }) =>
mappingPathIsComplete(mappingPath)
)
)
dispatch({
type: 'ValidationAction',
validationResults: validate(),
});
}}
/>
</ReadOnlyContext.Provider>
{!isReadOnly && (
<Button.Small
className={
Expand Down Expand Up @@ -396,11 +451,12 @@ export function Mapper(props: {
>
{isReadOnly ? wbText.dataEditor() : commonText.cancel()}
</Link.Small>
{!isReadOnly && (
{!disableSave && (
<Button.Small
disabled={!state.changesMade}
variant={className.saveButton}
onClick={(): void => handleSave(false)}
// This is a bit complicated to resolve correctly. Each component should have its own validator..
onClick={(): void => handleSave(isReadOnly)}
>
{commonText.save()}
</Button.Small>
Expand Down Expand Up @@ -557,7 +613,7 @@ export function Mapper(props: {
customSelectSubtype: 'simple',
fieldsData: mappingOptionsMenu({
id: (suffix) => id(`column-options-${line}-${suffix}`),
isReadOnly,
isReadOnly: props.readonlySpec?.columnOptions ?? isReadOnly,
columnOptions,
onChangeMatchBehaviour: (matchBehavior) =>
dispatch({
Expand Down
Loading
Loading