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

Commit a04e517

Browse files
authored
feat(gui): buttons to copy minimal API & usage data for an element (#809)
* feat(gui): include autogen as author when reporting wrong annotations * feat(gui): don't allow reporting changed annotations * feat(gui): tooltips for report buttons * feat(gui): disable reporting missing annotations on complete elements * feat(gui): disable reporting wrong annotations that are marked as correct * feat(gui): when reporting wrong/missing annotations attach minimal usage store * feat(gui): create minimal API data * fix(gui): don't automatically transfer usage data to GitHub (too long URL in a few cases) * feat(gui): improve formatting of copied text * style: apply automatic fixes of linters * fix(gui): minimal API data for functions * fix(gui): circular import * fix(gui): wrong tooltip * style: apply automatic fixes of linters * feat(gui): include target in summary * fix(gui): circular import * style: apply automatic fixes of linters Co-authored-by: lars-reimann <[email protected]>
1 parent 03af34c commit a04e517

23 files changed

+505
-122
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,15 @@ export const truncate = function (text: string, maxLength: number): string {
55
export const pluralize = function (count: number, noun: string): string {
66
return `${count} ${noun}${count === 1 ? '' : 's'}`;
77
};
8+
9+
export const jsonCode = function (json: string): string {
10+
return `\`\`\`json5\n${json}\n\`\`\``;
11+
};
12+
13+
export const details = function (text: string, summary: string): string {
14+
return `<details>
15+
<summary>${summary}</summary>
16+
17+
${text}
18+
</details>`;
19+
};

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

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Button, ButtonGroup, Icon, IconButton, Stack, Text as ChakraText } from '@chakra-ui/react';
1+
import { Button, ButtonGroup, Icon, IconButton, Stack, Text as ChakraText, Tooltip } from '@chakra-ui/react';
22
import React from 'react';
33
import { FaCheck, FaFlag, FaTrash, FaWrench } from 'react-icons/fa';
44
import { useAppDispatch, useAppSelector } from '../../app/hooks';
@@ -330,7 +330,8 @@ const AnnotationTag: React.FC<AnnotationTagProps> = function ({
330330
}) {
331331
const isValidUsername = useAppSelector(selectUsernameIsValid);
332332
const isCorrect = (annotation.reviewers?.length ?? 0) > 0;
333-
const isReportable = reportable && (annotation.authors ?? []).includes('$autogen$');
333+
const authors = annotation.authors ?? [];
334+
const isReportable = reportable && authors.length === 1 && authors.includes('$autogen$');
334335

335336
return (
336337
<ButtonGroup size="sm" variant="outline" isAttached>
@@ -373,14 +374,17 @@ const AnnotationTag: React.FC<AnnotationTagProps> = function ({
373374
</Button>
374375
)}
375376
{isReportable && (
376-
<IconButton
377-
icon={<FaFlag />}
378-
aria-label="Report Wrong Annotation"
379-
colorScheme="orange"
380-
onClick={() => {
381-
window.open(wrongAnnotationURL(type, annotation), '_blank');
382-
}}
383-
/>
377+
<Tooltip label="Report a wrong autogenerated annotation.">
378+
<IconButton
379+
icon={<FaFlag />}
380+
aria-label="Report Wrong Annotation"
381+
colorScheme="orange"
382+
disabled={isCorrect || !isValidUsername}
383+
onClick={() => {
384+
window.open(wrongAnnotationURL(type, annotation), '_blank');
385+
}}
386+
/>
387+
</Tooltip>
384388
)}
385389
</ButtonGroup>
386390
);
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { Button, ButtonGroup, Tooltip, useClipboard } from '@chakra-ui/react';
2+
import React from 'react';
3+
import { FaClipboard } from 'react-icons/fa';
4+
import { useAppSelector } from '../../app/hooks';
5+
import { selectRawPythonPackage } from '../packageData/apiSlice';
6+
import { selectUsages } from '../usages/usageSlice';
7+
import { buildMinimalAPIJson } from '../packageData/minimalAPIBuilder';
8+
import { details, jsonCode } from '../../common/util/stringOperations';
9+
import { buildMinimalUsagesStoreJson } from '../usages/minimalUsageStoreBuilder';
10+
11+
interface MinimalDataButtonsProps {
12+
target: string;
13+
}
14+
15+
export const MinimalDataCopyButtons: React.FC<MinimalDataButtonsProps> = function ({ target }) {
16+
const pythonPackage = useAppSelector(selectRawPythonPackage);
17+
const declaration = pythonPackage.getDeclarationById(target);
18+
const usages = useAppSelector(selectUsages);
19+
const { onCopy: onCopyAPI } = useClipboard(
20+
details(jsonCode(buildMinimalAPIJson(declaration)), `Minimal API Data for \`${target}\``),
21+
);
22+
const { onCopy: onCopyUsages } = useClipboard(
23+
details(jsonCode(buildMinimalUsagesStoreJson(usages, declaration)), `Minimal Usage Store for \`${target}\``),
24+
);
25+
26+
return (
27+
<ButtonGroup size="sm" variant="outline" isAttached>
28+
<Tooltip label="Copy the minimal API data to the clipboard. Paste this into the corresponding field in the issue form.">
29+
<Button
30+
leftIcon={<FaClipboard />}
31+
onClick={() => {
32+
onCopyAPI();
33+
}}
34+
>
35+
API
36+
</Button>
37+
</Tooltip>
38+
<Tooltip label="Copy the minimal usage store to the clipboard. Paste this into the corresponding field in the issue form.">
39+
<Button
40+
leftIcon={<FaClipboard />}
41+
onClick={() => {
42+
onCopyUsages();
43+
}}
44+
>
45+
Usages
46+
</Button>
47+
</Tooltip>
48+
</ButtonGroup>
49+
);
50+
};
Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
1-
import { IconButton } from '@chakra-ui/react';
1+
import { IconButton, Tooltip } from '@chakra-ui/react';
22
import React from 'react';
33
import { FaFlag } from 'react-icons/fa';
44
import { missingAnnotationURL } from '../externalLinks/urlBuilder';
5+
import { useAppSelector } from '../../app/hooks';
6+
import { selectComplete, selectUsernameIsValid } from './annotationSlice';
57

68
interface MissingAnnotationButtonProps {
79
target: string;
810
}
911

1012
export const MissingAnnotationButton: React.FC<MissingAnnotationButtonProps> = function ({ target }) {
13+
const isComplete = Boolean(useAppSelector(selectComplete(target)));
14+
const isValidUsername = Boolean(useAppSelector(selectUsernameIsValid));
15+
const isDisabled = isComplete || !isValidUsername;
16+
1117
return (
12-
<IconButton
13-
icon={<FaFlag />}
14-
aria-label="Report Missing Annotation"
15-
size="sm"
16-
variant="outline"
17-
colorScheme="orange"
18-
onClick={() => {
19-
window.open(missingAnnotationURL(target), '_blank');
20-
}}
21-
/>
18+
<Tooltip label="Report a missing autogenerated annotation.">
19+
<IconButton
20+
icon={<FaFlag />}
21+
aria-label="Report Missing Annotation"
22+
size="sm"
23+
variant="outline"
24+
colorScheme="orange"
25+
disabled={isDisabled}
26+
onClick={() => {
27+
window.open(missingAnnotationURL(target), '_blank');
28+
}}
29+
/>
30+
</Tooltip>
2231
);
2332
};

api-editor/gui/src/features/externalLinks/urlBuilder.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Annotation } from '../annotations/annotationSlice';
2+
import { jsonCode } from '../../common/util/stringOperations';
23

34
const baseURL = 'https://github.com/lars-reimann/api-editor';
45

@@ -19,24 +20,29 @@ const baseMissingAnnotationURL = `${issueBaseURL}?assignees=&labels=bug%2Cmissin
1920

2021
export const missingAnnotationURL = function (target: string): string {
2122
const urlHash = encodeURIComponent(`\`#/${target}\``);
22-
return `${baseMissingAnnotationURL}&url-hash=${urlHash}`;
23+
24+
return baseMissingAnnotationURL + `&url-hash=${urlHash}`;
2325
};
2426

