Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7236413
feat(dashboard): import a proposal from JSON when creating one
brunod-e Aug 2, 2026
c8f5cb0
refactor(dashboard): slim down the Import JSON modal
brunod-e Aug 2, 2026
b0de3eb
fix(dashboard): validate imported calldata before accepting the action
brunod-e Aug 2, 2026
cb77457
fix(dashboard): validate ABI-backed custom actions before import
brunod-e Aug 2, 2026
fd7f6fd
fix(dashboard): settle imported values that only fail at publish
brunod-e Aug 3, 2026
8e47afa
fix(dashboard): close four more paths where a paste fails after import
brunod-e Aug 3, 2026
fd943a7
fix(dashboard): carry the arg checks into composite values
brunod-e Aug 3, 2026
e30c9e9
fix(dashboard): reject amounts and abi types a paste can't carry
brunod-e Aug 3, 2026
75ebadc
fix(dashboard): void a pending decimals read when the paste is edited
brunod-e Aug 3, 2026
77d5930
fix(dashboard): require every imported figure to be quoted
brunod-e Aug 3, 2026
31d14a8
feat(dashboard): accept a .json file in the import modal
brunod-e Aug 3, 2026
86cf7b7
test(dashboard): collapse the import test boilerplate
brunod-e Aug 3, 2026
a883958
fix(dashboard): stop the import from storing what the editor can't hold
brunod-e Aug 3, 2026
2886163
fix(dashboard): refuse a bare function name several overloads share
brunod-e Aug 3, 2026
7ae1865
docs(changeset): trim the import entry to one line
brunod-e Aug 3, 2026
fcfceb3
fix(dashboard): stop accepting an ETH value the form can't show
brunod-e Aug 3, 2026
eccc01f
fix(dashboard): refuse a read-only function on import
brunod-e Aug 3, 2026
c6024de
fix(dashboard): reject a tuple filled past its declared fields
brunod-e Aug 3, 2026
a2b8c38
fix(dashboard): report a rejected upload beside the file button
brunod-e Aug 3, 2026
f9de508
fix(dashboard): trim scalar args, string excepted
brunod-e Aug 3, 2026
c6162dd
refactor(dashboard): one conversion from stored args to calldata
brunod-e Aug 3, 2026
86aeddd
refactor(dashboard): validate an action once, in the form that holds it
brunod-e Aug 3, 2026
cbb9716
fix(dashboard): close the three findings left open
brunod-e Aug 3, 2026
35707fd
feat(dashboard): rework the JSON proposal import per design review
brunod-e Aug 4, 2026
318e06c
fix(dashboard): refuse a custom action that mixes calldata with an AB…
brunod-e Aug 4, 2026
ef23bc4
fix(dashboard): fail closed when encoding a malformed composite arg
brunod-e Aug 5, 2026
1cf17b1
refactor(dashboard): reach create-proposal through its public entry p…
brunod-e Aug 5, 2026
929d8a0
fix(dashboard): agree on the import key whatever case the slug arrive…
brunod-e Aug 5, 2026
54b385b
fix(dashboard): keep the pasted proposal when the import handoff refu…
brunod-e Aug 5, 2026
f681113
fix(dashboard): refuse a composite arg leaf the ABI parameter cannot …
brunod-e Aug 5, 2026
0540720
fix(dashboard): check an action with the converter that will encode it
brunod-e Aug 5, 2026
8535f72
fix(dashboard): refuse a tuple arg that is short of its components
brunod-e Aug 5, 2026
5f94add
refactor(dashboard): cut the import utils down to what the task needs
brunod-e Aug 5, 2026
6e97745
fix(dashboard): refuse an arg count the ABI function doesn't take
brunod-e Aug 5, 2026
8b7e4d2
docs(dashboard): name the converter that actually degrades a bad leaf
brunod-e Aug 5, 2026
b266d1f
fix(dashboard): clear a cancelled import from the handoff stash
brunod-e Aug 5, 2026
96aecf5
fix(dashboard): carry the import handoff across browsing contexts
brunod-e Aug 5, 2026
a91ec72
Merge remote-tracking branch 'origin/dev' into feat/import-proposal-json
brunod-e Aug 5, 2026
21ceb96
style(dashboard): bring comments and one helper in line with the hous…
brunod-e Aug 5, 2026
9008276
refactor(dashboard): one validator for a proposal action, not three
brunod-e Aug 5, 2026
e853c5d
chore(dashboard): lock abitype and jsonc-parser
brunod-e Aug 5, 2026
906fc44
docs(dashboard): cut the commentary down to the traps
brunod-e Aug 5, 2026
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
5 changes: 5 additions & 0 deletions .changeset/import-proposal-json.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@anticapture/dashboard": minor
---

