mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
feat: let admins lock shared tags per domain or group
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react';
|
||||
|
||||
// Organisation > Sharing > Shared tags: one sentence and the lock for the
|
||||
// `sharedTags` restriction, written at the scope chosen in the scope bar.
|
||||
|
||||
const puts: Array<Record<string, unknown>> = [];
|
||||
let stored: Record<string, unknown> = {};
|
||||
vi.mock('@/lib/browser-navigation', () => ({
|
||||
apiFetch: vi.fn(async (_url: string, init?: RequestInit) => {
|
||||
if (init?.method === 'PUT') {
|
||||
const body = JSON.parse(String(init.body));
|
||||
puts.push(body);
|
||||
stored = body;
|
||||
return new Response('{"ok":true}');
|
||||
}
|
||||
return new Response(JSON.stringify(stored));
|
||||
}),
|
||||
withBasePath: (p: string) => p,
|
||||
getPathPrefix: () => '',
|
||||
}));
|
||||
|
||||
import { SharedTagsSection } from '../sections/shared-tags-section';
|
||||
import { useOrgPolicyDraft, useOrgScopeStore, EVERYONE } from '../use-org-policy-draft';
|
||||
import { RESTRICTION_KEYS } from '@/lib/admin/restriction-keys';
|
||||
import { resolvePolicy } from '@/lib/admin/policy-scopes';
|
||||
|
||||
const POLICY = {
|
||||
restrictions: {}, features: {}, defaults: {}, themePolicy: {},
|
||||
forceEnabledPlugins: [], approvedPlugins: [], forceEnabledThemes: [],
|
||||
scopes: [{ id: 'leads', label: 'Leads', match: { groups: ['leads@example.org'] }, policy: {} }],
|
||||
};
|
||||
|
||||
let latest: ReturnType<typeof useOrgPolicyDraft> | null = null;
|
||||
function Harness() {
|
||||
const draft = useOrgPolicyDraft('test');
|
||||
latest = draft;
|
||||
if (draft.loading) return null;
|
||||
return <SharedTagsSection draft={draft} />;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
puts.length = 0;
|
||||
stored = JSON.parse(JSON.stringify(POLICY));
|
||||
latest = null;
|
||||
useOrgScopeStore.setState({ scopeId: EVERYONE });
|
||||
});
|
||||
|
||||
describe('Shared tags section', () => {
|
||||
it('registers sharedTags on the Sharing page, lock only', () => {
|
||||
expect(RESTRICTION_KEYS.find((def) => def.key === 'sharedTags')).toMatchObject({ page: 'org-sharing', modes: ['locked'] });
|
||||
});
|
||||
|
||||
it('locks shared tags for everyone and unlocks them in a group scope', async () => {
|
||||
render(<Harness />);
|
||||
const section = await screen.findByTestId('org-shared-tags-section');
|
||||
expect(within(section).queryByText('Hide')).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(within(section).getByRole('checkbox'));
|
||||
expect(latest!.policy?.restrictions.sharedTags).toEqual({ locked: true });
|
||||
|
||||
act(() => latest!.setScopeId('leads'));
|
||||
await waitFor(() => expect(within(screen.getByTestId('org-shared-tags-section')).getByRole('checkbox')).toBeChecked());
|
||||
fireEvent.click(within(screen.getByTestId('org-shared-tags-section')).getByRole('checkbox'));
|
||||
// In a scope an empty restriction is a real override: it unlocks.
|
||||
expect(latest!.policy?.scopes?.[0].policy.restrictions?.sharedTags).toEqual({});
|
||||
const lead = resolvePolicy(latest!.policy!, { email: 'anna@example.org', groups: ['leads@example.org'] }).policy;
|
||||
const member = resolvePolicy(latest!.policy!, { email: 'bob@example.org', groups: ['team@example.org'] }).policy;
|
||||
expect(lead.restrictions.sharedTags?.locked).not.toBe(true);
|
||||
expect(member.restrictions.sharedTags?.locked).toBe(true);
|
||||
|
||||
await act(async () => { await latest!.save(); });
|
||||
expect(puts[0]).toMatchObject({
|
||||
restrictions: { sharedTags: { locked: true } },
|
||||
scopes: [{ id: 'leads', policy: { restrictions: { sharedTags: {} } } }],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,14 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { SettingsSection } from '@/components/settings/settings-section';
|
||||
import { restrictionKeysForPage } from '@/lib/admin/restriction-keys';
|
||||
import { RestrictionRow } from '../org-page';
|
||||
import type { OrgPolicyDraft } from '../use-org-policy-draft';
|
||||
|
||||
/** Organisation feature 6; filled in by its own step (local-data/org-features-plan.md). */
|
||||
/**
|
||||
* Organisation feature 6: shared tags in group mailboxes. The tags live in
|
||||
* the group's Inbox on the mail server (Stalwart 1.0 metadata), so the only
|
||||
* thing to set here is who may change them.
|
||||
*/
|
||||
export function SharedTagsSection({ draft }: { draft: OrgPolicyDraft }) {
|
||||
void draft;
|
||||
return (
|
||||
<SettingsSection id="shared-tags" title="Shared tags" description="Tag names and colours that every member of a shared mailbox sees.">
|
||||
<p className="text-sm text-muted-foreground">Nothing to set here yet.</p>
|
||||
<SettingsSection
|
||||
id="shared-tags"
|
||||
title="Shared tags"
|
||||
description="Members of a group mailbox see the same tag names and colours on its mail, and members who may change the mailbox edit them (Stalwart 1.0 or later)."
|
||||
>
|
||||
<div data-testid="org-shared-tags-section">
|
||||
{restrictionKeysForPage('org-sharing')
|
||||
.filter((def) => def.key === 'sharedTags')
|
||||
.map((def) => <RestrictionRow key={def.key} draft={draft} def={def} />)}
|
||||
</div>
|
||||
</SettingsSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,6 +51,15 @@ export const RESTRICTION_KEYS: readonly RestrictionKeyDef[] = [
|
||||
page: 'org-signatures',
|
||||
modes: ['locked', 'hidden'],
|
||||
},
|
||||
{
|
||||
key: 'sharedTags',
|
||||
label: 'Shared tags',
|
||||
description: "Lock: users can tag mail with the shared tags of their group mailboxes but can't add, rename or delete them. Unlock it in a group scope for the people who look after the tags.",
|
||||
category: 'Sharing',
|
||||
type: 'boolean',
|
||||
page: 'org-sharing',
|
||||
modes: ['locked'],
|
||||
},
|
||||
];
|
||||
|
||||
export function restrictionKeysForPage(page: AdminPageId): RestrictionKeyDef[] {
|
||||
|
||||
Reference in New Issue
Block a user