+ {showTabs && (
+
{
+ // Arrow keys, Home and End move between the lists, as in any tab list.
+ const index = sources.findIndex((s) => s.key === source?.key);
+ const next = e.key === 'ArrowRight' || e.key === 'ArrowDown' ? index + 1
+ : e.key === 'ArrowLeft' || e.key === 'ArrowUp' ? index - 1
+ : e.key === 'Home' ? 0
+ : e.key === 'End' ? sources.length - 1
+ : null;
+ if (next === null || sources.length === 0) return;
+ e.preventDefault();
+ const target = sources[(next + sources.length) % sources.length];
+ selectSource(target.key);
+ document.getElementById(tabId(target.key))?.focus();
+ }}
+ >
+ {sources.map((s) => (
+
+ ))}
+
+ )}
+
+
+ {note &&
{note}
}
+
+ {!source && (
+
+
+
{tSettings('no_templates')}
+
+ )}
+
+ {source && !isCreating && !editingId && (
@@ -107,129 +234,185 @@ export function TemplateManagerModal({ isOpen, onClose }: TemplateManagerModalPr
className="ps-9"
/>
-
+ {editable && (
+
+ )}
)}
- {isCreating && (
+ {error && (
+
{error}
+ )}
+
+ {editable && isCreating && (
{tSettings('add')}
setIsCreating(false)}
+ onCancel={closeForm}
/>
)}
- {editingId && (
+ {editable && editingId && (
{tSettings('edit')}
t.id === editingId)}
+ template={templates.find((tpl) => tpl.id === editingId)}
+ categorySource={templates}
+ shared={!isPersonal}
+ busy={busy}
onSave={handleSave}
- onCancel={() => setEditingId(null)}
+ onCancel={closeForm}
/>
)}
- {filtered.map((template) => (
-
-
+ {source && filtered.map((template) => (
+
+
+ {isPersonal && (
+
+ )}
-
-
-
- {template.name}
-
- {template.category && (
-
- {template.category}
-
+
+
+
+ {template.name}
+
+ {template.category && (
+
+ {template.category}
+
+ )}
+
+ {template.subject && (
+
+ {template.subject}
+
+ )}
+ {isPersonal && actions.localOnly[template.id] && (
+
+ {actions.localOnly[template.id] === 'too-large' ? tOrg('local_only_too_large') : tOrg('local_only_no_room')}
+
)}
- {template.subject && (
-
- {template.subject}
-
- )}
-
-
-
-
- {deleteConfirmId === template.id ? (
-
-
-
-
- ) : (
-
- )}
+
+ {editable ? (
+ <>
+
+
+ {deleteConfirmId === template.id ? (
+
+
+
+
+ ) : (
+
+ )}
+ >
+ ) : (
+ <>
+
+ {personal?.editable && (
+
+ )}
+ >
+ )}
+
+ {previewId === template.id && (
+
+
+
+ )}
))}
- {filtered.length === 0 && !isCreating && (
+ {source && filtered.length === 0 && !isCreating && (
@@ -238,6 +421,11 @@ export function TemplateManagerModal({ isOpen, onClose }: TemplateManagerModalPr
)}
+
+ {isPersonal && actions.storedInAccount && (
+
{tOrg('personal_in_account')}
+ )}
+
diff --git a/components/templates/template-picker.tsx b/components/templates/template-picker.tsx
index 533d858b2..8c2fdff41 100644
--- a/components/templates/template-picker.tsx
+++ b/components/templates/template-picker.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useState } from 'react';
+import { useEffect, useState } from 'react';
import { useTranslations, useLocale } from 'next-intl';
import { X, Search, Star, FileText } from '@/components/icons';
import { cn } from '@/lib/utils';
@@ -8,10 +8,15 @@ import { Input } from '@/components/ui/input';
import { useTemplateStore } from '@/stores/template-store';
import { useAuthStore } from '@/stores/auth-store';
import { useFocusTrap } from '@/hooks/use-focus-trap';
+import { useTemplateSources } from '@/hooks/use-template-sources';
import {
+ filterTemplates,
getPlaceholdersFromTemplate,
getAutoFilledPlaceholders,
} from '@/lib/template-utils';
+import type { TemplateSource } from '@/lib/templates/template-sources';
+import { refreshAccountTemplates, resolveAccountId } from '@/lib/templates/personal-templates';
+import { refreshTeamTemplates } from '@/lib/templates/team-templates';
import { PlaceholderFillModal } from './placeholder-fill-modal';
import type { EmailTemplate } from '@/lib/template-types';
@@ -19,34 +24,46 @@ interface TemplatePickerProps {
isOpen: boolean;
onClose: () => void;
onSelect: (template: EmailTemplate, filledValues: Record
) => void;
+ /**
+ * The signed-in account the message is written from: its own templates,
+ * its team templates and its organisation library. The active one when
+ * left out.
+ */
+ accountId?: string | null;
}
-export function TemplatePicker({ isOpen, onClose, onSelect }: TemplatePickerProps) {
+export function TemplatePicker({ isOpen, onClose, onSelect, accountId }: TemplatePickerProps) {
const t = useTranslations('templates');
+ const tOrg = useTranslations('org_templates');
const locale = useLocale();
- const { templates, getFavorites, getRecent, getTemplatesByCategory, searchTemplates, recordUsage } =
- useTemplateStore();
+ const recordUsage = useTemplateStore((s) => s.recordUsage);
+ const recentTemplateIds = useTemplateStore((s) => s.recentTemplateIds);
const { primaryIdentity } = useAuthStore();
+ const { sources } = useTemplateSources(accountId);
const [searchQuery, setSearchQuery] = useState('');
const [selectedTemplate, setSelectedTemplate] = useState(null);
const [showFillModal, setShowFillModal] = useState(false);
+ // What another device or a team member changed shows up when the picker opens.
+ useEffect(() => {
+ if (!isOpen) return;
+ refreshAccountTemplates(accountId);
+ const id = resolveAccountId(accountId);
+ if (id) refreshTeamTemplates(id);
+ }, [isOpen, accountId]);
+
const modalRef = useFocusTrap({
isActive: isOpen && !showFillModal,
onEscape: onClose,
restoreFocus: true,
});
- const favorites = getFavorites();
- const recent = getRecent();
- const byCategory = getTemplatesByCategory();
- const filtered = searchQuery ? searchTemplates(searchQuery) : null;
-
- const handleSelectTemplate = (template: EmailTemplate) => {
+ const handleSelectTemplate = (template: EmailTemplate, source: TemplateSource) => {
const placeholders = getPlaceholdersFromTemplate(template);
- recordUsage(template.id);
+ // Recently used lists the user's own templates only.
+ if (source.kind === 'personal') recordUsage(template.id);
if (placeholders.length > 0) {
setSelectedTemplate(template);
@@ -71,22 +88,22 @@ export function TemplatePicker({ isOpen, onClose, onSelect }: TemplatePickerProp
locale,
});
- const renderTemplateItem = (template: EmailTemplate) => (
+ const renderTemplateItem = (template: EmailTemplate, source: TemplateSource) => (