feat(contribution-update): 添加贡献更新提示逻辑和确认功能
This commit is contained in:
+68
-3
@@ -2369,11 +2369,70 @@ async function initializeReleaseInfo() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getContributionUpdateHintMessage(snapshot = currentContributionContentSnapshot) {
|
function getContributionUpdateHintMessage(snapshot = currentContributionContentSnapshot) {
|
||||||
if (!snapshot?.promptVersion) {
|
const lines = getContributionUpdatePromptLines(snapshot);
|
||||||
|
if (!lines.length) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
if (lines.length === 1) {
|
||||||
|
return lines[0];
|
||||||
|
}
|
||||||
|
return lines.map((line, index) => `${index + 1}. ${line}`).join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
return '公告 / 使用教程有更新了,可点上方“贡献/使用”查看。';
|
function getContributionUpdatePromptLines(snapshot = currentContributionContentSnapshot) {
|
||||||
|
if (!snapshot?.promptVersion) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = Array.isArray(snapshot.items) ? snapshot.items : [];
|
||||||
|
const hasAnnouncementOrTutorial = items.some((item) =>
|
||||||
|
item
|
||||||
|
&& item.isVisible
|
||||||
|
&& ['announcement', 'tutorial'].includes(String(item.slug || '').trim().toLowerCase())
|
||||||
|
);
|
||||||
|
const hasQuestionnaire = items.some((item) =>
|
||||||
|
item
|
||||||
|
&& item.isVisible
|
||||||
|
&& String(item.slug || '').trim().toLowerCase() === 'questionnaire'
|
||||||
|
);
|
||||||
|
|
||||||
|
const lines = [];
|
||||||
|
if (hasAnnouncementOrTutorial) {
|
||||||
|
lines.push('公告 / 使用教程有更新了,可点上方“贡献/使用”查看。');
|
||||||
|
}
|
||||||
|
if (hasQuestionnaire) {
|
||||||
|
lines.push('有新的征求意见,请佬友共同参与选择。');
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPromptContributionUpdateBeforeAutoRun(snapshot = currentContributionContentSnapshot) {
|
||||||
|
const promptVersion = String(snapshot?.promptVersion || '').trim();
|
||||||
|
if (!promptVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (promptVersion === getDismissedContributionContentPromptVersion()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return getContributionUpdatePromptLines(snapshot).length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeConfirmContributionUpdateBeforeAutoRun(snapshot = currentContributionContentSnapshot) {
|
||||||
|
if (!shouldPromptContributionUpdateBeforeAutoRun(snapshot)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmed = await openConfirmModal({
|
||||||
|
title: '自动前提醒',
|
||||||
|
message: getContributionUpdateHintMessage(snapshot),
|
||||||
|
confirmLabel: '确定继续',
|
||||||
|
confirmVariant: 'btn-primary',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
dismissContributionUpdateHint();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function positionContributionUpdateHint() {
|
function positionContributionUpdateHint() {
|
||||||
@@ -3982,12 +4041,18 @@ autoStartModal?.addEventListener('click', (event) => {
|
|||||||
btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
|
btnAutoStartClose?.addEventListener('click', () => resolveModalChoice(null));
|
||||||
|
|
||||||
async function startAutoRunFromCurrentSettings() {
|
async function startAutoRunFromCurrentSettings() {
|
||||||
|
let contributionSnapshot = null;
|
||||||
try {
|
try {
|
||||||
await refreshContributionContentHint();
|
contributionSnapshot = await refreshContributionContentHint();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Failed to refresh contribution content hint before auto run:', error);
|
console.warn('Failed to refresh contribution content hint before auto run:', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const confirmedContributionUpdate = await maybeConfirmContributionUpdateBeforeAutoRun(contributionSnapshot);
|
||||||
|
if (!confirmedContributionUpdate) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const totalRuns = getRunCountValue();
|
const totalRuns = getRunCountValue();
|
||||||
let mode = 'restart';
|
let mode = 'restart';
|
||||||
const autoRunSkipFailures = inputAutoSkipFailures.checked;
|
const autoRunSkipFailures = inputAutoSkipFailures.checked;
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ function extractFunction(name) {
|
|||||||
return sidepanelSource.slice(start, end);
|
return sidepanelSource.slice(start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createApi({ refreshImpl } = {}) {
|
function createApi({ refreshImpl, confirmImpl, dismissImpl } = {}) {
|
||||||
const bundle = extractFunction('startAutoRunFromCurrentSettings');
|
const bundle = extractFunction('startAutoRunFromCurrentSettings');
|
||||||
|
|
||||||
return new Function(`
|
return new Function(`
|
||||||
@@ -90,6 +90,14 @@ async function refreshContributionContentHint() {
|
|||||||
events.push({ type: 'refresh' });
|
events.push({ type: 'refresh' });
|
||||||
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
|
${refreshImpl ? 'return (' + refreshImpl + ')();' : 'return null;'}
|
||||||
}
|
}
|
||||||
|
async function maybeConfirmContributionUpdateBeforeAutoRun(snapshot) {
|
||||||
|
events.push({ type: 'confirm-check', snapshot });
|
||||||
|
${confirmImpl ? 'return (' + confirmImpl + ')(snapshot);' : 'return true;'}
|
||||||
|
}
|
||||||
|
function dismissContributionUpdateHint() {
|
||||||
|
events.push({ type: 'dismiss-hint' });
|
||||||
|
${dismissImpl ? '(' + dismissImpl + ')();' : ''}
|
||||||
|
}
|
||||||
${bundle}
|
${bundle}
|
||||||
return {
|
return {
|
||||||
startAutoRunFromCurrentSettings,
|
startAutoRunFromCurrentSettings,
|
||||||
@@ -108,9 +116,9 @@ test('startAutoRunFromCurrentSettings refreshes contribution content hint before
|
|||||||
assert.equal(result, true);
|
assert.equal(result, true);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
api.getEvents().map((entry) => entry.type),
|
api.getEvents().map((entry) => entry.type),
|
||||||
['refresh', 'send']
|
['refresh', 'confirm-check', 'send']
|
||||||
);
|
);
|
||||||
assert.equal(api.getEvents()[1].message.type, 'AUTO_RUN');
|
assert.equal(api.getEvents()[2].message.type, 'AUTO_RUN');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('startAutoRunFromCurrentSettings continues auto run when contribution content refresh fails', async () => {
|
test('startAutoRunFromCurrentSettings continues auto run when contribution content refresh fails', async () => {
|
||||||
@@ -124,8 +132,26 @@ test('startAutoRunFromCurrentSettings continues auto run when contribution conte
|
|||||||
assert.equal(result, true);
|
assert.equal(result, true);
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
events.map((entry) => entry.type),
|
events.map((entry) => entry.type),
|
||||||
['refresh', 'warn', 'send']
|
['refresh', 'warn', 'confirm-check', 'send']
|
||||||
);
|
);
|
||||||
assert.match(String(events[1].args[0]), /Failed to refresh contribution content hint before auto run/);
|
assert.match(String(events[1].args[0]), /Failed to refresh contribution content hint before auto run/);
|
||||||
assert.equal(events[2].message.type, 'AUTO_RUN');
|
assert.equal(events[3].message.type, 'AUTO_RUN');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('startAutoRunFromCurrentSettings aborts when contribution update confirmation is declined', async () => {
|
||||||
|
const api = createApi({
|
||||||
|
refreshImpl: `async () => ({
|
||||||
|
promptVersion: 'questionnaire:2026-04-23T00:00:00Z',
|
||||||
|
items: [{ slug: 'questionnaire', isVisible: true }],
|
||||||
|
})`,
|
||||||
|
confirmImpl: 'async () => false',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await api.startAutoRunFromCurrentSettings();
|
||||||
|
|
||||||
|
assert.equal(result, false);
|
||||||
|
assert.deepEqual(
|
||||||
|
api.getEvents().map((entry) => entry.type),
|
||||||
|
['refresh', 'confirm-check']
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const test = require('node:test');
|
||||||
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
|
|
||||||
|
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
|
||||||
|
|
||||||
|
function extractFunction(name) {
|
||||||
|
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||||
|
const start = markers
|
||||||
|
.map((marker) => sidepanelSource.indexOf(marker))
|
||||||
|
.find((index) => index >= 0);
|
||||||
|
if (start < 0) {
|
||||||
|
throw new Error(`missing function ${name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parenDepth = 0;
|
||||||
|
let signatureEnded = false;
|
||||||
|
let braceStart = -1;
|
||||||
|
for (let i = start; i < sidepanelSource.length; i += 1) {
|
||||||
|
const ch = sidepanelSource[i];
|
||||||
|
if (ch === '(') {
|
||||||
|
parenDepth += 1;
|
||||||
|
} else if (ch === ')') {
|
||||||
|
parenDepth -= 1;
|
||||||
|
if (parenDepth === 0) {
|
||||||
|
signatureEnded = true;
|
||||||
|
}
|
||||||
|
} else if (ch === '{' && signatureEnded) {
|
||||||
|
braceStart = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let depth = 0;
|
||||||
|
let end = braceStart;
|
||||||
|
for (; end < sidepanelSource.length; end += 1) {
|
||||||
|
const ch = sidepanelSource[end];
|
||||||
|
if (ch === '{') depth += 1;
|
||||||
|
if (ch === '}') {
|
||||||
|
depth -= 1;
|
||||||
|
if (depth === 0) {
|
||||||
|
end += 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return sidepanelSource.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
const helperBundle = [
|
||||||
|
extractFunction('getContributionUpdatePromptLines'),
|
||||||
|
extractFunction('getContributionUpdateHintMessage'),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const api = new Function(`
|
||||||
|
${helperBundle}
|
||||||
|
return {
|
||||||
|
getContributionUpdatePromptLines,
|
||||||
|
getContributionUpdateHintMessage,
|
||||||
|
};
|
||||||
|
`)();
|
||||||
|
|
||||||
|
test('getContributionUpdateHintMessage numbers contribution updates when both content and questionnaire are visible', () => {
|
||||||
|
const message = api.getContributionUpdateHintMessage({
|
||||||
|
promptVersion: 'announcement:2026-04-23T00:00:00Z|questionnaire:2026-04-23T00:00:01Z',
|
||||||
|
items: [
|
||||||
|
{ slug: 'announcement', isVisible: true },
|
||||||
|
{ slug: 'questionnaire', isVisible: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
message,
|
||||||
|
'1. 公告 / 使用教程有更新了,可点上方“贡献/使用”查看。\n2. 有新的征求意见,请佬友共同参与选择。'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getContributionUpdateHintMessage returns questionnaire prompt alone when only questionnaire is updated', () => {
|
||||||
|
const message = api.getContributionUpdateHintMessage({
|
||||||
|
promptVersion: 'questionnaire:2026-04-23T00:00:01Z',
|
||||||
|
items: [
|
||||||
|
{ slug: 'questionnaire', isVisible: true },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(message, '有新的征求意见,请佬友共同参与选择。');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user