-
-
Notifications
You must be signed in to change notification settings - Fork 465
chore: adopt Oxlint for JS/TS semantics; keep Biome for format/non-JS (#321) #371
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
90155fa
66801a2
6393682
82180bc
5bac52f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@open-slide/core": patch | ||
| --- | ||
|
|
||
| Fix lint findings uncovered by Oxlint (floating promises, hook deps, a11y labels). | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| { | ||
| "$schema": "./node_modules/oxlint/configuration_schema.json", | ||
| "plugins": ["typescript", "unicorn", "react", "import", "vitest", "jsx-a11y"], | ||
| "categories": { | ||
| "correctness": "error" | ||
| }, | ||
| "env": { | ||
| "builtin": true | ||
| }, | ||
| "settings": { | ||
| "react": { | ||
| "version": "19.2.7" | ||
| } | ||
| }, | ||
| "ignorePatterns": [ | ||
| "**/dist/**", | ||
| "**/node_modules/**", | ||
| "**/*.tsbuildinfo", | ||
| "packages/core/src/app/components/ui/**", | ||
| "packages/cli/template/**" | ||
| ], | ||
| "rules": { | ||
| "import/no-cycle": "error", | ||
| "typescript/no-floating-promises": "error", | ||
| "jsx-a11y/prefer-tag-over-role": "off", | ||
| "vitest/expect-expect": [ | ||
| "error", | ||
| { | ||
| "assertFunctionNames": [ | ||
| "expect", | ||
| "expectTypeOf", | ||
| "assert", | ||
| "expectTagged", | ||
| "expectTaggedTransform" | ||
| ] | ||
| } | ||
| ] | ||
| }, | ||
| "overrides": [ | ||
| { | ||
| "files": ["**/slides/**", "apps/web/components/landing/**"], | ||
| "rules": { | ||
| "jsx-a11y/no-static-element-interactions": "off", | ||
| "jsx-a11y/no-noninteractive-element-interactions": "off", | ||
| "jsx-a11y/prefer-tag-over-role": "off" | ||
| } | ||
|
Comment on lines
+41
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "== oxlintrc context =="
if [ -f .oxlintrc.json ]; then
nl -ba .oxlintrc.json | sed -n '1,120p'
fi
echo
echo "== matching files count =="
# Use git ls-files to inspect tracked matching paths, not filesystem only.
git ls-files | awk '
/\/slides\// {a++}
/^apps\/web\/components\/landing\// {b++}
END {print "slides_tree:", a+0; print "landing_tree:", b+0}
'
echo
echo "== specific rule occurrences in matching tracked files =="
python3 - <<'PY'
import subprocess
import pathlib,textwrap
files = subprocess.check_output(["git","ls-files"], text=True).splitlines()
slides=[]
landing=[]
for f in files:
if "/slides/" in f:
slides.append(f)
elif f.startswith("apps/web/components/landing/"):
landing.append(f)
patterns=[
'setAsDropZones',
'dropZone',
'isDropZone',
'asset',
'files',
'onDrop',
'onDragOver',
'onDragEnter',
'onClick',
'interactions',
]
for p in patterns:
hits=[]
for f in slides+landing:
try:
data=pathlib.Path(f).read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
if p in data:
hits.append(f"{f}:{data.count(p)}")
if hits:
print(f"\n[{p}] count={len(hits)}")
for h in hits[:50]:
print(" " + h)
if len(hits)>50:
print(f" ... and {len(hits)-50} more")
PY
echo
echo "== direct usages outside matching trees =="
python3 - <<'PY'
import subprocess,pathlib
files = subprocess.check_output(["git","ls-files"], text=True).splitlines()
terms=['setAsDropZones','dropZone','isDropZone']
for term in terms:
hits=[]
for f in files:
if pathlib.PurePath(f).is_absolute():
continue
try:
data=pathlib.Path(f).read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
if term in data:
hits.append(f)
print(f"{term}: {len(hits)}")
for h in hits[:80]:
print(" ", h)
if len(hits)>80:
print(f" ... and {len(hits)-80} more")
PYRepository: 1weiho/open-slide Length of output: 218 🏁 Script executed: #!/bin/bash
set -u
echo "== oxlintrc context =="
if [ -f .oxlintrc.json ]; then
awk '{printf "%6d %s\n", NR, $0}' .oxlintrc.json | sed -n '1,120p'
fi
echo
echo "== matching tracked files count =="
git ls-files | awk '
/\/slides\// {a++}
/^apps\/web\/components\/landing\// {b++}
END {print "slides_tree:", a+0; print "landing_tree:", b+0}
'
echo
echo "== relevant symbol occurrences in matching tracked files =="
python3 - <<'PY'
import subprocess, pathlib
files = subprocess.check_output(["git","ls-files"], text=True).splitlines()
slides=[f for f in files if "/slides/" in f]
landing=[f for f in files if f.startswith("apps/web/components/landing/")]
patterns=[
'setAsDropZones',
'dropZone',
'isDropZone',
'drop z',
'onDrop',
'onDragOver',
'onDragEnter',
'setDropZones',
]
for p in patterns:
hits=[]
for f in slides+landing:
try:
data=pathlib.Path(f).read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
if p in data:
hits.append((f, data.count(p)))
hits.sort(key=lambda x:x[1], reverse=True)
if hits:
print(f"\n[{p}] count={len(hits)}")
for f, n in hits[:40]:
print(f" {f}: {n}")
if len(hits)>40:
print(f" ... and {len(hits)-40} more")
PY
echo
echo "== targeted drop-zone symbol occurrences outside matching trees =="
python3 - <<'PY'
import subprocess, pathlib
files = subprocess.check_output(["git","ls-files"], text=True).splitlines()
terms=['setAsDropZones','dropZone','isDropZone','setDropZones']
matcher=lambda f: "/slides/" in f or f.startswith("apps/web/components/landing/")
for term in terms:
in_tree=[]
out_tree=[]
for f in files:
try:
data=pathlib.Path(f).read_text(encoding="utf-8", errors="ignore")
except Exception:
continue
if term in data:
(out_tree if not matcher(f) else in_tree).append(f)
print(f"{term}: in_tree={len(in_tree)}, out_tree={len(out_tree)}")
for f in out_tree[:80]:
print(" ", f)
PY
echo
echo "== rule occurrences outside .oxlintrc.json =="
if command -v rg >/dev/null 2>&1; then
rg -n 'no-static-element-interactions|no-noninteractive-element-interactions|prefer-tag-over-role|files.*slides|components/landing' -g '!node_modules' . | sed -n '1,160p'
else
grep -RInE 'no-static-element-interactions|no-noninteractive-element-interactions|prefer-tag-over-role|slide|landing' . | sed -n '1,160p'
fiRepository: 1weiho/open-slide Length of output: 7228 Narrow the accessibility override to the actual drop-zone files. These tree-level overrides affect every tracked file under both patterns ( 🤖 Prompt for AI Agents |
||
| } | ||
| ] | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,16 +23,20 @@ Shared config: `biome.json`, `turbo.json`, `pnpm-workspace.yaml`, `tsconfig` per | |
| pnpm dev # turbo: runs demo against local core | ||
| pnpm build # build all packages | ||
| pnpm typecheck # tsc across the graph | ||
| pnpm check # biome (format + lint + organize imports) | ||
| pnpm check:fix # auto-fix what biome can | ||
| pnpm format:check # biome formatter | ||
| pnpm lint # oxlint (JS/TS) + biome (CSS/JSON) | ||
| pnpm lint:js # oxlint only | ||
| pnpm lint:nonjs # biome non-JS lint only | ||
|
Comment on lines
+27
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Keep CSS lint ownership documentation consistent with The final configuration disables Biome CSS linting, so these lines promise coverage that
📍 Affects 3 files
🤖 Prompt for AI Agents |
||
| pnpm check # format:check + lint + typecheck | ||
| pnpm check:fix # oxlint --fix + biome check --write | ||
| pnpm test # vitest | ||
| ``` | ||
|
|
||
| Filter to one package: `pnpm core <script>` / `pnpm cli <script>`. | ||
|
|
||
| ## Hard rules | ||
|
|
||
| - **Biome must pass before commit.** Run `pnpm check` (or `pnpm check:fix`). CI and the user's review both expect a clean tree. | ||
| - **Format and lint must pass before commit.** Run `pnpm check` (or `pnpm check:fix`). Oxlint owns JS/TS semantics; Biome owns formatting and non-JS lint. CI and the user's review both expect a clean tree. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Do not present
🤖 Prompt for AI Agents |
||
| - **If `packages/core` or `packages/cli` changes, add a changeset.** Run `pnpm changeset`, pick the right package(s) and bump (`patch` for fixes/polish, `minor` for new public API, `major` for breaking). Apps (`demo`, `web`) and root tooling do **not** need one. | ||
| - **Changeset descriptions: short and direct.** One line, present-tense, what changed from a user's perspective. Match the tone of `.changeset/*.md` already in the repo. No paragraphs, no rationale, no "this PR…". | ||
| - Good: `Replace spinner with a hairline + sliding bar for slide and presenter loading states.` | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,30 +28,9 @@ | |
| }, | ||
| "overrides": [ | ||
| { | ||
| "includes": ["**/slides/**", "apps/web/components/landing/**"], | ||
| "includes": ["**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}", "**/*.d.ts"], | ||
| "linter": { | ||
| "rules": { | ||
| "a11y": { | ||
| "noSvgWithoutTitle": "off", | ||
| "noStaticElementInteractions": "off", | ||
| "useAriaPropsSupportedByRole": "off", | ||
| "useSemanticElements": "off" | ||
| }, | ||
| "performance": { | ||
| "noImgElement": "off" | ||
| }, | ||
| "complexity": { | ||
| "noUselessFragments": "off" | ||
| }, | ||
| "security": { | ||
| "noDangerouslySetInnerHtml": "off" | ||
| }, | ||
| "suspicious": { | ||
| "noArrayIndexKey": "off", | ||
| "noTemplateCurlyInString": "off", | ||
| "noShadowRestrictedNames": "off" | ||
| } | ||
| } | ||
| "enabled": false | ||
|
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'Files matching biome/oxlint/config scripts:\n'
git ls-files | rg '(^|/)(biome\.json|oxlint|oxlint\.json|eslint\.config|package\.json|pnpm-lock\.yaml|biome\.jsonc|\.oxlintrc)' || true
printf '\nbiome.json (if present):\n'
if [ -f biome.json ]; then
nl -ba biome.json | sed -n '1,220p'
fi
printf '\nOxlint/config related files content snippets:\n'
for f in $(git ls-files | rg '(^|/)(oxlint|\.oxlintrc|eslint\.config|package\.json)$'); do
printf '\n--- %s ---\n' "$f"
nl -ba "$f" | sed -n '1,220p'
done
printf '\nSearch security/relevant rule names:\n'
rg -n "noDangerouslySetInnerHtml|noDangerouslySetInnerHtmlWithChildren|noGlobalEval|dangerouslySetInnerHTML|global.*eval|no.*eval|dangerous|oxlint|lint" -S . --glob '!node_modules' --glob '!dist' --glob '!build' | sed -n '1,240p'Repository: 1weiho/open-slide Length of output: 524 🌐 Web query:
💡 Result: Biome provides several security-focused lint rules designed to prevent vulnerabilities related to arbitrary code execution and Cross-Site Scripting (XSS), including the following: noDangerouslySetInnerHtml: This rule (lint/security/noDangerouslySetInnerHtml) prevents the use of the dangerouslySetInnerHTML prop in React, which can expose users to XSS attacks by injecting unescaped HTML [1][2]. It is enabled by default and set to error severity [1]. noGlobalEval: This rule (lint/security/noGlobalEval) prohibits the use of global eval, which evaluates strings as JavaScript code [3]. This is considered a security risk because it allows for arbitrary code execution if the input is untrusted, and it also negatively impacts performance by preventing JavaScript engine optimizations [3]. Additional related security rules include: - noDangerouslySetInnerHtmlWithChildren: Reports when an element uses both children and dangerouslySetInnerHTML simultaneously [4][5]. - noScriptUrl: Disallows the use of javascript: URLs in HTML (e.g., in href attributes), as they act as a form of eval and can lead to XSS [6]. - noImpliedEval: A nursery rule (currently unstable) that flags eval-like methods such as using setTimeout, setInterval, or the Function constructor with string arguments [7]. These rules are part of Biome's focus on identifying patterns that lead to security vulnerabilities and performance issues, favoring safer alternatives like direct function references or standard JSX rendering [1][3][7]. Citations:
🏁 Script executed: #!/bin/bash
set -euo pipefail
printf 'biome.json excerpt:\n'
awk '{printf "%6d\t%s\n", NR, $0}' biome.json | sed -n '1,120p'
printf '\n.oxlintrc.json:\n'
awk '{printf "%6d\t%s\n", NR, $0}' .oxlintrc.json | sed -n '1,220p'
printf '\nRelevant rule/config mentions:\n'
for f in biometxt=".biome.json" || true; do
true
done
rg -n "noDangerouslySetInnerHtml|noDangerouslySetInnerHtmlWithChildren|noGlobalEval|noUndocumentedGlobals|dangerouslySetInnerHTML|dangerous|eval|allowComments|noScriptUrl|noImpliedEval|noDangerous|dangerous" -S .biome.json biome.json .oxlintrc.json package.json 2>/dev/null || true
printf '\nPackage scripts:\n'
python3 - <<'PY'
import json, pathlib
for p in ["package.json", ".oxlintrc.json", "biome.json"]:
if pathlib.Path(p).exists():
print(f"\n--- {p} ---")
data=json.loads(pathlib.Path(p).read_text())
if p=="package.json":
print(json.dumps({"scripts": data.get("scripts", {})}, indent=2)[:2000])
else:
print(json.dumps(data, indent=2)[:3000])
PYRepository: 1weiho/open-slide Length of output: 3299 Preserve Biome’s JS/TS security checks or add equivalent Oxlint rules. The JS/TS override disables Biome security rules such as 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| ], | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -291,6 +291,7 @@ export function AssetView({ slideId }: Props) { | |
| ); | ||
| } | ||
|
|
||
| /* oxlint-disable jsx-a11y/no-noninteractive-element-interactions -- file drop zone */ | ||
| return ( | ||
| <section | ||
| aria-label={t.asset.sectionAria} | ||
|
|
@@ -626,6 +627,7 @@ export function AssetView({ slideId }: Props) { | |
| </section> | ||
| ); | ||
| } | ||
| /* oxlint-enable jsx-a11y/no-noninteractive-element-interactions */ | ||
|
|
||
| function EmptyState() { | ||
| const t = useLocale(); | ||
|
|
@@ -1130,13 +1132,13 @@ function RenameAsset({ | |
| disabled={saving} | ||
| onChange={(event) => setValue(event.target.value)} | ||
| onBlur={() => { | ||
| if (!saving) commit(); | ||
| if (!saving) void commit(); | ||
| }} | ||
| onKeyDown={(event) => { | ||
| if (event.nativeEvent.isComposing) return; | ||
| if (event.key === 'Enter') { | ||
| event.preventDefault(); | ||
| commit(); | ||
| void commit(); | ||
|
Comment on lines
+1138
to
+1144
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Locate files:"
git ls-files | rg 'packages/core/src/app/(components/asset-view\.tsx|routes/slide\.tsx)$' || true
echo
echo "Relevant asset-view lines:"
sed -n '1110,1200p' packages/core/src/app/components/asset-view.tsx
echo
echo "Relevant slide lines:"
sed -n '1085,1120p' packages/core/src/app/routes/slide.tsx
echo
echo "Search commit definitions/callback usages:"
rg -n "function .*commit|const .*commit|commit=\(|commit=\(\)|onBlur=|onKeyDown|Promise|reject|Error|Async" packages/core/src/app -g '*.tsx' -g '*.ts' | head -n 300Repository: 1weiho/open-slide Length of output: 27297 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "asset-view rename component definition:"
sed -n '1030,1105p' packages/core/src/app/components/asset-view.tsx
sed -n '1100,1135p' packages/core/src/app/components/asset-view.tsx
echo
echo "slide rename component definition:"
sed -n '1030,1105p' packages/core/src/app/routes/slide.tsx
sed -n '1100,1115p' packages/core/src/app/routes/slide.tsx
echo
echo "Component callers for RenameEditable:"
rg -n "<RenameEditable|RenameEditable<|onSubmit=\\(" packages/core/src/app -g '*.tsx' -g '*.ts' -C 3
echo
echo "Read-only behavioral probe: unhandled async rejection logged by Node when using void"
node - <<'JS'
let logged;
process.on('unhandledRejection', (reason, promise) => {
logged = String(reason);
});
async function reject() {
throw new Error('rename failed');
}
(async () => {
void reject();
await new Promise(resolve => setTimeout(resolve, 20)).then(() => {
console.log(logged);
});
})();
JSRepository: 1weiho/open-slide Length of output: 5712 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "Find RenameEditable declarations/usages by regex across tracked files:"
rg -n "function RenameEditable|const Rename|class Rename|type Rename|interface Rename|<RenameEditable|RenameEditable<|onSubmit=\(next|" packages/core/src -g '*.tsx' -g '*.ts' -C 4 || true
echo
echo "Surrounding RenameAsset usages:"
rg -n "RenameAsset<" packages/core/src/app -g '*.tsx' -g '*.ts' -C 8 || true
echo
echo "Exact rename call sites containing toast/error handling with await onSubmit or promise rejection:"
rg -n "await onSubmit|onSubmit\\\\(|toast|toast\\.error|\\.catch|unhandled" packages/core/src/app -g '*.tsx' -g '*.ts' -C 4 || true
echo
echo "Read-only behavioral probe: unhandled async rejection logged by Node when using void"
node - <<'JS'
let logged = null;
process.on('unhandledRejection', (reason, promise) => {
logged = String(reason);
});
(async () => {
void Promise.reject(new Error('rename failed'));
await new Promise(resolve => setTimeout(resolve, 20));
console.log(logged);
})();
JSRepository: 1weiho/open-slide Length of output: 50374 🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "RenameAsset declarations/usages in app subdir only (limited):"
rg -n "function RenameAsset|<RenameAsset|onSubmit=\(" packages/core/src/app -g '*.tsx' -C 6 || true
echo
echo "Renamed asset toast/error call sites in app/assets.ts:"
sed -n '1,80p' packages/core/src/app/lib/assets.ts
rg -n "renameAsset|toast\.error|toastRename|rename\(" packages/core/src/app lib packages/core/src -g '*.tsx' -g '*.ts' -C 4 || true
echo
echo "Slide commit caller context around InlineTitleEditor if present:"
rg -n "InlineTitleEditor|renameSlide|toastSlide|toast.*rename|toast.*failed" packages/core/src/app/routes/slide.tsx -C 8 || true
echo
echo "Focused text matches around RenameEditable in all tracked files:"
rg -n "RenameEditable|onSubmit=\(\w+\)" packages/core/src -g '*.tsx' -g '*.ts' --glob '!node_modules' -C 4 | head -n 200 || trueRepository: 1weiho/open-slide Length of output: 50373 Handle rename promise rejections at each commit boundary. These handlers return promises to a 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
| } else if (event.key === 'Escape') { | ||
| event.preventDefault(); | ||
| onCancel(); | ||
|
|
@@ -1180,13 +1182,13 @@ function RenameAsset({ | |
| disabled={saving} | ||
| onChange={(e) => setValue(e.target.value)} | ||
| onBlur={() => { | ||
| if (!saving) commit(); | ||
| if (!saving) void commit(); | ||
| }} | ||
| onKeyDown={(e) => { | ||
| if (e.nativeEvent.isComposing) return; | ||
| if (e.key === 'Enter') { | ||
| e.preventDefault(); | ||
| commit(); | ||
| void commit(); | ||
| } else if (e.key === 'Escape') { | ||
| e.preventDefault(); | ||
| onCancel(); | ||
|
|
@@ -1379,7 +1381,7 @@ function LogoSearchDialog({ | |
| queueMicrotask(() => inputRef.current?.focus()); | ||
| }, []); | ||
|
|
||
| // biome-ignore lint/correctness/useExhaustiveDependencies: retryToken is a bump-to-refetch trigger | ||
| // retryToken is a bump-to-refetch trigger; toast copy is locale-stable | ||
| useEffect(() => { | ||
| const ctrl = new AbortController(); | ||
| const timer = setTimeout(() => { | ||
|
|
@@ -1400,7 +1402,7 @@ function LogoSearchDialog({ | |
| clearTimeout(timer); | ||
| ctrl.abort(); | ||
| }; | ||
| }, [query, retryToken]); | ||
| }, [query, retryToken]); // oxlint-disable-line react-hooks/exhaustive-deps -- toast string is locale-stable | ||
|
|
||
| return ( | ||
| <Dialog open onOpenChange={(open) => !open && onClose()}> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -688,7 +688,10 @@ function ColorField({ | |
|
|
||
| return ( | ||
| <Field label={label}> | ||
| <label className="relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-md border bg-background shadow-xs"> | ||
| <label | ||
| aria-label={label} | ||
| className="relative inline-flex size-8 shrink-0 cursor-pointer items-center justify-center overflow-hidden rounded-md border bg-background shadow-xs" | ||
| > | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| <span | ||
| className="size-5 rounded-sm" | ||
| style={{ | ||
|
|
@@ -927,7 +930,7 @@ function CommentsSection({ | |
| onKeyDown={(e) => { | ||
| if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { | ||
| e.preventDefault(); | ||
| submit(); | ||
| void submit(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Handle rejected submissions before discarding the promise.
🤖 Prompt for AI Agents |
||
| } | ||
| }} | ||
| placeholder={t.inspector.commentPlaceholder} | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.