mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: show the rules again after saving a script the visual builder can read from the raw Sieve editor
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest';
|
||||
import type { FilterRule } from '@/lib/jmap/sieve-types';
|
||||
import { generateScript } from '@/lib/sieve/generator';
|
||||
import { mockStalwartAccount } from '@/lib/filters/__tests__/sieve-mock';
|
||||
import { useFilterStore } from '../filter-store';
|
||||
|
||||
// After a save from the raw Sieve editor the page reads the script the way
|
||||
// a page load does: a script the builder understands shows its rules again,
|
||||
// rather than "edited outside the visual builder" until the next visit.
|
||||
|
||||
const EXTENSIONS = ['fileinto', 'mailbox', 'imap4flags'];
|
||||
|
||||
const rule: FilterRule = {
|
||||
id: 'r1',
|
||||
name: 'Newsletters',
|
||||
enabled: true,
|
||||
matchType: 'all',
|
||||
conditions: [{ field: 'from', comparator: 'contains', value: 'news@example.com' }],
|
||||
actions: [{ type: 'mark_read' }],
|
||||
stopProcessing: false,
|
||||
};
|
||||
|
||||
/** What the settings page does when the raw editor saves. */
|
||||
async function saveRaw(client: never, content: string) {
|
||||
useFilterStore.getState().setRawScript(content);
|
||||
useFilterStore.setState({ isOpaque: true, rules: [] });
|
||||
await useFilterStore.getState().saveFilters(client);
|
||||
}
|
||||
|
||||
afterEach(() => useFilterStore.getState().clearState());
|
||||
|
||||
describe('saving from the raw Sieve editor', () => {
|
||||
it('shows the rules of a script the builder understands, as a fresh load does', async () => {
|
||||
const script = generateScript([rule], undefined, { extensions: EXTENSIONS });
|
||||
const server = mockStalwartAccount('a', [{ name: 'filters', content: script, isActive: true }], EXTENSIONS);
|
||||
await useFilterStore.getState().fetchFilters(server.client as never, 'a');
|
||||
expect(useFilterStore.getState().rules).toHaveLength(1);
|
||||
|
||||
await saveRaw(server.client as never, script);
|
||||
|
||||
const after = useFilterStore.getState();
|
||||
expect(after.isOpaque).toBe(false);
|
||||
expect(after.rules.map((r) => r.name)).toEqual(['Newsletters']);
|
||||
|
||||
// The same as a fresh visit to the page.
|
||||
await useFilterStore.getState().fetchFilters(server.client as never, 'a');
|
||||
expect(useFilterStore.getState().isOpaque).toBe(after.isOpaque);
|
||||
expect(useFilterStore.getState().rules.map((r) => r.name)).toEqual(['Newsletters']);
|
||||
});
|
||||
|
||||
it('stays in raw mode for a script the builder cannot read', async () => {
|
||||
const server = mockStalwartAccount('a', [{ name: 'filters', content: 'keep;', isActive: true }], EXTENSIONS);
|
||||
await useFilterStore.getState().fetchFilters(server.client as never, 'a');
|
||||
|
||||
await saveRaw(server.client as never, '/* @metadata:begin\n{corrupt\n@metadata:end */');
|
||||
|
||||
expect(useFilterStore.getState().isOpaque).toBe(true);
|
||||
expect(useFilterStore.getState().rules).toEqual([]);
|
||||
});
|
||||
});
|
||||
+36
-29
@@ -93,6 +93,30 @@ interface FilterStore {
|
||||
*/
|
||||
let fetchGeneration = 0;
|
||||
|
||||
/** What the filter view holds for a parsed script, on load and after a raw save. */
|
||||
function stateFromScript(result: ReturnType<typeof parseScript>, vacationActive: boolean) {
|
||||
if (result.isOpaque) {
|
||||
return {
|
||||
isOpaque: true,
|
||||
rules: [] as FilterRule[],
|
||||
vacationSettings: result.vacation || null,
|
||||
externalRequires: result.externalRequires,
|
||||
includeVacation: false,
|
||||
vacationForward: null,
|
||||
vacationAudience: null,
|
||||
};
|
||||
}
|
||||
return {
|
||||
isOpaque: false,
|
||||
rules: result.rules,
|
||||
vacationSettings: result.vacation || null,
|
||||
externalRequires: result.externalRequires,
|
||||
includeVacation: !!result.includeVacation || vacationActive,
|
||||
vacationForward: result.vacationForward ?? null,
|
||||
vacationAudience: result.vacationAudience ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
rules: [],
|
||||
isLoading: false,
|
||||
@@ -163,34 +187,10 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
set({ rawScript: content });
|
||||
|
||||
const result = parseScript(content);
|
||||
|
||||
if (result.isOpaque) {
|
||||
debug.log('filters', 'Sieve script is opaque (hand-edited)');
|
||||
emitFiltersLoaded([]);
|
||||
set({
|
||||
isLoading: false,
|
||||
isOpaque: true,
|
||||
rules: [],
|
||||
vacationSettings: result.vacation || null,
|
||||
externalRequires: result.externalRequires,
|
||||
includeVacation: false,
|
||||
vacationForward: null,
|
||||
vacationAudience: null,
|
||||
});
|
||||
} else {
|
||||
debug.log('filters', 'Parsed', result.rules.length, 'filter rules');
|
||||
emitFiltersLoaded(result.rules);
|
||||
set({
|
||||
isLoading: false,
|
||||
isOpaque: false,
|
||||
rules: result.rules,
|
||||
vacationSettings: result.vacation || null,
|
||||
externalRequires: result.externalRequires,
|
||||
includeVacation: !!result.includeVacation || vacationActive,
|
||||
vacationForward: result.vacationForward ?? null,
|
||||
vacationAudience: result.vacationAudience ?? null,
|
||||
});
|
||||
}
|
||||
if (result.isOpaque) debug.log('filters', 'Sieve script is opaque (hand-edited)');
|
||||
else debug.log('filters', 'Parsed', result.rules.length, 'filter rules');
|
||||
emitFiltersLoaded(result.isOpaque ? [] : result.rules);
|
||||
set({ isLoading: false, ...stateFromScript(result, vacationActive) });
|
||||
} catch (error) {
|
||||
if (stale()) return;
|
||||
debug.error('Failed to fetch filters:', error);
|
||||
@@ -245,7 +245,14 @@ export const useFilterStore = create<FilterStore>()((set, get) => ({
|
||||
const written = await writeFiltersScript(client, selectedAccountId || null, content, activeScriptId);
|
||||
if (!activeScriptId) set({ activeScriptId: written.scriptId });
|
||||
|
||||
set({ isSaving: false, rawScript: written.content });
|
||||
if (isOpaque) {
|
||||
// A script saved from the raw editor is read the way a page load
|
||||
// reads it: one the builder understands shows its rules again.
|
||||
const result = parseScript(written.content);
|
||||
set({ isSaving: false, rawScript: written.content, ...stateFromScript(result, includeVacation) });
|
||||
} else {
|
||||
set({ isSaving: false, rawScript: written.content });
|
||||
}
|
||||
debug.log('filters', 'Filters saved successfully');
|
||||
} catch (error) {
|
||||
debug.error('Failed to save filters:', error);
|
||||
|
||||
Reference in New Issue
Block a user