Skip to content

Commit

Permalink
Allow editing tree definition
Browse files Browse the repository at this point in the history
  • Loading branch information
maxpatiiuk committed Apr 11, 2022
1 parent 069eee0 commit 07b58f9
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 118 deletions.
111 changes: 75 additions & 36 deletions specifyweb/frontend/js_src/lib/components/toolbar/treerepair.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
import React from 'react';

import { ping } from '../../ajax';
import type { TaxonTreeDef } from '../../datamodel';
import type { FilterTablesByEndsWith } from '../../datamodelutils';
import { f } from '../../functools';
import type { SpecifyResource } from '../../legacytypes';
import commonText from '../../localization/common';
import { hasTreeAccess } from '../../permissions';
import * as querystring from '../../querystring';
import { getTreeModel } from '../../schema';
import { getDisciplineTrees, treeRanksPromise } from '../../treedefinitions';
import { defined } from '../../types';
import { Button, className, Link, Ul } from '../basic';
import {
getDisciplineTrees,
treeDefinitions,
treeRanksPromise,
} from '../../treedefinitions';
import { Button, className, DataEntry, Link, Ul } from '../basic';
import { TableIcon } from '../common';
import { useAsyncState, useTitle } from '../hooks';
import { LoadingContext } from '../contexts';
import { useAsyncState, useBooleanState, useTitle } from '../hooks';
import type { UserTool } from '../main';
import { Dialog } from '../modaldialog';
import createBackboneView from '../reactbackboneextend';
import { LoadingContext } from '../contexts';
import { f } from '../../functools';
import { hasTreeAccess } from '../../permissions';
import { ResourceView } from '../resourceview';

