diff --git a/.agents/skills/code-review/SKILL.md b/.agents/skills/code-review/SKILL.md
index ded514e..b91c582 100644
--- a/.agents/skills/code-review/SKILL.md
+++ b/.agents/skills/code-review/SKILL.md
@@ -13,20 +13,20 @@ JSON should have strictly the following form:
```json
[
- {
- "name": "typos",
- "file": "path/from/the/root/of/the/repo/file.py",
- "line_from": 10,
- "line_to": 20,
- "description": "Word 'asembly' is written with a typo, you likely meant 'assembly'"
- },
- {
- "name": "incorrect-comments",
- "file": "path/from/the/root/of/the/repo/another.py",
- "line_from": 15,
- "line_to": 21,
- "description": "The comment says that this algorithm works in O(N) but most common path according to numpy documentation is O(N^2)"
- }
+ {
+ "name": "typos",
+ "file": "path/from/the/root/of/the/repo/file.py",
+ "line_from": 10,
+ "line_to": 20,
+ "description": "Word 'asembly' is written with a typo, you likely meant 'assembly'"
+ },
+ {
+ "name": "incorrect-comments",
+ "file": "path/from/the/root/of/the/repo/another.py",
+ "line_from": 15,
+ "line_to": 21,
+ "description": "The comment says that this algorithm works in O(N) but most common path according to numpy documentation is O(N^2)"
+ }
]
```
@@ -34,6 +34,11 @@ You MUST adhere to this form because it will be later used by automation to crea
If the rule asks for a citation, put it in the `description` field.
+To fetch the full diff use
+```shell
+git fetch origin && git diff origin/master...HEAD
+```
+
Below are rules you should check when reviewing the code.
### duplicate-code
@@ -46,7 +51,7 @@ The comment used for a function, variable, or expression contradicts the content
### misleading-name
-The name of a function, method, variable, parameter, or class contradicts what it actually does. Examples of contradictions: a get_* or fetch_* function that mutates state, an is__/has__ name that does not return a boolean, a singular name bound to a collection, a boolean flag whose name implies the opposite polarity of the behavior it controls, or a verb that names a different operation than the one performed. Only flag when the mismatch is visible in the body of the token in the diff or in the code the diff calls. Do not flag names that are merely vague, short, or abbreviated.
+The name of a function, method, variable, parameter, or class contradicts what it actually does. Examples of contradictions: a get_* or fetch_* function that mutates state, an is_*/has_* name that does not return a boolean, a singular name bound to a collection, a boolean flag whose name implies the opposite polarity of the behavior it controls, or a verb that names a different operation than the one performed. Only flag when the mismatch is visible in the body of the token in the diff or in the code the diff calls. Do not flag names that are merely vague, short, or abbreviated.
### typos
@@ -123,3 +128,7 @@ subprocess/os.system/os.popen runs a shell with concatenated or formatted user/e
### pointless-wrapper
A new or changed function or method only forwards to another callable with the same arguments and return value, adding no conversion, validation, defaulting, error handling, or other logic. Only flag when call sites could invoke the inner callable directly, the wrapper does not implement an interface, protocol, or abstract method, and it is not a public re-export of a private or third-party symbol.
+
+### redundant-parameter
+
+All callers of a new or changed function, method or class pass the same value for the parameter such that a parameter can be deleted without affecting any behaviour of all callers.
diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx
index 54dfb92..95518f1 100644
--- a/apps/admin/src/App.tsx
+++ b/apps/admin/src/App.tsx
@@ -11,6 +11,7 @@ import {
MdAdminPanelSettings,
MdAccountTree,
MdCode,
+ MdLibraryBooks,
MdOpenInNew,
MdPublic,
MdTableChart,
@@ -22,6 +23,8 @@ import {
DataCatalogPage,
LoginPage,
RecordCrossmatchDetailsPage,
+ ReferenceDetailsPage,
+ ReferencesPage,
SqlQueryPage,
TableDetailsPage,
TablesPage,
@@ -110,6 +113,9 @@ function Layout() {
+
+
+
@@ -137,6 +143,11 @@ function App() {
} />
} />
} />
+ } />
+ }
+ />
} />
} />
} />
diff --git a/apps/admin/src/components/ui/EditableField.tsx b/apps/admin/src/components/ui/EditableField.tsx
new file mode 100644
index 0000000..00447a4
--- /dev/null
+++ b/apps/admin/src/components/ui/EditableField.tsx
@@ -0,0 +1,208 @@
+import classNames from "classnames";
+import { ReactElement, useEffect, useState } from "react";
+import { MdEdit } from "react-icons/md";
+import { formatCaughtError } from "@leda/lib/tap";
+import { FieldInput, type FieldInputConfig } from "./FieldInput";
+
+export interface EditableFieldProps {
+ value: T;
+ formatValue: (value: T) => string;
+ parseDraft: (draft: string) => T;
+ input: FieldInputConfig;
+ onSave: (value: T) => void | Promise;
+ editLabel: string;
+ saving?: boolean;
+ displayClassName?: string;
+ inputClassName?: string;
+ align?: "center" | "start";
+ emptyDisplayValue?: string;
+ renderDisplay?: (value: T) => ReactElement;
+ trimOnCommit?: boolean;
+ revertOnError?: boolean;
+ isEmpty?: (value: T) => boolean;
+ isUnchanged?: (draft: string, value: T) => boolean;
+}
+
+export interface StringEditableFieldProps extends Omit<
+ EditableFieldProps,
+ "formatValue" | "parseDraft" | "input"
+> {
+ input?: FieldInputConfig;
+}
+
+function EditableFieldInner({
+ value,
+ formatValue,
+ parseDraft,
+ input,
+ onSave,
+ editLabel,
+ saving = false,
+ displayClassName,
+ inputClassName,
+ align = "center",
+ emptyDisplayValue,
+ renderDisplay,
+ trimOnCommit = true,
+ revertOnError = true,
+ isEmpty,
+ isUnchanged,
+}: EditableFieldProps): ReactElement {
+ const [editing, setEditing] = useState(false);
+ const [draft, setDraft] = useState(() => formatValue(value));
+ const [error, setError] = useState(null);
+
+ function valueIsEmpty(nextValue: T): boolean {
+ if (isEmpty) {
+ return isEmpty(nextValue);
+ }
+ return !formatValue(nextValue);
+ }
+
+ function toDisplay(nextValue: T): string {
+ if (valueIsEmpty(nextValue) && emptyDisplayValue) {
+ return emptyDisplayValue;
+ }
+ return formatValue(nextValue);
+ }
+
+ useEffect(() => {
+ if (!editing) {
+ setDraft(formatValue(value));
+ setError(null);
+ }
+ }, [value, editing, formatValue]);
+
+ function startEdit(): void {
+ setDraft(formatValue(value));
+ setError(null);
+ setEditing(true);
+ }
+
+ function cancelEdit(): void {
+ setDraft(formatValue(value));
+ setError(null);
+ setEditing(false);
+ }
+
+ async function handleSave(): Promise {
+ const nextDraft = trimOnCommit ? draft.trim() : draft;
+ const changed = isUnchanged
+ ? !isUnchanged(nextDraft, value)
+ : nextDraft !== formatValue(value);
+
+ if (!changed) {
+ setEditing(false);
+ setError(null);
+ return;
+ }
+
+ try {
+ await onSave(parseDraft(nextDraft));
+ setEditing(false);
+ setError(null);
+ } catch (err) {
+ setError(formatCaughtError(err));
+ if (revertOnError) {
+ setDraft(formatValue(value));
+ }
+ }
+ }
+
+ function defaultRender(nextValue: T): ReactElement {
+ return (
+
+ {toDisplay(nextValue)}
+
+ );
+ }
+
+ function renderValue(nextValue: T): ReactElement {
+ if (valueIsEmpty(nextValue) && emptyDisplayValue) {
+ return defaultRender(nextValue);
+ }
+ if (renderDisplay) {
+ return renderDisplay(nextValue);
+ }
+ return defaultRender(nextValue);
+ }
+
+ if (editing) {
+ return (
+