Skip to content
Merged
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
39 changes: 24 additions & 15 deletions .agents/skills/code-review/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,32 @@ 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)"
}
]
```

You MUST adhere to this form because it will be later used by automation to create a user-friendly UI.

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
Expand All @@ -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

Expand Down Expand Up @@ -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.
11 changes: 11 additions & 0 deletions apps/admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
MdAdminPanelSettings,
MdAccountTree,
MdCode,
MdLibraryBooks,
MdOpenInNew,
MdPublic,
MdTableChart,
Expand All @@ -22,6 +23,8 @@ import {
DataCatalogPage,
LoginPage,
RecordCrossmatchDetailsPage,
ReferenceDetailsPage,
ReferencesPage,
SqlQueryPage,
TableDetailsPage,
TablesPage,
Expand Down Expand Up @@ -110,6 +113,9 @@ function Layout() {
<NavItem to="/data-catalog" label="Data catalog">
<MdAccountTree size={20} />
</NavItem>
<NavItem to="/references" label="References">
<MdLibraryBooks size={20} />
</NavItem>
<NavItem to="/sql" label="SQL">
<MdCode size={20} />
</NavItem>
Expand Down Expand Up @@ -137,6 +143,11 @@ function App() {
<Route path="/tasks" element={<AdminPage />} />
<Route path="/merge-pgc" element={<AdminMergePgcPage />} />
<Route path="/tables" element={<TablesPage />} />
<Route path="/references" element={<ReferencesPage />} />
<Route
path="/references/:schema/:table"
element={<ReferenceDetailsPage />}
/>
<Route path="/sql" element={<SqlQueryPage />} />
<Route path="/data-catalog" element={<DataCatalogPage />} />
<Route path="/data-catalog/query" element={<DataCatalogPage />} />
Expand Down
208 changes: 208 additions & 0 deletions apps/admin/src/components/ui/EditableField.tsx
Original file line number Diff line number Diff line change
@@ -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<T> {
value: T;
formatValue: (value: T) => string;
parseDraft: (draft: string) => T;
input: FieldInputConfig;
onSave: (value: T) => void | Promise<void>;
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<string>,
"formatValue" | "parseDraft" | "input"
> {
input?: FieldInputConfig;
}

function EditableFieldInner<T>({
value,
formatValue,
parseDraft,
input,
onSave,
editLabel,
saving = false,
displayClassName,
inputClassName,
align = "center",
emptyDisplayValue,
renderDisplay,
trimOnCommit = true,
revertOnError = true,
isEmpty,
isUnchanged,
}: EditableFieldProps<T>): ReactElement {
const [editing, setEditing] = useState(false);
const [draft, setDraft] = useState(() => formatValue(value));
const [error, setError] = useState<string | null>(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<void> {
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 (
<span
className={classNames(
displayClassName,
valueIsEmpty(nextValue) && emptyDisplayValue && "text-muted",
)}
>
{toDisplay(nextValue)}
</span>
);
}

function renderValue(nextValue: T): ReactElement {
if (valueIsEmpty(nextValue) && emptyDisplayValue) {
return defaultRender(nextValue);
}
if (renderDisplay) {
return renderDisplay(nextValue);
}
return defaultRender(nextValue);
}

if (editing) {
return (
<div className="min-w-0">
<FieldInput
input={input}
value={draft}
onChange={setDraft}
disabled={saving}
autoFocus
appearance="inline"
className={inputClassName}
onSave={() => void handleSave()}
onCancel={cancelEdit}
commitOnChange={input.kind === "select"}
/>
{error ? <p className="mt-1 text-xs text-danger">{error}</p> : null}
</div>
);
}

return (
<div
className={classNames(
"group/editable flex gap-2 min-w-0",
align === "start" ? "items-start" : "items-center",
)}
>
<div className="min-w-0 flex-1">{renderValue(value)}</div>
<button
type="button"
aria-label={editLabel}
disabled={saving}
className="shrink-0 p-1 rounded text-muted hover:text-primary cursor-pointer opacity-0 group-hover/editable:opacity-100 focus:opacity-100 transition-opacity disabled:opacity-50"
onClick={(event) => {
event.stopPropagation();
startEdit();
}}
>
<MdEdit className="w-4 h-4" />
</button>
</div>
);
}

export function EditableField<T>(props: EditableFieldProps<T>): ReactElement;
export function EditableField(props: StringEditableFieldProps): ReactElement;
export function EditableField<T>(
props: EditableFieldProps<T> | StringEditableFieldProps,
): ReactElement {
if ("parseDraft" in props) {
return <EditableFieldInner {...props} />;
}

return (
<EditableFieldInner
value={props.value}
formatValue={(v) => v}
parseDraft={(d) => d}
input={props.input ?? { kind: "text" }}
onSave={props.onSave}
editLabel={props.editLabel}
saving={props.saving}
displayClassName={props.displayClassName}
inputClassName={props.inputClassName}
align={props.align}
emptyDisplayValue={props.emptyDisplayValue}
renderDisplay={props.renderDisplay}
trimOnCommit={props.trimOnCommit ?? true}
revertOnError={props.revertOnError ?? true}
isEmpty={props.isEmpty}
isUnchanged={props.isUnchanged}
/>
);
}
Loading
Loading