{rows.length === 0 ? (
@@ -58,7 +160,12 @@ export function PermissionCard({ rows }: { rows: PermissionRowData[] }) {
) : (
rows.map((row, i) => (
0 && "border-t border-border")}>
-
+ {/* 有变动时未变动的类别塌成单行让位;整卡零变化时用户是主动点开的,给全量 */}
+ {changed.length > 0 && !isPermissionChanged(row) ? (
+
+ ) : (
+
+ )}
))
)}
diff --git a/src/pages/install/components/PermissionRow.test.tsx b/src/pages/install/components/PermissionRow.test.tsx
index 15a927e9d..36b11c75a 100644
--- a/src/pages/install/components/PermissionRow.test.tsx
+++ b/src/pages/install/components/PermissionRow.test.tsx
@@ -69,3 +69,83 @@ describe("PermissionRow 权限行", () => {
expect(within(row).queryByTestId("permission-more")).not.toBeInTheDocument();
});
});
+
+describe("PermissionRow 更新差异态", () => {
+ const changed = {
+ kind: "connect" as const,
+ risk: "danger" as const,
+ values: ["*", "api.a.com", "api.b.com"],
+ sensitive: [],
+ diff: { added: ["*"], removed: ["old.a.com"] },
+ };
+
+ it("新增与移除的取值常驻可见,未变动的收进折叠桶", () => {
+ render(
);
+ const row = screen.getByTestId("permission-row");
+ expect(within(row).getByText("*")).toBeInTheDocument();
+ expect(within(row).getByText("old.a.com")).toBeInTheDocument();
+ expect(within(row).queryByText("api.a.com")).not.toBeInTheDocument();
+ expect(within(row).getByTestId("permission-more")).toHaveTextContent("未变动 2 项");
+ });
+
+ it("变动状态标在 chip 上,且新增/移除带可读状态文案而不只靠颜色", () => {
+ render(
);
+ const row = screen.getByTestId("permission-row");
+ expect(within(row).getByText("*").closest("[data-chip]")).toHaveAttribute("data-change", "added");
+ expect(within(row).getByText("old.a.com").closest("[data-chip]")).toHaveAttribute("data-change", "removed");
+ expect(within(row).getByText("新增")).toBeInTheDocument();
+ expect(within(row).getByText("已移除")).toBeInTheDocument();
+ });
+
+ it("展开折叠桶后未变动取值可见并标记为 unchanged", () => {
+ render(
);
+ const row = screen.getByTestId("permission-row");
+ fireEvent.click(within(row).getByTestId("permission-more"));
+ expect(within(row).getByText("api.a.com").closest("[data-chip]")).toHaveAttribute("data-change", "unchanged");
+ expect(within(row).queryByTestId("permission-more")).not.toBeInTheDocument();
+ });
+
+ it("行内变动计数按新增与移除分别呈现", () => {
+ render(
);
+ const row = screen.getByTestId("permission-row");
+ expect(within(row).getByTestId("permission-delta")).toHaveTextContent("+1");
+ expect(within(row).getByTestId("permission-delta")).toHaveTextContent("−1");
+ });
+
+ it("差异为空的行不渲染变动计数,取值全部按未变动呈现", () => {
+ render(
+
+ );
+ const row = screen.getByTestId("permission-row");
+ expect(within(row).queryByTestId("permission-delta")).not.toBeInTheDocument();
+ expect(within(row).getByText("a").closest("[data-chip]")).toHaveAttribute("data-change", "unchanged");
+ });
+});
+
+describe("PermissionRow 零变动行的取值折叠", () => {
+ const manyUnchanged = {
+ kind: "match" as const,
+ risk: "normal" as const,
+ values: ["https://a.com/*", "https://b.com/*", "https://c.com/*", "https://d.com/*"],
+ sensitive: [],
+ diff: { added: [], removed: [] },
+ };
+
+ it("没有可钉住的增删时,取值仍按 maxVisible 折叠为 +N", () => {
+ render(
);
+ const row = screen.getByTestId("permission-row");
+ expect(within(row).getByText("https://a.com/*")).toBeInTheDocument();
+ expect(within(row).queryByText("https://c.com/*")).not.toBeInTheDocument();
+ expect(within(row).getByTestId("permission-more")).toHaveTextContent("+2");
+ });
+
+ it("点开 +N 后其余取值可见", () => {
+ render(
);
+ const row = screen.getByTestId("permission-row");
+ fireEvent.click(within(row).getByTestId("permission-more"));
+ expect(within(row).getByText("https://d.com/*")).toBeInTheDocument();
+ expect(within(row).queryByTestId("permission-more")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/pages/install/components/PermissionRow.tsx b/src/pages/install/components/PermissionRow.tsx
index bc18da4c2..c83d703fe 100644
--- a/src/pages/install/components/PermissionRow.tsx
+++ b/src/pages/install/components/PermissionRow.tsx
@@ -2,7 +2,12 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Globe, ArrowLeftRight, ChevronDown, KeyRound, Package, TriangleAlert, type LucideIcon } from "lucide-react";
import { cn } from "@App/pkg/utils/cn";
-import type { PermissionKind, PermissionRisk, PermissionRow as PermissionRowData } from "../permissions";
+import {
+ isPermissionChanged,
+ type PermissionKind,
+ type PermissionRisk,
+ type PermissionRow as PermissionRowData,
+} from "../permissions";
export const KIND_META: Record
= {
match: { icon: Globe, labelKey: "install:perm_match_label", summaryKey: "install:perm_match_summary" },
@@ -35,7 +40,64 @@ export const RISK_STYLE: Record = {
+ added: "install:perm_change_added",
+ removed: "install:perm_change_removed",
+ unchanged: "install:perm_change_unchanged",
+};
+
+function Chip({ value, row, change }: { value: string; row: PermissionRowData; change?: ChangeState }) {
+ const { t } = useTranslation(["install", "common"]);
+ const isSensitive = row.sensitive.includes(value);
+ const base = isSensitive ? "border border-warning-fg bg-muted text-warning-fg" : RISK_STYLE[row.risk].chip;
+
+ // 变动状态只用字重、描边与 +/− 记号表达,底色继续留给风险等级——
+ // 否则新增的 @connect * 会被染成「新增色」,把最该报警的情况伪装成安全的。
+ const changeClass =
+ change === "removed"
+ ? "border border-border bg-transparent text-muted-foreground line-through"
+ : change === "unchanged"
+ ? "bg-muted border border-border text-muted-foreground"
+ : change === "added"
+ ? cn(base, "font-semibold", row.risk === "danger" && !isSensitive ? "border-destructive" : "border-current")
+ : base;
+
+ return (
+
+ {change === "added" && {"+"}}
+ {change === "removed" && {"−"}}
+ {isSensitive && change !== "removed" && }
+ {change && {t(CHANGE_LABEL_KEY[change])}}
+ {value}
+
+ );
+}
+
+function MoreButton({ label, onClick }: { label: string; onClick: () => void }) {
+ return (
+
+ );
+}
+
+/**
+ * 权限取值 chip 列表;桌面行与移动 Accordion 共用。
+ * 全新安装按可见项 + 折叠的 +N 呈现;更新场景把新增与移除常驻在前,未变动项收进「未变动 N 项」的折叠桶。
+ */
export function PermissionChips({
row,
maxVisible = DEFAULT_MAX_VISIBLE,
@@ -43,46 +105,74 @@ export function PermissionChips({
row: PermissionRowData;
maxVisible?: number;
}) {
- const style = RISK_STYLE[row.risk];
- const sensitive = new Set(row.sensitive);
+ const { t } = useTranslation(["install", "common"]);
const [expanded, setExpanded] = useState(false);
- const visible = expanded ? row.values : row.values.slice(0, maxVisible);
- const hidden = row.values.length - visible.length;
+
+ if (!row.diff) {
+ const visible = expanded ? row.values : row.values.slice(0, maxVisible);
+ const hidden = row.values.length - visible.length;
+ return (
+
+ {visible.map((v) => (
+
+ ))}
+ {hidden > 0 && setExpanded(true)} />}
+
+ );
+ }
+
+ const { added, removed } = row.diff;
+ const addedSet = new Set(added);
+ const unchanged = row.values.filter((v) => !addedSet.has(v));
+ const pinned = added.length + removed.length;
+ // 有增删时未变动项整体让位给折叠桶;一项没变时没有可钉住的内容,桶会变成必须点开才能看到全部的空壳,
+ // 故退回全新安装的 maxVisible 截断——否则几十条 @match 的脚本一更新就会整片摊开。
+ const visibleUnchanged = expanded ? unchanged : pinned > 0 ? [] : unchanged.slice(0, maxVisible);
+ const hidden = unchanged.length - visibleUnchanged.length;
return (
- {visible.map((v) => {
- const isSensitive = sensitive.has(v);
- return (
-
- {isSensitive && }
- {v}
-
- );
- })}
+ {added.map((v) => (
+
+ ))}
+ {removed.map((v) => (
+
+ ))}
+ {visibleUnchanged.map((v) => (
+
+ ))}
{hidden > 0 && (
-
+ />
)}
);
}
+/** 无变化标记;更新场景用来把「这一类你上次已经确认过」说出来 */
+export function NoChangeTag() {
+ const { t } = useTranslation(["install", "common"]);
+ return (
+
+ {t("install:perm_no_change")}
+
+ );
+}
+
+/** 行头的变动计数;沿用代码卡 +N −M 的记号,无变动时不渲染 */
+export function PermissionDelta({ row }: { row: PermissionRowData }) {
+ if (!isPermissionChanged(row)) return null;
+ const { added, removed } = row.diff!;
+ return (
+
+ {added.length > 0 && {`+${added.length}`}}
+ {removed.length > 0 && {`−${removed.length}`}}
+
+ );
+}
+
export function PermissionRow({ row, maxVisible }: { row: PermissionRowData; maxVisible?: number }) {
const { t } = useTranslation(["install", "common"]);
const { icon: Icon, labelKey, summaryKey } = KIND_META[row.kind];
@@ -97,6 +187,8 @@ export function PermissionRow({ row, maxVisible }: { row: PermissionRowData; max
{t(labelKey)}
{row.values.length}
+
+ {row.diff && !isPermissionChanged(row) &&
}
{t(summaryKey)}
diff --git a/src/pages/install/permissions.test.ts b/src/pages/install/permissions.test.ts
index 3484d7691..bdf0ed955 100644
--- a/src/pages/install/permissions.test.ts
+++ b/src/pages/install/permissions.test.ts
@@ -1,6 +1,6 @@
import { describe, it, expect } from "vitest";
import type { SCMetadata } from "@App/app/repo/metadata";
-import { derivePermissions } from "./permissions";
+import { derivePermissions, derivePermissionDiff } from "./permissions";
describe("derivePermissions 权限派生", () => {
it("无任何权限元数据时返回空数组", () => {
@@ -92,3 +92,73 @@ describe("derivePermissions 权限派生", () => {
expect(rows.map((r) => r.risk)).toEqual(["danger", "warn", "normal", "normal"]);
});
});
+
+describe("derivePermissionDiff 权限差异派生", () => {
+ it("新增的取值被标进 diff.added,未变动的不进", () => {
+ const rows = derivePermissionDiff(
+ { connect: ["api.example.com"] },
+ { connect: ["api.example.com", "cdn.example.com"] }
+ );
+ const connect = rows.find((r) => r.kind === "connect")!;
+ expect(connect.diff!.added).toEqual(["cdn.example.com"]);
+ expect(connect.diff!.removed).toEqual([]);
+ });
+
+ it("旧版本有、新版本没有的取值进 diff.removed", () => {
+ const rows = derivePermissionDiff({ grant: ["GM_setValue", "GM_notification"] }, { grant: ["GM_setValue"] });
+ const grant = rows.find((r) => r.kind === "grant")!;
+ expect(grant.diff!.removed).toEqual(["GM_notification"]);
+ expect(grant.values).toEqual(["GM_setValue"]);
+ });
+
+ it("权限完全一致时每行的 diff 增删都为空", () => {
+ const metadata: SCMetadata = { match: ["https://a.com/*"], grant: ["GM_setValue"] };
+ const rows = derivePermissionDiff(metadata, metadata);
+ expect(rows).toHaveLength(2);
+ expect(rows.every((r) => r.diff!.added.length === 0 && r.diff!.removed.length === 0)).toBe(true);
+ });
+
+ it("类别计数取新版本取值,移除项不计入 values", () => {
+ const rows = derivePermissionDiff(
+ { grant: ["GM_setValue", "GM_notification"] },
+ { grant: ["GM_setValue", "GM_cookie"] }
+ );
+ const grant = rows.find((r) => r.kind === "grant")!;
+ expect(grant.values).toHaveLength(2);
+ expect(grant.values).not.toContain("GM_notification");
+ });
+
+ it("类别在新版本被清空但旧版本非空时仍然成行,风险降为 normal", () => {
+ const rows = derivePermissionDiff({ connect: ["*"] }, {});
+ const connect = rows.find((r) => r.kind === "connect")!;
+ expect(connect.values).toEqual([]);
+ expect(connect.diff!.removed).toEqual(["*"]);
+ expect(connect.risk).toBe("normal");
+ });
+
+ it("新旧两版都没有的类别不成行", () => {
+ const rows = derivePermissionDiff({ match: ["https://a.com/*"] }, { match: ["https://a.com/*"] });
+ expect(rows.map((r) => r.kind)).toEqual(["match"]);
+ });
+
+ it("有变动的类别排在未变动的高危类别之前", () => {
+ const rows = derivePermissionDiff(
+ { connect: ["*"], match: ["https://a.com/*"] },
+ { connect: ["*"], match: ["https://a.com/*", "https://b.com/*"] }
+ );
+ expect(rows.map((r) => r.kind)).toEqual(["match", "connect"]);
+ });
+
+ it("同为有变动时仍按风险排序", () => {
+ const rows = derivePermissionDiff({}, { connect: ["*"], match: ["https://a.com/*"] });
+ expect(rows.map((r) => r.kind)).toEqual(["connect", "match"]);
+ });
+
+ it("新增的危险取值仍排在该行取值最前", () => {
+ const rows = derivePermissionDiff({ connect: ["api.example.com"] }, { connect: ["api.example.com", "*"] });
+ const connect = rows.find((r) => r.kind === "connect")!;
+ expect(connect.values[0]).toBe("*");
+ expect(connect.risk).toBe("danger");
+ expect(connect.diff!.added).toEqual(["*"]);
+ });
+});
diff --git a/src/pages/install/permissions.ts b/src/pages/install/permissions.ts
index 70b5c20ce..6962b79c1 100644
--- a/src/pages/install/permissions.ts
+++ b/src/pages/install/permissions.ts
@@ -4,12 +4,20 @@ export type PermissionRisk = "normal" | "warn" | "danger";
export type PermissionKind = "match" | "connect" | "grant" | "require";
+/** 相对已安装版本的取值增删;仅更新场景派生 */
+export interface PermissionDiff {
+ added: string[];
+ removed: string[];
+}
+
export interface PermissionRow {
kind: PermissionKind;
risk: PermissionRisk;
values: string[];
/** values 中被判定为敏感的子集(如 GM_cookie),用于额外高亮 */
sensitive: string[];
+ /** 更新场景才有;全新安装为 undefined,此时整行按「全部都是新的」呈现 */
+ diff?: PermissionDiff;
}
// 需要额外标记的敏感 GM 能力(可访问 Cookie 等隐私数据)
@@ -20,6 +28,9 @@ const RISK_ORDER: Record = {
normal: 2,
};
+// 基础类别顺序:运行网站 → 跨域访问 → GM 能力 → 外部资源
+const KIND_ORDER: PermissionKind[] = ["match", "connect", "grant", "require"];
+
function sortDangerConnectFirst(values: string[]): string[] {
return [...values].sort((a, b) => Number(b === "*") - Number(a === "*"));
}
@@ -28,43 +39,75 @@ function sortSensitiveGrantFirst(values: string[]): string[] {
return [...values].sort((a, b) => Number(SENSITIVE_GRANTS.has(b)) - Number(SENSITIVE_GRANTS.has(a)));
}
+/** 按类别摊平元数据中的权限取值,空类别保留为空数组以便与另一版本对齐比较 */
+function collectValues(metadata: SCMetadata): Record {
+ return {
+ match: [...(metadata.match || []), ...(metadata.include || [])],
+ connect: metadata.connect || [],
+ grant: (metadata.grant || []).filter((g) => g !== "none"),
+ require: [...(metadata.require || []), ...(metadata.resource || [])],
+ };
+}
+
+/** 由单个类别的取值构造权限行,决定行内排序、风险与敏感项 */
+function buildRow(kind: PermissionKind, values: string[]): PermissionRow {
+ // 取值为空意味着该类别什么都不请求,无论原本属于哪一档都不再构成风险
+ if (!values.length) {
+ return { kind, risk: "normal", values: [], sensitive: [] };
+ }
+ switch (kind) {
+ case "connect": {
+ const sorted = sortDangerConnectFirst(values);
+ return { kind, risk: sorted.includes("*") ? "danger" : "warn", values: sorted, sensitive: [] };
+ }
+ case "grant": {
+ const sorted = sortSensitiveGrantFirst(values);
+ return { kind, risk: "warn", values: sorted, sensitive: sorted.filter((g) => SENSITIVE_GRANTS.has(g)) };
+ }
+ default:
+ return { kind, risk: "normal", values, sensitive: [] };
+ }
+}
+
+/** 该行相对已安装版本是否发生了增删;全新安装(无 diff)恒为 false */
+export const isPermissionChanged = (row: PermissionRow) =>
+ !!row.diff && (row.diff.added.length > 0 || row.diff.removed.length > 0);
+
/**
* 把脚本元数据派生为「权限行」,作为安装页信任决策的核心呈现。
- * 高危/告警权限优先展示,同风险内保持基础类别顺序:运行网站 → 跨域访问 → GM 能力 → 外部资源。
+ * 高危/告警权限优先展示,同风险内保持基础类别顺序。
*/
export function derivePermissions(metadata: SCMetadata): PermissionRow[] {
- const rows: PermissionRow[] = [];
-
- const match = [...(metadata.match || []), ...(metadata.include || [])];
- if (match.length) {
- rows.push({ kind: "match", risk: "normal", values: match, sensitive: [] });
- }
-
- const connect = metadata.connect || [];
- if (connect.length) {
- const values = sortDangerConnectFirst(connect);
- rows.push({
- kind: "connect",
- risk: values.includes("*") ? "danger" : "warn",
- values,
- sensitive: [],
- });
- }
+ const values = collectValues(metadata);
+ return KIND_ORDER.filter((kind) => values[kind].length)
+ .map((kind) => buildRow(kind, values[kind]))
+ .sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk]);
+}
- const grant = sortSensitiveGrantFirst((metadata.grant || []).filter((g) => g !== "none"));
- if (grant.length) {
- rows.push({
- kind: "grant",
- risk: "warn",
- values: grant,
- sensitive: grant.filter((g) => SENSITIVE_GRANTS.has(g)),
- });
- }
+/**
+ * 以已安装脚本为基线派生权限行,逐取值标注新增与移除。
+ * 排序键为「有变动 → 风险 → 基础类别顺序」:更新页首先要回答的是这次变了什么,
+ * 未变动的高危项是用户上次安装时已经确认过的内容。
+ * 某类别在新版本被清空但旧版本非空时仍然成行,否则「不再请求网络访问」这类变化会整行消失。
+ */
+export function derivePermissionDiff(oldMetadata: SCMetadata, newMetadata: SCMetadata): PermissionRow[] {
+ const oldValues = collectValues(oldMetadata);
+ const newValues = collectValues(newMetadata);
+ const rows: PermissionRow[] = [];
- const require = [...(metadata.require || []), ...(metadata.resource || [])];
- if (require.length) {
- rows.push({ kind: "require", risk: "normal", values: require, sensitive: [] });
+ for (const kind of KIND_ORDER) {
+ const next = newValues[kind];
+ const prev = oldValues[kind];
+ const nextSet = new Set(next);
+ const prevSet = new Set(prev);
+ const removed = prev.filter((v) => !nextSet.has(v));
+ if (!next.length && !removed.length) continue;
+ const row = buildRow(kind, next);
+ row.diff = { added: row.values.filter((v) => !prevSet.has(v)), removed };
+ rows.push(row);
}
- return rows.sort((a, b) => RISK_ORDER[a.risk] - RISK_ORDER[b.risk]);
+ return rows.sort(
+ (a, b) => Number(isPermissionChanged(b)) - Number(isPermissionChanged(a)) || RISK_ORDER[a.risk] - RISK_ORDER[b.risk]
+ );
}
diff --git a/src/pages/install/useInstallData.test.ts b/src/pages/install/useInstallData.test.ts
index 99a28eea6..caee21fd2 100644
--- a/src/pages/install/useInstallData.test.ts
+++ b/src/pages/install/useInstallData.test.ts
@@ -284,6 +284,42 @@ describe("useInstallData 数据流编排", () => {
expect(state.view.oldCode).toBe("// old code");
});
+ it("uuid 更新时以已安装脚本的 metadata 为基线派生权限差异", async () => {
+ window.history.replaceState({}, "", "/install.html?uuid=u1");
+ const metadata = { name: ["示例脚本"], version: ["2.0.0"], connect: ["api.e.com", "cdn.e.com"] };
+ const oldMetadata = { name: ["示例脚本"], version: ["1.0.0"], connect: ["api.e.com"] };
+ const info: ScriptInfo = {
+ url: "https://e.com/x.user.js",
+ code: "",
+ uuid: "u1",
+ userSubscribe: false,
+ metadata,
+ source: "user",
+ };
+ (scriptClient.getInstallInfo as Mock).mockResolvedValue([true, info, {}]);
+ (getTempCode as Mock).mockResolvedValue("// new code");
+ (prepareScriptByCode as Mock).mockResolvedValue({
+ script: { name: "示例脚本", metadata, status: SCRIPT_STATUS_ENABLE } as unknown as Script,
+ oldScript: { metadata: oldMetadata } as unknown as Script,
+ oldScriptCode: "// old code",
+ });
+
+ const { result } = renderHook(() => useInstallData());
+ await waitFor(() => expect(result.current.state.status).toBe("ready"));
+ const state = result.current.state;
+ if (state.status !== "ready") throw new Error("not ready");
+ const connect = state.view.permissions.find((r) => r.kind === "connect")!;
+ expect(connect.diff).toEqual({ added: ["cdn.e.com"], removed: [] });
+ });
+
+ it("全新安装时权限行不携带差异", async () => {
+ const result = await setupReady();
+ const state = result.current.state;
+ if (state.status !== "ready") throw new Error("not ready");
+ expect(state.view.isUpdate).toBe(false);
+ expect(state.view.permissions.every((r) => r.diff === undefined)).toBe(true);
+ });
+
describe("安装成功后离开安装页:独立新标签应关闭,网页链接接管的原标签应返回上一页", () => {
it("独立新标签 history.length 为 1 时应使用 window.close() 关闭", async () => {
const result = await setupReady();
diff --git a/src/pages/install/useInstallData.ts b/src/pages/install/useInstallData.ts
index 080341ab2..64683a847 100644
--- a/src/pages/install/useInstallData.ts
+++ b/src/pages/install/useInstallData.ts
@@ -18,7 +18,7 @@ import { loadHandle } from "@App/pkg/utils/filehandle-db";
import { startFileTrack, unmountFileTrack, type FTInfo } from "@App/pkg/utils/file-tracker";
import { TempStorageDAO } from "@App/app/repo/tempStorage";
import { EnableAgent } from "@App/app/const";
-import { derivePermissions, type PermissionRow } from "./permissions";
+import { derivePermissions, derivePermissionDiff, type PermissionRow } from "./permissions";
import {
deriveVersion,
deriveAntifeatures,
@@ -68,8 +68,10 @@ export function assembleInstallView(args: {
code: string;
oldVersion: string | null;
oldCode?: string;
+ /** 已安装脚本的元数据,作为权限差异的基线;全新安装与订阅为 undefined */
+ oldMetadata?: SCMetadata;
}): InstallView {
- const { isUpdate, inTrash, scriptInfo, action, code, oldVersion, oldCode } = args;
+ const { isUpdate, inTrash, scriptInfo, action, code, oldVersion, oldCode, oldMetadata } = args;
const metadata = scriptInfo.metadata;
const schedule = deriveScheduleInfo(metadata);
return {
@@ -82,7 +84,7 @@ export function assembleInstallView(args: {
source: prettyUrl(scriptInfo.url),
description: i18nDescription(action),
version: deriveVersion(metadata.version?.[0], oldVersion),
- permissions: derivePermissions(metadata),
+ permissions: oldMetadata ? derivePermissionDiff(oldMetadata, metadata) : derivePermissions(metadata),
antifeatures: deriveAntifeatures(metadata),
schedule,
scheduleNextRun: schedule?.kind === "cron" ? nextTimeDisplay(schedule.expression) : undefined,
@@ -253,6 +255,7 @@ export function useInstallData(): UseInstallData {
let action: Script | Subscribe;
let oldVersion: string | null;
let oldCode: string | undefined;
+ let oldMetadata: SCMetadata | undefined;
let inTrash = false;
if (info.userSubscribe) {
const p = await prepareSubscribeByCode(code, info.url);
@@ -271,6 +274,7 @@ export function useInstallData(): UseInstallData {
action = p.script;
oldVersion = versionOf(p.oldScript);
oldCode = p.oldScriptCode;
+ oldMetadata = p.oldScript?.metadata;
inTrash = p.oldInTrash === true;
}
if (cancelled) return;
@@ -288,6 +292,7 @@ export function useInstallData(): UseInstallData {
code,
oldVersion,
oldCode,
+ oldMetadata,
}),
});
};