Skip to content
This repository was archived by the owner on Jan 19, 2025. It is now read-only.

Commit 40ee293

Browse files
authored
feat(gui): authors and reviewers of of annotations (#684)
* refactor: add super-interface for annotations * fix: build error * feat(package-parser): add authors and reviewer for auto-generated annotations * chore(data): regenerate annotations * feat(gui): add username to UI slice * feat(gui): add input field for username * feat(gui): disable adding/editing of annotations when username is invalid * refactor(gui): move username to annotation slice * fix(gui): build error * feat(gui): button to mark annotation as correct * feat(gui): change color scheme of correct/complete buttons to reduce noise * style: apply automatic fixes of linters * feat(gui): store author after editing/changing an annotation * fix(gui): store multiple authors * feat(gui): dedupe authors and move latest author to end of list * feat(gui): toggle button between "Correct" and "Mark as Correct" * feat(gui): store reviewers * feat(gui): properly toggle between correct and not reviewed * feat(gui): prevent deleting/editing correct annotations Co-authored-by: lars-reimann <[email protected]>
1 parent 940d2f6 commit 40ee293

File tree

23 files changed

+12663
-3402
lines changed

23 files changed

+12663
-3402
lines changed

api-editor/gui/src/app/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ import { initializePythonPackage, selectRawPythonPackage } from '../features/pac
5656
import { PythonClass } from '../features/packageData/model/PythonClass';
5757
import { PythonParameter } from '../features/packageData/model/PythonParameter';
5858
import { ConstantBatchForm } from '../features/annotations/batchforms/ConstantBatchForm';
59-
import { ActionBar } from '../features/packageData/selectionView/ActionBar';
59+
import { ActionBar } from '../features/actionBar/ActionBar';
6060
import { useLocation } from 'react-router-dom';
6161
import { RenameBatchForm } from '../features/annotations/batchforms/RenameBatchForm';
6262
import { RequiredBatchForm } from '../features/annotations/batchforms/RequiredBatchForm';

api-editor/gui/src/common/util/validation.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ export const isValidPythonIdentifier = function (value: string): boolean {
22
return /^[A-Za-z_][A-Za-z_0-9]*$/u.test(value);
33
};
44

5+
export const isValidUsername = function (value: string): boolean {
6+
return /^[A-Za-z0-9_\-]+$/u.test(value);
7+
};
8+
59
export const isValidJsonFile = function (value: string): boolean {
610
const validJsonIdentifier = /^.*\.(json)$/u;
711
return validJsonIdentifier.test(value);
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { Button, HStack, Spacer } from '@chakra-ui/react';
2+
import React from 'react';
3+
import { UsernameInput } from './UsernameInput';
4+
import { useAppDispatch, useAppSelector } from '../../app/hooks';
5+
import { PythonPackage } from '../packageData/model/PythonPackage';
6+
import { selectFilteredPythonPackage, selectFlatSortedDeclarationList } from '../packageData/apiSlice';
7+
import { Optional } from '../../common/util/types';
8+
import { AbstractPythonFilter } from '../packageData/model/filters/AbstractPythonFilter';
9+
import { AnnotationStore, redo, selectAnnotationStore, undo } from '../annotations/annotationSlice';
10+
import { selectFilter, setAllCollapsedInTreeView, setAllExpandedInTreeView } from '../ui/uiSlice';
11+
import { PythonDeclaration } from '../packageData/model/PythonDeclaration';
12+
import { selectUsages } from '../usages/usageSlice';
13+
import { UsageCountStore } from '../usages/model/UsageCountStore';
14+
import { useNavigate } from 'react-router';
15+
16+
interface ActionBarProps {
17+
declaration: Optional<PythonDeclaration>;
18+
}
19+
20+
export const ActionBar: React.FC<ActionBarProps> = function ({ declaration }) {
21+
const dispatch = useAppDispatch();
22+
const navigate = useNavigate();
23+
24+
const allDeclarations = useAppSelector(selectFlatSortedDeclarationList);
25+
const pythonPackage = useAppSelector(selectFilteredPythonPackage);
26+
const pythonFilter = useAppSelector(selectFilter);
27+
const annotations = useAppSelector(selectAnnotationStore);
28+
const usages = useAppSelector(selectUsages);
29+
30+
return (
31+
<HStack borderTop={1} layerStyle="subtleBorder" padding="0.5em 1em" marginTop={0} w="100%">
32+
<HStack>
33+
<Button
34+
accessKey="p"
35+
disabled={!declaration}
36+
onClick={() => {
37+
let navStr = getPreviousElementPath(
38+
allDeclarations,
39+
declaration!,
40+
pythonFilter,
41+
annotations,
42+
usages,
43+
);
44+
if (navStr !== null) {
45+
//navigate to element
46+
navigate(`/${navStr}`);
47+
48+
//update tree selection
49+
const parents = getAncestors(navStr, pythonPackage);
50+
dispatch(setAllExpandedInTreeView(parents));
51+
}
52+
}}
53+
>
54+
Previous
55+
</Button>
56+
<Button
57+
accessKey="n"
58+
disabled={!declaration}
59+
onClick={() => {
60+
let navStr = getNextElementPath(
61+
allDeclarations,
62+
declaration!,
63+
pythonFilter,
64+
annotations,
65+
usages,
66+
);
67+
if (navStr !== null) {
68+
//navigate to element
69+
navigate(`/${navStr}`);
70+
71+
//update tree selection
72+
const parents = getAncestors(navStr, pythonPackage);
73+
dispatch(setAllExpandedInTreeView(parents));
74+
}
75+
}}
76+
>
77+
Next
78+
</Button>
79+
80+
<Button
81+
accessKey="u"
82+
onClick={() => {
83+
const parent = declaration?.parent();
84+
if (parent && !(parent instanceof PythonPackage)) {
85+
navigate(`/${parent.id}`);
86+
}
87+
}}
88+
>
89+
Go to Parent
90+
</Button>
91+
92+
<Button
93+
onClick={() => {
94+
dispatch(setAllExpandedInTreeView(getDescendantsOrSelf(pythonPackage)));
95+
}}
96+
>
97+
Expand All
98+
</Button>
99+
<Button
100+
onClick={() => {
101+
dispatch(setAllCollapsedInTreeView(getDescendantsOrSelf(pythonPackage)));
102+
}}
103+
>
104+
Collapse All
105+
</Button>
106+
107+
<Button
108+
disabled={!declaration}
109+
onClick={() => {
110+
dispatch(setAllExpandedInTreeView(getDescendantsOrSelf(declaration!)));
111+
}}
112+
>
113+
Expand Selected
114+
</Button>
115+
<Button
116+
disabled={!declaration}
117+
onClick={() => {
118+
dispatch(setAllCollapsedInTreeView(getDescendantsOrSelf(declaration!)));
119+
}}
120+
>
121+
Collapse Selected
122+
</Button>
123+
<Button
124+
accessKey="z"
125+
disabled={!declaration}
126+
onClick={() => {
127+
dispatch(undo());
128+
}}
129+
>
130+
Undo
131+
</Button>
132+
<Button
133+
accessKey="y"
134+
disabled={!declaration}
135+
onClick={() => {
136+
dispatch(redo());
137+
}}
138+
>
139+
Redo
140+
</Button>
141+
</HStack>
142+
143+
<Spacer />
144+
145+
<HStack>
146+
<UsernameInput />
147+
</HStack>
148+
</HStack>
149+
);
150+
};
151+
152+
const getNextElementPath = function (
153+
declarations: PythonDeclaration[],
154+
start: PythonDeclaration,
155+
filter: AbstractPythonFilter,
156+
annotations: AnnotationStore,
157+
usages: UsageCountStore,
158+
): string {
159+
let current = getNextElementInTree(declarations, start);
160+
while (current !== start) {
161+
if (filter.shouldKeepDeclaration(current, annotations, usages)) {
162+
return current.id;
163+
}
164+
current = getNextElementInTree(declarations, current);
165+
}
166+
return start.id;
167+
};
168+
169+
const getNextElementInTree = function (
170+
declarations: PythonDeclaration[],
171+
current: PythonDeclaration,
172+
): PythonDeclaration {
173+
if (declarations.length === 0) {
174+
return current;
175+
}
176+
177+
const index = declarations.findIndex((it) => it.id === current.id);
178+
const nextIndex = (index + 1) % declarations.length;
179+
return declarations[nextIndex];
180+
};
181+
182+
const getPreviousElementPath = function (
183+
declarations: PythonDeclaration[],
184+
start: PythonDeclaration,
185+
filter: AbstractPythonFilter,
186+
annotations: AnnotationStore,
187+
usages: UsageCountStore,
188+
): string | null {
189+
let current = getPreviousElementInTree(declarations, start);
190+
while (current !== start && current !== null) {
191+
if (filter.shouldKeepDeclaration(current, annotations, usages)) {
192+
return current.id;
193+
}
194+
current = getPreviousElementInTree(declarations, current);
195+
}
196+
return null;
197+
};
198+
199+
const getPreviousElementInTree = function (
200+
declarations: PythonDeclaration[],
201+
current: PythonDeclaration,
202+
): PythonDeclaration {
203+
if (declarations.length === 0) {
204+
return current;
205+
}
206+
207+
const index = declarations.findIndex((it) => it.id === current.id);
208+
const previousIndex = (index - 1 + declarations.length) % declarations.length;
209+
return declarations[previousIndex];
210+
};
211+
212+
const getAncestors = function (navStr: string, filteredPythonPackage: PythonPackage): string[] {
213+
const ancestors: string[] = [];
214+
215+
let currentElement = filteredPythonPackage.getDeclarationById(navStr);
216+
if (currentElement) {
217+
currentElement = currentElement.parent();
218+
while (currentElement) {
219+
ancestors.push(currentElement.id);
220+
currentElement = currentElement.parent();
221+
}
222+
}
223+
224+
return ancestors;
225+
};
226+
227+
const getDescendantsOrSelf = function (current: PythonDeclaration): string[] {
228+
return [...current.descendantsOrSelf()].map((descendant) => descendant.id);
229+
};
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import {
2+
Box,
3+
FormControl,
4+
Input,
5+
Popover,
6+
PopoverArrow,
7+
PopoverBody,
8+
PopoverContent,
9+
PopoverHeader,
10+
PopoverTrigger,
11+
} from '@chakra-ui/react';
12+
import React from 'react';
13+
import { useAppDispatch, useAppSelector } from '../../app/hooks';
14+
import { isValidUsername } from '../../common/util/validation';
15+
import { selectUsername, setUsername } from '../annotations/annotationSlice';
16+
17+
export const UsernameInput: React.FC = function () {
18+
const dispatch = useAppDispatch();
19+
20+
const username = useAppSelector(selectUsername);
21+
const usernameIsValid = isValidUsername(username);
22+
23+
return (
24+
<Box zIndex={50}>
25+
<Popover
26+
returnFocusOnClose={false}
27+
isOpen={!usernameIsValid}
28+
placement="bottom"
29+
closeOnBlur={false}
30+
autoFocus={false}
31+
>
32+
<PopoverTrigger>
33+
<FormControl isInvalid={!usernameIsValid}>
34+
<Input
35+
type="text"
36+
placeholder="Username..."
37+
value={username}
38+
onChange={(event) => dispatch(setUsername(event.target.value))}
39+
spellCheck={false}
40+
minWidth="400px"
41+
/>
42+
</FormControl>
43+
</PopoverTrigger>
44+
{!usernameIsValid && (
45+
<PopoverContent minWidth={462} fontSize="sm" marginRight={2}>
46+
<PopoverArrow />
47+
<PopoverHeader>Invalid username</PopoverHeader>
48+
<PopoverBody>
49+
In order to annotate you need to provide a valid username. A valid username must not be
50+
empty and contain only alphanumeric characters, underscores, and dashes.
51+
</PopoverBody>
52+
</PopoverContent>
53+
)}
54+
</Popover>
55+
</Box>
56+
);
57+
};

api-editor/gui/src/features/annotations/AnnotationDropdown.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { Box, Button, Icon, Menu, MenuButton, MenuItem, MenuList } from '@chakra
22
import React from 'react';
33
import { FaChevronDown } from 'react-icons/fa';
44
import { useAppDispatch, useAppSelector } from '../../app/hooks';
5-
import { addPure, addRemove, addRequired, selectComplete } from './annotationSlice';
5+
import { addPure, addRemove, addRequired, selectComplete, selectUsernameIsValid } from './annotationSlice';
66
import {
77
showAttributeAnnotationForm,
88
showBoundaryAnnotationForm,
@@ -54,6 +54,8 @@ export const AnnotationDropdown: React.FC<AnnotationDropdownProps> = function ({
5454
}) {
5555
const dispatch = useAppDispatch();
5656
const isComplete = Boolean(useAppSelector(selectComplete(target)));
57+
const isValidUsername = Boolean(useAppSelector(selectUsernameIsValid));
58+
const isDisabled = isComplete || !isValidUsername;
5759

5860
return (
5961
// Box gets rid of popper.js warning "CSS margin styles cannot be used"
@@ -64,7 +66,7 @@ export const AnnotationDropdown: React.FC<AnnotationDropdownProps> = function ({
6466
size="sm"
6567
variant="outline"
6668
rightIcon={<Icon as={FaChevronDown} />}
67-
disabled={isComplete}
69+
disabled={isDisabled}
6870
>
6971
Annotations
7072
</MenuButton>

0 commit comments

Comments
 (0)