chore: v2.8.7 — Git SSH keys in tabs, safer Artifacts preview and search
- Settings → Git → Your git identity: SSH keys are one tab per host kind (GitHub · GitLab, AWS CodeCommit, More keys, Other hosts); opens on the AWS tab when only an RSA key exists. - Artifacts: a file type the sandboxed preview cannot show (Office, zip, PDF) offers Download only, never a blank Preview. - Artifacts search: `%`/`_`/`\` in a query match literally (LIKE ... ESCAPE). - Skill frontmatter rewrite replaces a key with its continuation lines and quotes values YAML could misread. - Plugin marketplace.json is read with a 1 MB streaming cap. - Artifact lists look each owner up once.
This commit is contained in:
@@ -13,6 +13,9 @@
|
||||
* the server's sandboxed `raw` route in a sandboxed iframe, so nothing a
|
||||
* shared HTML file carries runs on the dashboard's origin. Right-clicking a
|
||||
* card offers the same actions as its buttons, plus copying a page's link.
|
||||
* Preview is offered only for what a browser shows inside that sandbox (text,
|
||||
* images, HTML, audio/video); an Office file, a zip or a PDF is downloaded —
|
||||
* the sandbox blocks both a download and the PDF viewer, so it would be blank.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -63,6 +66,9 @@ function fmtBytes(n: number | null): string {
|
||||
}
|
||||
|
||||
const IMAGE = /\.(png|jpe?g|gif|webp|svg)$/i;
|
||||
// What the sandboxed preview can show: the server's typed list minus PDF.
|
||||
const PREVIEWABLE =
|
||||
/\.(html?|md|markdown|txt|log|csv|tsv|json|ya?ml|sh|py|js|ts|png|jpe?g|gif|webp|svg|mp4|webm|mp3|wav)$/i;
|
||||
|
||||
export function Artifacts() {
|
||||
const { t } = useTranslation("artifacts");
|
||||
@@ -73,6 +79,7 @@ export function Artifacts() {
|
||||
const [items, setItems] = useState<Artifact[] | null>(null);
|
||||
const [adding, setAdding] = useState<ArtifactKind | null>(null);
|
||||
const [addMenu, setAddMenu] = useState(false);
|
||||
const addMenuRef = useRef<HTMLDivElement>(null);
|
||||
const [preview, setPreview] = useState<{ item: Artifact; path: string } | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -105,6 +112,23 @@ export function Artifacts() {
|
||||
[load]
|
||||
);
|
||||
|
||||
// The Add menu closes on Escape or a click anywhere outside it.
|
||||
useEffect(() => {
|
||||
if (!addMenu) return;
|
||||
const onDown = (e: MouseEvent) => {
|
||||
if (!addMenuRef.current?.contains(e.target as Node)) setAddMenu(false);
|
||||
};
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === "Escape") setAddMenu(false);
|
||||
};
|
||||
document.addEventListener("mousedown", onDown);
|
||||
document.addEventListener("keydown", onKey);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onDown);
|
||||
document.removeEventListener("keydown", onKey);
|
||||
};
|
||||
}, [addMenu]);
|
||||
|
||||
const server = !!settings?.serverMode;
|
||||
|
||||
const share = async (a: Artifact) => {
|
||||
@@ -186,11 +210,13 @@ export function Artifacts() {
|
||||
<p className="text-sm text-fg-muted">{t(server ? "subtitleServer" : "subtitle")}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<div className="relative" ref={addMenuRef}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn-primary flex items-center gap-1.5 text-sm"
|
||||
data-testid="artifacts-add"
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={addMenu}
|
||||
onClick={() => setAddMenu((v) => !v)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
@@ -334,7 +360,9 @@ function ArtifactCard({
|
||||
fn();
|
||||
};
|
||||
const previewPath = a.kind === "skill" ? "SKILL.md" : (a.files[0]?.path ?? "");
|
||||
const canPreview = (a.kind === "file" || a.kind === "skill") && a.files.length > 0;
|
||||
const canPreview =
|
||||
a.files.length > 0 &&
|
||||
(a.kind === "skill" || (a.kind === "file" && PREVIEWABLE.test(previewPath)));
|
||||
const btn =
|
||||
"flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-3 hover:text-fg-primary";
|
||||
return (
|
||||
@@ -598,6 +626,7 @@ function PreviewDialog({
|
||||
path: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("artifacts");
|
||||
const [current, setCurrent] = useState(path);
|
||||
const url = api.artifacts.rawUrl(item.id, current);
|
||||
return (
|
||||
@@ -624,7 +653,18 @@ function PreviewDialog({
|
||||
</ul>
|
||||
)}
|
||||
<div className="min-w-0 flex-1 rounded border border-border bg-white">
|
||||
{IMAGE.test(current) && !/\.svg$/i.test(current) ? (
|
||||
{!PREVIEWABLE.test(current) ? (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 bg-surface-1 p-6 text-center">
|
||||
<p className="text-sm text-fg-secondary">{t("noPreview")}</p>
|
||||
<a
|
||||
href={api.artifacts.rawUrl(item.id, current, true)}
|
||||
className="btn-primary flex items-center gap-1.5 text-sm"
|
||||
>
|
||||
<Download className="h-4 w-4" />
|
||||
{t("download")}
|
||||
</a>
|
||||
</div>
|
||||
) : IMAGE.test(current) && !/\.svg$/i.test(current) ? (
|
||||
<img src={url} alt={current} className="h-full w-full object-contain" />
|
||||
) : (
|
||||
// No allow-* tokens: a shared page renders, nothing in it runs.
|
||||
|
||||
Reference in New Issue
Block a user