From 2c1f266fba294f71156196f21fe86e08d20fc24e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 31 Oct 2026 04:00:30 +0100 Subject: [PATCH] feat: open the template manager from the composer's template picker --- src/components/TemplateSheet.tsx | 69 ++++++++++++++++++- .../__tests__/template-sheet-wiring.test.ts | 21 ++++++ 2 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/lib/__tests__/template-sheet-wiring.test.ts diff --git a/src/components/TemplateSheet.tsx b/src/components/TemplateSheet.tsx index 5864475e..bde63594 100644 --- a/src/components/TemplateSheet.tsx +++ b/src/components/TemplateSheet.tsx @@ -3,7 +3,7 @@ import { View, Text, StyleSheet, ScrollView, Pressable, Modal, Animated, Easing, TextInput, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { X, Star, FileText, Users } from './icons'; +import { X, Star, FileText, Users, Settings, ArrowLeft } from './icons'; import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; import { useColors } from '../theme/colors'; import { useSheetDrag } from '../lib/use-sheet-drag'; @@ -12,6 +12,7 @@ import { useTemplatesStore, type EmailTemplate } from '../stores/templates-store import { filterTemplates } from '../lib/template-utils'; import { MODAL_ORIENTATIONS } from '../lib/modal-orientations'; import { useSheetInsets } from '../lib/use-layout'; +import { TemplateSettings } from './settings/TemplateSettings'; interface TemplateSheetProps { visible: boolean; @@ -24,6 +25,8 @@ interface TemplateSheetProps { * (webmail template picker): favourites first, then the rest, with a search * box over name/subject/category. The templates of the user's group * mailboxes follow, each team under its name (Stalwart 1.0 and later). + * "Manage templates" opens the template manager in its place, and back + * returns to the list (webmail template picker, 19676a14f). */ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) { const c = useColors(); @@ -36,6 +39,8 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) const teams = useTemplatesStore((s) => s.teams); const refresh = useTemplatesStore((s) => s.refresh); const [query, setQuery] = React.useState(''); + // The template manager, shown in place of the picker. + const [managing, setManaging] = React.useState(false); const slideY = React.useRef(new Animated.Value(500)).current; // Centred on a wide window, clear of side insets in landscape. const sheetInsets = useSheetInsets(); @@ -49,6 +54,7 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) React.useEffect(() => { if (visible) { setQuery(''); + setManaging(false); // Templates changed in the webmail or on another device, and the team ones. void refresh(); Animated.parallel([ @@ -109,7 +115,15 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) ); return ( - + setManaging(false) : onClose} + statusBarTranslucent + > @@ -165,7 +179,36 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) )} + setManaging(true)} + style={({ pressed }) => [styles.manage, pressed && styles.rowPressed]} + accessibilityRole="button" + > + + {t('templates.manage', 'Manage templates')} + + {managing && ( + + + setManaging(false)} + hitSlop={10} + style={styles.sheetClose} + accessibilityRole="button" + accessibilityLabel={t('common.back', 'Back')} + > + + + + {t('settings.tabs.templates', 'Templates')} + + + + + + + )} ); } @@ -173,6 +216,28 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) function makeStyles(c: ThemePalette) { return StyleSheet.create({ sheetOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' }, + manage: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + minHeight: 44, + paddingHorizontal: spacing.lg, + borderTopWidth: 1, + borderTopColor: c.border, + }, + manageText: { ...typography.body, color: c.textSecondary }, + manager: { ...StyleSheet.absoluteFillObject, backgroundColor: c.background }, + managerHeader: { + flexDirection: 'row', + alignItems: 'center', + gap: spacing.sm, + paddingHorizontal: spacing.md, + paddingVertical: spacing.sm, + borderBottomWidth: 1, + borderBottomColor: c.border, + }, + managerTitle: { ...typography.h3, color: c.text, flex: 1 }, + managerContent: { padding: spacing.lg }, sheetOverlayPress: { flex: 1 }, sheet: { position: 'absolute', diff --git a/src/lib/__tests__/template-sheet-wiring.test.ts b/src/lib/__tests__/template-sheet-wiring.test.ts new file mode 100644 index 00000000..3743a091 --- /dev/null +++ b/src/lib/__tests__/template-sheet-wiring.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import fs from 'node:fs'; +import path from 'node:path'; + +// The template picker in the composer (a component the node test environment +// can't render). With no templates it said "No templates yet" and offered +// no way on (webmail 19676a14f: "Manage" in the template picker). +const source = fs.readFileSync(path.resolve(__dirname, '../../components/TemplateSheet.tsx'), 'utf8'); + +describe('TemplateSheet', () => { + it('offers "Manage templates", which opens the template manager in place of the list', () => { + const manage = source.slice(source.indexOf("t('templates.manage'") - 400, source.indexOf("t('templates.manage'")); + expect(manage).toMatch(/onPress=\{\(\) => setManaging\(true\)\}/); + expect(source).toMatch(/\{managing && \([\s\S]*?/); + }); + + it('leaves the manager for the list on back, and opens on the list', () => { + expect(source).toMatch(/onRequestClose=\{managing \? \(\) => setManaging\(false\) : onClose\}/); + expect(source).toMatch(/if \(visible\) \{\s+setQuery\(''\);\s+setManaging\(false\);/); + }); +});