export function TreeSelectDialog({
onClose: handleClose,
Expand All @@ -33,10 +39,6 @@ export function TreeSelectDialog({
true
);

const trees = Object.fromEntries(
getDisciplineTrees().map((tree) => [tree, defined(getTreeModel(tree))])
);

return typeof treeRanks === 'object' ? (
<Dialog
header={title}
Expand All @@ -49,30 +51,42 @@ export function TreeSelectDialog({
>
<nav>
<Ul>
{Object.entries(trees)
.filter(([_tree, { name }]) => hasTreeAccess(name, 'update'))
.map(([tree, model]) => (
<li key={tree}>
<Link.Default
href={getLink(tree)}
className={
typeof handleClick === 'function'
? className.navigationHandled
: undefined
}
onClick={(event): void => {
if (typeof handleClick === 'undefined') return;
event.preventDefault();
loading(
Promise.resolve(handleClick(tree)).then(handleClose)
);
}}
>
<TableIcon name={tree} tableLabel={false} />
{model.label}
</Link.Default>
</li>
))}
{getDisciplineTrees()
.filter((treeName) => hasTreeAccess(treeName, 'update'))
.map((treeName) =>
f.var(
treeDefinitions[treeName]
.definition as SpecifyResource<TaxonTreeDef>,
(treeDefinition) => (
<li key={treeName}>
<div className="flex gap-2">
<Link.Default
href={getLink(treeName)}
className={`flex-1 ${
typeof handleClick === 'function'
? className.navigationHandled
: undefined
}`}
onClick={(event): void => {
if (typeof handleClick === 'undefined') return;
event.preventDefault();
loading(
Promise.resolve(handleClick(treeName)).then(
handleClose
)
);
}}
title={treeDefinition.get('remarks') ?? undefined}
>
<TableIcon name={treeName} tableLabel={false} />
{treeDefinition.get('name')}
</Link.Default>
<EditTreeDefinition treeDefinition={treeDefinition} />
</div>
</li>
)
)
)}
</Ul>
</nav>
</Dialog>
Expand Down Expand Up @@ -108,6 +122,31 @@ function RepairTree({
);
}

export function EditTreeDefinition({
treeDefinition,
}: {
readonly treeDefinition: SpecifyResource<FilterTablesByEndsWith<'TreeDef'>>;
}): JSX.Element {
const [isOpen, handleOpen, handleClose] = useBooleanState();
return (
<>
<DataEntry.Edit onClick={handleOpen} />
{isOpen && (
<ResourceView
resource={treeDefinition}
mode="edit"
canAddAnother={false}
dialog="modal"
onClose={handleClose}
onSaved={(): void => window.location.reload()}
onDeleted={undefined}
isSubForm={false}
/>
)}
</>
);
}

const View = createBackboneView(RepairTree);

const userTool: UserTool = {
Expand Down
15 changes: 7 additions & 8 deletions specifyweb/frontend/js_src/lib/components/treelevelcombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ const fetchPossibleRanks = async (
).then(({ records }) =>
// Remove ranks after enforced rank
records
.slice(0, records.findIndex((model) => model.isEnforced) + 1)
.map((model) => ({
value: model.resource_uri,
title: model.title ?? model.name,
.slice(0, records.findIndex((resource) => resource.isEnforced) + 1)
.map((resource) => ({
value: resource.resource_uri,
title: resource.title ?? resource.name,
}))
);

Expand All @@ -47,7 +47,7 @@ export const fetchLowestChildRank = async (
.then(({ models }) =>
models.length === 0
? -1
: Math.min(...models.map((model) => model.get('rankId')))
: Math.min(...models.map((resource) => resource.get('rankId')))
);

export function TreeLevelComboBox(props: DefaultComboBoxProps): JSX.Element {
Expand All @@ -68,14 +68,13 @@ export function TreeLevelComboBox(props: DefaultComboBoxProps): JSX.Element {
// Parent is undefined for root tree node
.then(async (parent) =>
(parent as SpecifyResource<Geography> | undefined)?.rgetPromise(
'definitionItem',
true
'definitionItem'
)
)
.then((treeDefinitionItem) =>
typeof treeDefinitionItem === 'object'
? treeDefinitionItem
.rgetPromise('treeDef', true)
.rgetPromise('treeDef')
.then(async ({ id }) =>
lowestChildRank.then(async (rankId) =>
fetchPossibleRanks(
Expand Down
63 changes: 55 additions & 8 deletions specifyweb/frontend/js_src/lib/components/treeview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
SerializedResource,
} from '../datamodelutils';
import { caseInsensitiveHash, sortObjectsByKey, toggleItem } from '../helpers';
import type { SpecifyResource } from '../legacytypes';
import treeText from '../localization/tree';
import * as navigation from '../navigation';
import { hasTreeAccess } from '../permissions';
Expand All @@ -26,25 +27,28 @@ import {
} from '../treeviewutils';
import type { IR, RA } from '../types';
import { Autocomplete } from './autocomplete';
import { Button, Container, H2, Input } from './basic';
import { Button, Container, DataEntry, H2, Input } from './basic';
import { TableIcon } from './common';
import { useAsyncState, useId, useTitle } from './hooks';
import { useAsyncState, useBooleanState, useId, useTitle } from './hooks';
import { NotFound } from './notfoundview';
import { PermissionDenied } from './permissiondenied';
import createBackboneView from './reactbackboneextend';
import { deserializeResource } from './resource';
import { ResourceView } from './resourceview';
import { useCachedState } from './stateCache';
import { EditTreeDefinition } from './toolbar/treerepair';
import { TreeViewActions } from './treeviewactions';
import { TreeRow } from './treeviewrow';

const defaultCacheValue = [] as const;

function TreeView<SCHEMA extends AnyTree>({
tableName,
treeDefinitionId,
treeDefinition,
treeDefinitionItems,
}: {
readonly tableName: SCHEMA['tableName'];
readonly treeDefinitionId: number;
readonly treeDefinition: SpecifyResource<FilterTablesByEndsWith<'TreeDef'>>;
readonly treeDefinitionItems: RA<
SerializedResource<FilterTablesByEndsWith<'TreeDefItem'>>
>;
Expand Down Expand Up @@ -88,7 +92,9 @@ function TreeView<SCHEMA extends AnyTree>({

// Node sort order
const sortField = getPref(`${tableName as 'Geography'}.treeview_sort_field`);
const baseUrl = `/api/specify_tree/${tableName.toLowerCase()}/${treeDefinitionId}`;
const baseUrl = `/api/specify_tree/${tableName.toLowerCase()}/${
treeDefinition.id
}`;
const getRows = React.useCallback(
async (parentId: number | 'null') =>
fetchRows(`${baseUrl}/${parentId}/${sortField}`),
Expand Down Expand Up @@ -122,12 +128,17 @@ function TreeView<SCHEMA extends AnyTree>({
const toolbarButtonRef = React.useRef<HTMLButtonElement | null>(null);
const [searchValue, setSearchValue] = React.useState<string>('');

const [isEditingRanks, _, __, handleToggleEditingRanks] = useBooleanState();

return typeof rows === 'undefined' ? null : (
<Container.Full>
<header className="flex flex-wrap items-center gap-2">
<TableIcon name={table.name} />
<H2>{table.label}</H2>
{/* A react component that is also a TypeScript generic */}
<H2 title={treeDefinition.get('remarks') ?? undefined}>
{treeDefinition.get('name')}
</H2>
<EditTreeDefinition treeDefinition={treeDefinition} />
{/* A React component that is also a TypeScript generic */}
<Autocomplete<SerializedResource<SCHEMA>>
value={searchValue}
source={async (value) =>
Expand Down Expand Up @@ -194,6 +205,12 @@ function TreeView<SCHEMA extends AnyTree>({
)}
</Autocomplete>
<span className="flex-1 -ml-2" />
<Button.Simple
onClick={handleToggleEditingRanks}
aria-pressed={isEditingRanks}
>
{treeText('editRanks')}
</Button.Simple>
<TreeViewActions<SCHEMA>
tableName={tableName}
focusRef={toolbarButtonRef}
Expand Down Expand Up @@ -256,6 +273,10 @@ function TreeView<SCHEMA extends AnyTree>({
(name) => name[0]
)}
</Button.LikeLink>
{isEditingRanks &&
collapsedRanks?.includes(rank.rankId) !== true ? (
<EditTreeRank rank={rank} />
) : undefined}
</div>
))}
</div>
Expand Down Expand Up @@ -316,6 +337,32 @@ function TreeView<SCHEMA extends AnyTree>({
);
}

function EditTreeRank({
rank,
}: {
readonly rank: SerializedResource<FilterTablesByEndsWith<'TreeDefItem'>>;
}): JSX.Element {
const [isOpen, handleOpen, handleClose] = useBooleanState();
const resource = React.useMemo(() => deserializeResource(rank), [rank]);
return (
<>
<DataEntry.Edit onClick={handleOpen} />
{isOpen && (
<ResourceView
resource={resource}
mode="edit"
canAddAnother={false}
dialog="modal"
onClose={handleClose}
onSaved={(): void => window.location.reload()}
onDeleted={undefined}
isSubForm={false}
/>
)}
</>
);
}

function TreeViewWrapper({
table,
}: {
Expand All @@ -341,7 +388,7 @@ function TreeViewWrapper({
return typeof treeDefinition === 'object' ? (
<TreeView
tableName={tableName}
treeDefinitionId={treeDefinition.definition.id}
treeDefinition={treeDefinition.definition}
treeDefinitionItems={treeDefinition.ranks}
/>
) : null;
Expand Down
7 changes: 7 additions & 0 deletions specifyweb/frontend/js_src/lib/localization/tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ const treeText = createDictionary({
ca: "Recompte d'objectes de col·lecció indirecta",
'es-es': 'Indirect Collection Object Count',
},
// FIXME: localize
editRanks: {
'en-us': 'Edit Ranks',
'ru-ru': 'Edit Ranks',
ca: 'Edit Ranks',
'es-es': 'Edit Ranks',
},
});

export default treeText;
Loading

0 comments on commit 07b58f9

Please sign in to comment.