mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-02 00:06:23 +00:00
feat: open the template manager from the composer's template picker
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
|||||||
View, Text, StyleSheet, ScrollView, Pressable, Modal, Animated, Easing, TextInput,
|
View, Text, StyleSheet, ScrollView, Pressable, Modal, Animated, Easing, TextInput,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
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 { spacing, radius, typography, type ThemePalette } from '../theme/tokens';
|
||||||
import { useColors } from '../theme/colors';
|
import { useColors } from '../theme/colors';
|
||||||
import { useSheetDrag } from '../lib/use-sheet-drag';
|
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 { filterTemplates } from '../lib/template-utils';
|
||||||
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
import { MODAL_ORIENTATIONS } from '../lib/modal-orientations';
|
||||||
import { useSheetInsets } from '../lib/use-layout';
|
import { useSheetInsets } from '../lib/use-layout';
|
||||||
|
import { TemplateSettings } from './settings/TemplateSettings';
|
||||||
|
|
||||||
interface TemplateSheetProps {
|
interface TemplateSheetProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
@@ -24,6 +25,8 @@ interface TemplateSheetProps {
|
|||||||
* (webmail template picker): favourites first, then the rest, with a search
|
* (webmail template picker): favourites first, then the rest, with a search
|
||||||
* box over name/subject/category. The templates of the user's group
|
* box over name/subject/category. The templates of the user's group
|
||||||
* mailboxes follow, each team under its name (Stalwart 1.0 and later).
|
* 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) {
|
export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
@@ -36,6 +39,8 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
|||||||
const teams = useTemplatesStore((s) => s.teams);
|
const teams = useTemplatesStore((s) => s.teams);
|
||||||
const refresh = useTemplatesStore((s) => s.refresh);
|
const refresh = useTemplatesStore((s) => s.refresh);
|
||||||
const [query, setQuery] = React.useState('');
|
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;
|
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||||
// Centred on a wide window, clear of side insets in landscape.
|
// Centred on a wide window, clear of side insets in landscape.
|
||||||
const sheetInsets = useSheetInsets();
|
const sheetInsets = useSheetInsets();
|
||||||
@@ -49,6 +54,7 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
|||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (visible) {
|
if (visible) {
|
||||||
setQuery('');
|
setQuery('');
|
||||||
|
setManaging(false);
|
||||||
// Templates changed in the webmail or on another device, and the team ones.
|
// Templates changed in the webmail or on another device, and the team ones.
|
||||||
void refresh();
|
void refresh();
|
||||||
Animated.parallel([
|
Animated.parallel([
|
||||||
@@ -109,7 +115,15 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal supportedOrientations={MODAL_ORIENTATIONS} visible={visible} transparent animationType="none" onRequestClose={onClose} statusBarTranslucent>
|
<Modal
|
||||||
|
supportedOrientations={MODAL_ORIENTATIONS}
|
||||||
|
visible={visible}
|
||||||
|
transparent
|
||||||
|
animationType="none"
|
||||||
|
// Back leaves the manager for the list first.
|
||||||
|
onRequestClose={managing ? () => setManaging(false) : onClose}
|
||||||
|
statusBarTranslucent
|
||||||
|
>
|
||||||
<Animated.View style={[styles.sheetOverlay, { opacity: overlayOpacity }]}>
|
<Animated.View style={[styles.sheetOverlay, { opacity: overlayOpacity }]}>
|
||||||
<Pressable style={styles.sheetOverlayPress} onPress={onClose} />
|
<Pressable style={styles.sheetOverlayPress} onPress={onClose} />
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
@@ -165,7 +179,36 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setManaging(true)}
|
||||||
|
style={({ pressed }) => [styles.manage, pressed && styles.rowPressed]}
|
||||||
|
accessibilityRole="button"
|
||||||
|
>
|
||||||
|
<Settings size={16} color={c.textSecondary} />
|
||||||
|
<Text style={styles.manageText}>{t('templates.manage', 'Manage templates')}</Text>
|
||||||
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
{managing && (
|
||||||
|
<View style={[styles.manager, { paddingTop: insets.top, paddingBottom: insets.bottom }]}>
|
||||||
|
<View style={styles.managerHeader}>
|
||||||
|
<Pressable
|
||||||
|
onPress={() => setManaging(false)}
|
||||||
|
hitSlop={10}
|
||||||
|
style={styles.sheetClose}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('common.back', 'Back')}
|
||||||
|
>
|
||||||
|
<ArrowLeft size={22} color={c.text} />
|
||||||
|
</Pressable>
|
||||||
|
<Text style={styles.managerTitle} numberOfLines={1} accessibilityRole="header">
|
||||||
|
{t('settings.tabs.templates', 'Templates')}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<ScrollView style={styles.scrollList} contentContainerStyle={styles.managerContent} keyboardShouldPersistTaps="handled">
|
||||||
|
<TemplateSettings />
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -173,6 +216,28 @@ export function TemplateSheet({ visible, onClose, onPick }: TemplateSheetProps)
|
|||||||
function makeStyles(c: ThemePalette) {
|
function makeStyles(c: ThemePalette) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
sheetOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)' },
|
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 },
|
sheetOverlayPress: { flex: 1 },
|
||||||
sheet: {
|
sheet: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
|
|||||||
@@ -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]*?<TemplateSettings \/>/);
|
||||||
|
});
|
||||||
|
|
||||||
|
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\);/);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user