Import a proposal from JSON when creating one.
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
MDXEditor,
Separator,
StrikeThroughSupSubToggles,
codeBlockPlugin,
codeMirrorPlugin,
diffSourcePlugin,
headingsPlugin,
linkDialogPlugin,
Expand Down Expand Up @@ -76,7 +78,6 @@ export const BodyField = ({ version = 0 }: BodyFieldProps) => {
"[&_.mdxeditor-toolbar]:rounded-base! [&_.mdxeditor-toolbar]:!mb-2 [&_.mdxeditor-toolbar]:!bg-transparent [&_.mdxeditor-toolbar]:!px-0 [&_.mdxeditor-toolbar]:!py-0",
"[&_.mdxeditor-root-contenteditable]:max-h-100 md:[&_.mdxeditor-root-contenteditable]:max-h-150 [&_.mdxeditor-root-contenteditable]:overflow-y-auto",
"[&_.cm-editor]:min-h-75 [&_.cm-editor]:max-h-100 [&_.cm-scroller]:min-h-75 [&_.cm-scroller]:max-h-100 md:[&_.cm-editor]:min-h-130 md:[&_.cm-editor]:max-h-150 md:[&_.cm-scroller]:min-h-130 md:[&_.cm-scroller]:max-h-150",
// Markdown source view (CodeMirror) — match the dashboard theme
"[&_.cm-editor]:bg-surface-background [&_.cm-editor]:text-primary",
"[&_.cm-scroller]:bg-surface-background",
"[&_.cm-content]:bg-surface-background [&_.cm-content]:text-primary [&_.cm-content]:caret-primary",
Expand All @@ -91,7 +92,6 @@ export const BodyField = ({ version = 0 }: BodyFieldProps) => {
"[&_.tok-comment]:text-secondary [&_.tok-comment]:italic",
"[&_.tok-number]:text-warning [&_.tok-bool]:text-warning [&_.tok-atom]:text-warning",
"[&_.tok-punctuation]:text-secondary [&_.tok-meta]:text-secondary",
// Heading styles for Visual Editor content (mdxeditor content area)
"[&_.mdxeditor-root-contenteditable_h1]:mb-3 [&_.mdxeditor-root-contenteditable_h1]:mt-4 [&_.mdxeditor-root-contenteditable_h1]:text-2xl [&_.mdxeditor-root-contenteditable_h1]:font-semibold",
"[&_.mdxeditor-root-contenteditable_h2]:mb-2 [&_.mdxeditor-root-contenteditable_h2]:mt-4 [&_.mdxeditor-root-contenteditable_h2]:text-xl [&_.mdxeditor-root-contenteditable_h2]:font-semibold",
"[&_.mdxeditor-root-contenteditable_h3]:mb-2 [&_.mdxeditor-root-contenteditable_h3]:mt-3 [&_.mdxeditor-root-contenteditable_h3]:text-lg [&_.mdxeditor-root-contenteditable_h3]:font-semibold",
Expand Down Expand Up @@ -122,6 +122,17 @@ export const BodyField = ({ version = 0 }: BodyFieldProps) => {
linkPlugin(),
linkDialogPlugin(),
tablePlugin(),
codeBlockPlugin({ defaultCodeBlockLanguage: "" }),
codeMirrorPlugin({
codeBlockLanguages: {
"": "Plain text",
json: "JSON",
js: "JavaScript",
ts: "TypeScript",
solidity: "Solidity",
bash: "Shell",
},
}),
markdownShortcutPlugin(),
diffSourcePlugin({
viewMode: mode === "markdown" ? "source" : "rich-text",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"use client";

import { useRef, type Ref } from "react";

import { cn } from "@/shared/utils/cn";

type JsonTextareaProps = {
value: string;
onChange: (value: string) => void;
placeholder?: string;
errorLine?: number;
showLineNumbers?: boolean;
hasError?: boolean;
ariaLabel?: string;
className?: string;
ref?: Ref<HTMLTextAreaElement>;
};

export const JsonTextarea = ({
value,
onChange,
placeholder,
errorLine,
showLineNumbers = false,
hasError = false,
ariaLabel,
className,
ref,
}: JsonTextareaProps) => {
const gutterRef = useRef<HTMLDivElement>(null);

const lineCount = value === "" ? 0 : value.split("\n").length;

const gutterDigits = Math.max(2, String(lineCount).length);

return (
<div
className={cn(
"border-border-contrast bg-surface-default rounded-base flex overflow-hidden border transition-all duration-200",
"focus-within:shadow-[var(--shadow-focus-ring)]",
"max-h-[50vh] min-h-32 resize-y",
hasError && "border-error",
className,
)}
>
{showLineNumbers && (
<div
ref={gutterRef}
aria-hidden
style={{ width: `calc(${gutterDigits}ch + 1rem)` }}
className="text-dimmed pointer-events-none shrink-0 select-none overflow-hidden py-2 pl-2 pr-2 text-right font-mono text-xs leading-5"
>
{Array.from({ length: lineCount }, (_, index) => {
const line = index + 1;
return (
<div
key={line}
className={line === errorLine ? "text-error" : ""}
>
{line}
</div>
);
})}
</div>
)}

<textarea
ref={ref}
value={value}
onChange={(event) => onChange(event.target.value)}
onScroll={(event) => {
const gutter = gutterRef.current;
if (gutter) gutter.scrollTop = event.currentTarget.scrollTop;
}}
placeholder={placeholder}
spellCheck={false}
aria-label={ariaLabel}
className={cn(
"text-primary placeholder:text-dimmed h-full w-full resize-none whitespace-pre bg-transparent py-2 pr-2.5 font-mono text-xs leading-5",
"scrollbar-thin overflow-auto border-0 outline-none",
showLineNumbers ? "pl-0" : "pl-2.5",
)}
/>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"use client";

import * as PopoverPrimitive from "@radix-ui/react-popover";
import { Plus } from "lucide-react";
import { useState } from "react";

import { Button } from "@/shared/components/design-system/buttons/button/Button";
import { itemStatusStyles } from "@/shared/components/design-system/combobox/styles";
import { cn } from "@/shared/utils/cn";

type NewProposalMenuProps = {
onCreateNew: () => void;
onImportJson: () => void;
triggerProps?: React.ComponentProps<typeof Button> &
Partial<Record<`data-${string}`, string | undefined>>;
};

const MenuItem = ({
label,
onSelect,
}: {
label: string;
onSelect: () => void;
}) => (
<button
type="button"
role="menuitem"
onClick={onSelect}
className={cn(
"flex w-full items-center px-3 py-2 text-left",
"text-primary text-sm font-normal leading-5",
itemStatusStyles.default,
"hover:bg-surface-hover cursor-pointer transition-colors duration-150",
)}
>
{label}
</button>
);

export const NewProposalMenu = ({
onCreateNew,
onImportJson,
triggerProps,
}: NewProposalMenuProps) => {
const [open, setOpen] = useState(false);

const choose = (action: () => void) => () => {
setOpen(false);
action();
};

return (
<PopoverPrimitive.Root open={open} onOpenChange={setOpen}>
<PopoverPrimitive.Trigger asChild>
<Button
variant="primary"
size="md"
aria-haspopup="menu"
aria-expanded={open}
{...triggerProps}
className={cn(
"flex-1 whitespace-nowrap lg:w-fit lg:flex-none",
triggerProps?.className,
)}
>
<Plus className="size-4" />
New Proposal
</Button>
</PopoverPrimitive.Trigger>

<PopoverPrimitive.Content
role="menu"
align="end"
sideOffset={4}
className={cn(
"flex flex-col",
"min-w-40 py-1",
"bg-surface-contrast",
"border-border-contrast rounded-base border",
"z-50",
"animate-[popover-slide-in_0.15s_ease-out]",
)}
>
<MenuItem label="Create new" onSelect={choose(onCreateNew)} />
<MenuItem label="Import JSON" onSelect={choose(onImportJson)} />
</PopoverPrimitive.Content>
</PopoverPrimitive.Root>
);
};
Loading
Loading