镜像自地址
https://github.com/lhly/cloud-drive-renamer.git
已同步 2026-10-21 14:04:01 +08:00
feat: 增加失效规则模板保护
这个提交包含在:
父节点
653cd3fc1b
当前提交
4df5375b92
@ -11,6 +11,7 @@ import {
|
|||||||
type RulePresetRecord,
|
type RulePresetRecord,
|
||||||
} from '../../types/rule-preset';
|
} from '../../types/rule-preset';
|
||||||
import { I18nService } from '../../utils/i18n';
|
import { I18nService } from '../../utils/i18n';
|
||||||
|
import { buildRulePresetSummary, isRulePresetConfigValid } from '../../utils/rule-presets';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Config Panel Component
|
* Config Panel Component
|
||||||
@ -225,6 +226,10 @@ export class ConfigPanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private handleApplyPreset(record: RulePresetRecord): void {
|
private handleApplyPreset(record: RulePresetRecord): void {
|
||||||
|
if (!isRulePresetConfigValid(record.config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.dispatchEvent(
|
this.dispatchEvent(
|
||||||
new CustomEvent('apply-rule-preset', {
|
new CustomEvent('apply-rule-preset', {
|
||||||
detail: { presetId: record.id, source: record.source },
|
detail: { presetId: record.id, source: record.source },
|
||||||
@ -818,16 +823,26 @@ export class ConfigPanel extends LitElement {
|
|||||||
|
|
||||||
private renderPresetItem(record: RulePresetRecord, deletable: boolean) {
|
private renderPresetItem(record: RulePresetRecord, deletable: boolean) {
|
||||||
const summary = this.buildPresetSummary(record);
|
const summary = this.buildPresetSummary(record);
|
||||||
|
const isValid = isRulePresetConfigValid(record.config);
|
||||||
return html`
|
return html`
|
||||||
<div class="preset-item" data-preset-id=${record.id}>
|
<div class="preset-item ${isValid ? '' : 'is-invalid'}" data-preset-id=${record.id}>
|
||||||
<div class="preset-content">
|
<div class="preset-content">
|
||||||
<div class="preset-name">${record.name || summary}</div>
|
<div class="preset-name">
|
||||||
|
${record.name || summary}
|
||||||
|
${isValid
|
||||||
|
? ''
|
||||||
|
: html`<span class="preset-invalid-badge">${I18nService.t('rule_preset_invalid_badge')}</span>`}
|
||||||
|
</div>
|
||||||
<div class="preset-summary">${summary}</div>
|
<div class="preset-summary">${summary}</div>
|
||||||
|
${isValid
|
||||||
|
? ''
|
||||||
|
: html`<div class="preset-invalid-hint">${I18nService.t('rule_preset_invalid_hint')}</div>`}
|
||||||
</div>
|
</div>
|
||||||
<div class="preset-actions">
|
<div class="preset-actions">
|
||||||
<button
|
<button
|
||||||
class="text-action-button"
|
class="text-action-button"
|
||||||
data-role="apply-rule-preset"
|
data-role="apply-rule-preset"
|
||||||
|
?disabled=${!isValid}
|
||||||
@click=${() => this.handleApplyPreset(record)}
|
@click=${() => this.handleApplyPreset(record)}
|
||||||
>
|
>
|
||||||
${I18nService.t('rule_preset_apply_button')}
|
${I18nService.t('rule_preset_apply_button')}
|
||||||
@ -849,24 +864,7 @@ export class ConfigPanel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private buildPresetSummary(record: RulePresetRecord): string {
|
private buildPresetSummary(record: RulePresetRecord): string {
|
||||||
switch (record.config.type) {
|
return buildRulePresetSummary(record.config);
|
||||||
case 'replace':
|
|
||||||
return `${I18nService.t('rule_replace')}: ${String(record.config.params.search || '')} → ${String(record.config.params.replace || '')}`;
|
|
||||||
case 'regex':
|
|
||||||
return `${I18nService.t('rule_regex')}: ${String(record.config.params.pattern || '')}`;
|
|
||||||
case 'prefix':
|
|
||||||
return `${I18nService.t('rule_prefix')}: ${String(record.config.params.prefix || '')}`;
|
|
||||||
case 'suffix':
|
|
||||||
return `${I18nService.t('rule_suffix')}: ${String(record.config.params.suffix || '')}`;
|
|
||||||
case 'numbering':
|
|
||||||
return `${I18nService.t('rule_numbering')}: ${String(record.config.params.format || '{num}')}`;
|
|
||||||
case 'sanitize':
|
|
||||||
return `${I18nService.t('rule_sanitize')}: ${String(record.config.params.removeChars || I18nService.t('param_remove_illegal'))}`;
|
|
||||||
case 'episodeExtract':
|
|
||||||
return `${I18nService.t('rule_episode_extract')}: ${String(record.config.params.template || '{prefix}.S{season}E{episode}{ext}')}`;
|
|
||||||
default:
|
|
||||||
return record.config.type;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderEpisodeExtractAssist() {
|
private renderEpisodeExtractAssist() {
|
||||||
@ -1697,14 +1695,32 @@ export class ConfigPanel extends LitElement {
|
|||||||
border-top: 1px solid var(--cdr-border, #f0f0f0);
|
border-top: 1px solid var(--cdr-border, #f0f0f0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preset-item.is-invalid {
|
||||||
|
opacity: 0.88;
|
||||||
|
}
|
||||||
|
|
||||||
.preset-summary,
|
.preset-summary,
|
||||||
.preset-empty,
|
.preset-empty,
|
||||||
.assist-sample-name,
|
.assist-sample-name,
|
||||||
.assist-warning {
|
.assist-warning,
|
||||||
|
.preset-invalid-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--cdr-text-secondary, #595959);
|
color: var(--cdr-text-secondary, #595959);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.preset-invalid-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--cdr-warning-bg, #fff7e6);
|
||||||
|
border: 1px solid var(--cdr-warning-border, #ffd591);
|
||||||
|
color: var(--cdr-warning-text, #fa8c16);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.assist-sample-name {
|
.assist-sample-name {
|
||||||
margin: 6px 0 10px;
|
margin: 6px 0 10px;
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
|
|||||||
@ -51,6 +51,7 @@ import {
|
|||||||
getRulePresetById,
|
getRulePresetById,
|
||||||
getRecentRulePresets,
|
getRecentRulePresets,
|
||||||
getTemplateRulePresets,
|
getTemplateRulePresets,
|
||||||
|
isRulePresetConfigValid,
|
||||||
recordRecentRulePreset,
|
recordRecentRulePreset,
|
||||||
saveTemplateRulePreset,
|
saveTemplateRulePreset,
|
||||||
} from '../../utils/rule-presets';
|
} from '../../utils/rule-presets';
|
||||||
@ -623,6 +624,11 @@ export class FileSelectorPanel extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isRulePresetConfigValid(record.config)) {
|
||||||
|
alert(I18nService.t('rule_preset_invalid'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.ruleConfig = record.config;
|
this.ruleConfig = record.config;
|
||||||
this.episodeExtractAssistState = {
|
this.episodeExtractAssistState = {
|
||||||
...DEFAULT_EPISODE_EXTRACT_ASSIST_STATE,
|
...DEFAULT_EPISODE_EXTRACT_ASSIST_STATE,
|
||||||
|
|||||||
@ -237,5 +237,8 @@
|
|||||||
"episode_assist_apply_full_name": "Fill current field with full text",
|
"episode_assist_apply_full_name": "Fill current field with full text",
|
||||||
"episode_assist_use_item_as_sample": "Use this item as sample",
|
"episode_assist_use_item_as_sample": "Use this item as sample",
|
||||||
"episode_assist_use_prefix_from_item": "Try using this as prefix",
|
"episode_assist_use_prefix_from_item": "Try using this as prefix",
|
||||||
"episode_assist_focus_segment_mode": "Switch to segment mode"
|
"episode_assist_focus_segment_mode": "Switch to segment mode",
|
||||||
|
"rule_preset_invalid": "This template is incompatible with the current version. Please delete and save it again.",
|
||||||
|
"rule_preset_invalid_badge": "Invalid",
|
||||||
|
"rule_preset_invalid_hint": "This template config is no longer valid and cannot be applied."
|
||||||
}
|
}
|
||||||
|
|||||||
@ -237,5 +237,8 @@
|
|||||||
"episode_assist_apply_full_name": "整段回填到当前字段",
|
"episode_assist_apply_full_name": "整段回填到当前字段",
|
||||||
"episode_assist_use_item_as_sample": "将该项设为样本",
|
"episode_assist_use_item_as_sample": "将该项设为样本",
|
||||||
"episode_assist_use_prefix_from_item": "尝试提取剧名到 prefix",
|
"episode_assist_use_prefix_from_item": "尝试提取剧名到 prefix",
|
||||||
"episode_assist_focus_segment_mode": "切换到片段点选模式"
|
"episode_assist_focus_segment_mode": "切换到片段点选模式",
|
||||||
|
"rule_preset_invalid": "该模板与当前版本不兼容,请删除后重新保存。",
|
||||||
|
"rule_preset_invalid_badge": "已失效",
|
||||||
|
"rule_preset_invalid_hint": "该模板配置已失效,暂时无法应用。"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -237,5 +237,8 @@
|
|||||||
"episode_assist_apply_full_name": "整段回填到目前欄位",
|
"episode_assist_apply_full_name": "整段回填到目前欄位",
|
||||||
"episode_assist_use_item_as_sample": "將此項設為樣本",
|
"episode_assist_use_item_as_sample": "將此項設為樣本",
|
||||||
"episode_assist_use_prefix_from_item": "嘗試提取劇名到 prefix",
|
"episode_assist_use_prefix_from_item": "嘗試提取劇名到 prefix",
|
||||||
"episode_assist_focus_segment_mode": "切換到片段點選模式"
|
"episode_assist_focus_segment_mode": "切換到片段點選模式",
|
||||||
|
"rule_preset_invalid": "此模板與目前版本不相容,請刪除後重新儲存。",
|
||||||
|
"rule_preset_invalid_badge": "已失效",
|
||||||
|
"rule_preset_invalid_hint": "此模板設定已失效,暫時無法套用。"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,37 @@ describe('ConfigPanel rule preset interactions', () => {
|
|||||||
vi.stubGlobal('prompt', vi.fn(() => '剧集模板'));
|
vi.stubGlobal('prompt', vi.fn(() => '剧集模板'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('marks invalid presets as disabled and does not dispatch apply events', async () => {
|
||||||
|
const panel = new ConfigPanel();
|
||||||
|
panel.recentRulePresets = [
|
||||||
|
createPreset({
|
||||||
|
id: 'invalid-1',
|
||||||
|
source: 'recent',
|
||||||
|
name: '失效模板',
|
||||||
|
config: { type: 'replace', params: { search: '', replace: '' } },
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const applyEvents: Array<Event> = [];
|
||||||
|
panel.addEventListener('apply-rule-preset', (event) => applyEvents.push(event));
|
||||||
|
|
||||||
|
document.body.appendChild(panel);
|
||||||
|
await panel.updateComplete;
|
||||||
|
|
||||||
|
const applyButton = panel.shadowRoot?.querySelector<HTMLButtonElement>(
|
||||||
|
'[data-preset-id="invalid-1"] [data-role="apply-rule-preset"]'
|
||||||
|
);
|
||||||
|
const badgeText = panel.shadowRoot?.querySelector('[data-preset-id="invalid-1"] .preset-invalid-badge')?.textContent ?? '';
|
||||||
|
|
||||||
|
expect(applyButton?.disabled).toBe(true);
|
||||||
|
expect(badgeText).toContain('失效');
|
||||||
|
|
||||||
|
applyButton?.dispatchEvent(new MouseEvent('click', { bubbles: true, composed: true }));
|
||||||
|
expect(applyEvents).toHaveLength(0);
|
||||||
|
|
||||||
|
panel.remove();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders recent/templates and dispatches apply, save, delete events', async () => {
|
it('renders recent/templates and dispatches apply, save, delete events', async () => {
|
||||||
const panel = new ConfigPanel();
|
const panel = new ConfigPanel();
|
||||||
panel.activeRuleConfig = {
|
panel.activeRuleConfig = {
|
||||||
|
|||||||
@ -132,6 +132,34 @@ describe('FileSelectorPanel rule preset integration', () => {
|
|||||||
expect(panel.episodeExtractAssistState.fillMode).toBe('full-name');
|
expect(panel.episodeExtractAssistState.fillMode).toBe('full-name');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('blocks invalid preset application and shows alert', async () => {
|
||||||
|
const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness;
|
||||||
|
const alertSpy = vi.spyOn(window, 'alert');
|
||||||
|
vi.spyOn(rulePresets, 'getRulePresetById').mockResolvedValue({
|
||||||
|
id: 'invalid-template',
|
||||||
|
name: '失效模板',
|
||||||
|
source: 'template',
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1,
|
||||||
|
config: {
|
||||||
|
type: 'replace',
|
||||||
|
params: { search: '', replace: '' },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.ruleConfig = {
|
||||||
|
type: 'prefix',
|
||||||
|
params: { prefix: 'Keep', separator: '.' },
|
||||||
|
};
|
||||||
|
|
||||||
|
await panel.handleApplyRulePreset(new CustomEvent('apply-rule-preset', { detail: { presetId: 'invalid-template' } }));
|
||||||
|
|
||||||
|
expect(alertSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(panel.ruleConfig.type).toBe('prefix');
|
||||||
|
expect(panel.ruleConfig.params.prefix).toBe('Keep');
|
||||||
|
});
|
||||||
|
|
||||||
it('records recent presets after a successful execution', async () => {
|
it('records recent presets after a successful execution', async () => {
|
||||||
const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness;
|
const panel = new FileSelectorPanel() as FileSelectorPanelPresetHarness;
|
||||||
panel.adapter = new PresetTestAdapter();
|
panel.adapter = new PresetTestAdapter();
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户