2527
const baseWrongAnnotationURL = `${issueBaseURL}?assignees=&template=wrong_annotation.yml&labels=bug%2Cwrong+annotation%2C`;
2628

2729
export const wrongAnnotationURL = function (annotationType: string, annotation: Annotation): string {
28-
const minimalAnnotation = { ...annotation };
29-
30-
// noinspection JSConstantReassignment
31-
delete minimalAnnotation.authors;
30+
const minimalAnnotation = {
31+
...annotation,
32+
authors: ['$autogen$'],
33+
};
3234
// noinspection JSConstantReassignment
3335
delete minimalAnnotation.reviewers;
3436

3537
const label = encodeURIComponent(`@${annotationType}`);
3638
const urlHash = encodeURIComponent(`\`#/${annotation.target}\``);
3739
const actualAnnotationType = encodeURIComponent(`\`@${annotationType}\``);
38-
const actualAnnotationInputs = encodeURIComponent(
39-
`\`\`\`json5\n${JSON.stringify(minimalAnnotation, null, 4)}\n\`\`\``,
40+
const actualAnnotationInputs = encodeURIComponent(jsonCode(JSON.stringify(minimalAnnotation, null, 4)));
41+
42+
return (
43+
`${baseWrongAnnotationURL}${label}` +
44+
`&url-hash=${urlHash}` +
45+
`&actual-annotation-type=${actualAnnotationType}` +
46+
`&actual-annotation-inputs=${actualAnnotationInputs}`
4047
);
41-
return `${baseWrongAnnotationURL}${label}&url-hash=${urlHash}&actual-annotation-type=${actualAnnotationType}&actual-annotation-inputs=${actualAnnotationInputs}`;
4248
};

api-editor/gui/src/features/packageData/PackageDataImportDialog.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ import { useAppDispatch } from '../../app/hooks';
2020
import { StyledDropzone } from '../../common/StyledDropzone';
2121
import { isValidJsonFile } from '../../common/util/validation';
2222
import { resetAnnotationStore } from '../annotations/annotationSlice';
23-
import { parsePythonPackageJson, PythonPackageJson } from './model/PythonPackageBuilder';
23+
import { parsePythonPackageJson } from './model/PythonPackageBuilder';
2424
import { resetUIAfterAPIImport, toggleAPIImportDialog } from '../ui/uiSlice';
2525
import { persistPythonPackage, setPythonPackage } from './apiSlice';
2626
import { resetUsages } from '../usages/usageSlice';
27+
import { PythonPackageJson } from './model/APIJsonData';
2728

2829
export const PackageDataImportDialog: React.FC = function () {
2930
const toast = useToast();

api-editor/gui/src/features/packageData/apiSlice.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { createAsyncThunk, createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit';
22
import { RootState } from '../../app/store';
33
import { PythonPackage } from './model/PythonPackage';
4-
import { parsePythonPackageJson, PythonPackageJson } from './model/PythonPackageBuilder';
4+
import { parsePythonPackageJson } from './model/PythonPackageBuilder';
55
import * as idb from 'idb-keyval';
66
import { selectFilter, selectSorter } from '../ui/uiSlice';
77
import { selectUsages } from '../usages/usageSlice';
88
import { selectAnnotationStore } from '../annotations/annotationSlice';
99
import { PythonDeclaration } from './model/PythonDeclaration';
10-
11-
export const EXPECTED_API_SCHEMA_VERSION = 1;
10+
import { EXPECTED_API_SCHEMA_VERSION, PythonPackageJson } from './model/APIJsonData';
1211

1312
export interface APIState {
1413
pythonPackage: PythonPackage;
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { Optional } from '../../common/util/types';
2+
import { PythonDeclaration } from './model/PythonDeclaration';
3+
import { PythonClass } from './model/PythonClass';
4+
import { PythonParameter } from './model/PythonParameter';
5+
import { PythonModule } from './model/PythonModule';
6+
import { PythonFunction } from './model/PythonFunction';
7+
import { PythonPackage } from './model/PythonPackage';
8+
9+
export const buildMinimalAPIJson = function (declaration: Optional<PythonDeclaration>): string {
10+
let result: PythonPackage;
11+
12+
if (declaration instanceof PythonModule) {
13+
result = buildMinimalModule(declaration);
14+
} else if (declaration instanceof PythonClass) {
15+
result = buildMinimalClass(declaration);
16+
} else if (declaration instanceof PythonFunction) {
17+
result = buildMinimalFunction(declaration);
18+
} else if (declaration instanceof PythonParameter) {
19+
result = buildMinimalParameter(declaration);
20+
} else {
21+
result = new PythonPackage('test', 'test', '0.0.1');
22+
}
23+
24+
return JSON.stringify(result.toJson(), null, 4);
25+
};
26+
27+
const buildMinimalPackage = function (declaration?: PythonPackage, modules: PythonModule[] = []): PythonPackage {
28+
if (declaration) {
29+
return declaration.shallowCopy({ modules });
30+
} else {
31+
return new PythonPackage('test', 'test', '0.0.1', modules);
32+
}
33+
};
34+
35+
const buildMinimalModule = function (
36+
declaration?: PythonModule,
37+
classes: PythonClass[] = [],
38+
functions: PythonFunction[] = [],
39+
): PythonPackage {
40+
let module: PythonModule;
41+
if (declaration) {
42+
module = declaration.shallowCopy({ classes, functions });
43+
} else {
44+
module = new PythonModule('test/test_module', 'test_module', [], [], classes, functions);
45+
}
46+
return buildMinimalPackage(module?.parent() ?? undefined, [module]);
47+
};
48+
49+
const buildMinimalClass = function (declaration?: PythonClass, methods: PythonFunction[] = []): PythonPackage {
50+
let clazz: PythonClass;
51+
if (declaration) {
52+
clazz = declaration.shallowCopy({ methods });
53+
} else {
54+
clazz = new PythonClass('test/test_module/TestClass', 'TestClass', 'test_module.TestClass', [], [], methods);
55+
}
56+
return buildMinimalModule(clazz?.parent() ?? undefined, [clazz]);
57+
};
58+
59+
const buildMinimalFunction = function (
60+
declaration?: PythonFunction,
61+
parameters: PythonParameter[] = [],
62+
): PythonPackage {
63+
if (declaration) {
64+
const fun = declaration.shallowCopy({ parameters, results: [] });
65+
const parent = fun.parent();
66+
if (parent instanceof PythonClass) {
67+
return buildMinimalClass(parent, [fun]);
68+
} else {
69+
return buildMinimalModule(parent ?? undefined, [], [fun]);
70+
}
71+
} else {
72+
const fun = new PythonFunction(
73+
'test/test_module/test_function',
74+
'test_function',
75+
'test_module.test_function',
76+
[],
77+
parameters,
78+
);
79+
return buildMinimalModule(undefined, [], [fun]);
80+
}
81+
};
82+
83+
const buildMinimalParameter = function (declaration?: PythonParameter): PythonPackage {
84+
let parameter: PythonParameter;
85+
if (declaration) {
86+
parameter = declaration.clone();
87+
} else {
88+
parameter = new PythonParameter(
89+
'test/test_module/test_function/test_parameter',
90+
'test_parameter',
91+
'test_module.test_function.test_parameter',
92+
);
93+
}
94+
return buildMinimalFunction(declaration?.parent() ?? undefined, [parameter]);
95+
};

0 commit comments

Comments
 (0)