feat(domain,application,infrastructure,backend,web-server,app-tauri,frontend): lot utilisateur #325 #326 — icônes projet avec watermark CLI custom et localisation FR/EN (QA verte)
Lot en un seul commit: les deux features partagent les mêmes contrats (domain/settings.rs, DTO, commandes Tauri/HTTP, web-server) et les mêmes surfaces UI — la séparation par fichiers était impossible sans chirurgie de hunks risquant des états intermédiaires non compilables (convention lot des batches #310-#313 et #315). #325 — icône configurable par projet, watermark CLI custom: - domaine: ProjectIconSet (devicon, simple-icons, lucide, idea), ProjectIconRef validé (nom ASCII 1..=64, -_. internes), champ icon optionnel sur ProjectSettings; sérialisation camelCase et try_from strict - application: UpdateProjectSettings.execute accepte un patch theme_color OU icon (absent = inchangé, null = défaut produit), tests in-dependants avec store mémoire - backend/app-tauri/web-server: ProjectIconRefDto, UpdateProjectSettingsRequestDto avec icon optionnel (désérialisation additive), commandes update_project_settings alignées Tauri/HTTP, tests DTO et HTTP (défaut null, set, clear, nom invalide rejeté) - frontend: catalogue projectIcons (recherche EN), ProjectIconPicker en grille multi-lignes dans Paramètres projet, affichage de l'icône dans ProjectTabs et watermark grand format à faible opacité en fond de CLI custom; ports ProjectSettings.icon, Partial<ProjectSettings>, adapters tauri/mock/http et tests alignés #326 — localisation FR/EN et traduction des textes IdeA: - domaine: IdeaLanguage (fr | en), champ language optionnel sur IdeaUserSettings (absence = défaut produit, série additive validée) - backend/web-server: UserSettingsDto.language, update_user_settings accepte language, tests HTTP alignés; fixtures structured_launch_d3 mises à jour - frontend: i18n/ (catalogue FR/EN, I18nProvider, scan anti-régression des sources visibles), GeneralSettings avec sélecteur de langue dans les paramètres, setupI18n dans vitest; migration de l'ensemble des textes hardcodés vers le catalogue sur toutes les surfaces (agents, layout, projets, tickets, sprints, web, plugins, permissions, git, templates, skills, mémoire, model servers, embedder, first-run, devices, scheduled tasks, workstate, dialogs), FR conservé par défaut; recherche d'icônes #325 en anglais conformément au ticket QA verte sur feature/batch-325-326-project-icons-localisation. Aucun push.
This commit is contained in:
@ -11,10 +11,12 @@ import type {
|
||||
Template,
|
||||
} from "@/domain";
|
||||
import {
|
||||
mcpToolGroupLabel,
|
||||
mcpToolGroupsForCatalogue,
|
||||
mcpToolLabel,
|
||||
} from "@/features/permissions/mcpToolGroups";
|
||||
import { Button, Input, SmallDropdown, cn } from "@/shared";
|
||||
import { useI18n } from "@/i18n";
|
||||
import { profileLabel } from "./templatePresentation";
|
||||
|
||||
export interface TemplateEditorSaveInput {
|
||||
@ -38,16 +40,6 @@ export interface TemplateEditorProps {
|
||||
|
||||
type EditorTab = "edit" | "preview";
|
||||
|
||||
const NETWORK_OPTIONS: { value: "" | NetworkPolicy; label: string }[] = [
|
||||
{ value: "", label: "Aucune permission système" },
|
||||
{ value: "ask", label: "Réseau sur demande" },
|
||||
{ value: "allow", label: "Réseau autorisé" },
|
||||
{ value: "deny", label: "Réseau refusé" },
|
||||
];
|
||||
|
||||
const TEMPLATE_CROSS_PROJECT_HELP =
|
||||
"Les agents créés avec ce template reprendront cette permission.";
|
||||
|
||||
export function TemplateEditor({
|
||||
template,
|
||||
profiles,
|
||||
@ -56,6 +48,13 @@ export function TemplateEditor({
|
||||
onClose,
|
||||
busy = false,
|
||||
}: TemplateEditorProps) {
|
||||
const { tr } = useI18n();
|
||||
const networkOptions: { value: "" | NetworkPolicy; label: string }[] = [
|
||||
{ value: "", label: tr("Aucune permission système", "No system permission") },
|
||||
{ value: "ask", label: tr("Réseau sur demande", "Ask for network") },
|
||||
{ value: "allow", label: tr("Réseau autorisé", "Network allowed") },
|
||||
{ value: "deny", label: tr("Réseau refusé", "Network denied") },
|
||||
];
|
||||
const [name, setName] = useState(template?.name ?? "");
|
||||
const [content, setContent] = useState(template?.contentMd ?? "");
|
||||
const [defaultAgentName, setDefaultAgentName] = useState(
|
||||
@ -114,16 +113,16 @@ export function TemplateEditor({
|
||||
className="fixed inset-0 z-50 flex flex-col bg-canvas"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="template editor"
|
||||
aria-label={tr("Éditeur de modèle", "Template editor")}
|
||||
>
|
||||
<div className="flex shrink-0 items-center justify-between border-b border-border bg-surface px-4 py-3">
|
||||
<h2 className="text-sm font-semibold text-content">
|
||||
{template ? `Edit template — ${template.name}` : "New template"}
|
||||
{template ? tr(`Modifier le modèle — ${template.name}`, `Edit template — ${template.name}`) : tr("Nouveau modèle", "New template")}
|
||||
</h2>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
aria-label="close template editor"
|
||||
aria-label={tr("Fermer l’éditeur de modèle", "Close template editor")}
|
||||
onClick={onClose}
|
||||
disabled={busy}
|
||||
>
|
||||
@ -138,12 +137,12 @@ export function TemplateEditor({
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-4 overflow-auto px-4 py-3">
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<label htmlFor="te-name" className="text-xs font-medium text-muted">
|
||||
Nom du template
|
||||
{tr("Nom du modèle", "Template name")}
|
||||
</label>
|
||||
<Input
|
||||
id="te-name"
|
||||
aria-label="template name"
|
||||
placeholder="Template revue"
|
||||
aria-label={tr("Nom du modèle", "Template name")}
|
||||
placeholder={tr("Modèle de revue", "Review template")}
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
disabled={busy}
|
||||
@ -156,15 +155,15 @@ export function TemplateEditor({
|
||||
htmlFor="te-content"
|
||||
className="py-2 text-xs font-medium text-muted"
|
||||
>
|
||||
Description
|
||||
{tr("Description", "Description")}
|
||||
</label>
|
||||
<div role="tablist" aria-label="template content mode" className="flex">
|
||||
<div role="tablist" aria-label={tr("Mode du contenu du modèle", "Template content mode")} className="flex">
|
||||
{(["edit", "preview"] as EditorTab[]).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-label={t === "edit" ? "Edit" : "Preview"}
|
||||
aria-label={t === "edit" ? tr("Modifier", "Edit") : tr("Aperçu", "Preview")}
|
||||
aria-selected={tab === t}
|
||||
onClick={() => setTab(t)}
|
||||
className={cn(
|
||||
@ -174,7 +173,7 @@ export function TemplateEditor({
|
||||
: "text-muted hover:text-content",
|
||||
)}
|
||||
>
|
||||
{t === "edit" ? "Edit" : "Preview"}
|
||||
{t === "edit" ? tr("Modifier", "Edit") : tr("Aperçu", "Preview")}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -182,7 +181,7 @@ export function TemplateEditor({
|
||||
{tab === "edit" ? (
|
||||
<textarea
|
||||
id="te-content"
|
||||
aria-label="template content"
|
||||
aria-label={tr("Contenu du modèle", "Template content")}
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
disabled={busy}
|
||||
@ -190,7 +189,7 @@ export function TemplateEditor({
|
||||
"min-h-60 flex-1 resize-none bg-raised px-3 py-2 font-mono text-sm text-content",
|
||||
"outline-none placeholder:text-faint disabled:cursor-not-allowed disabled:opacity-50",
|
||||
)}
|
||||
placeholder="# Instructions de l'agent"
|
||||
placeholder={tr("# Instructions de l’agent", "# Agent instructions")}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@ -205,7 +204,7 @@ export function TemplateEditor({
|
||||
{content.trim() ? (
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
|
||||
) : (
|
||||
<p className="text-sm italic text-muted">Nothing to preview yet.</p>
|
||||
<p className="text-sm italic text-muted">{tr("Rien à prévisualiser pour le moment.", "Nothing to preview yet.")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@ -217,11 +216,11 @@ export function TemplateEditor({
|
||||
htmlFor="te-default-agent"
|
||||
className="text-xs font-medium text-muted"
|
||||
>
|
||||
Nom par défaut de l'agent
|
||||
{tr("Nom par défaut de l’agent", "Default agent name")}
|
||||
</label>
|
||||
<Input
|
||||
id="te-default-agent"
|
||||
aria-label="template default agent name"
|
||||
aria-label={tr("Nom d’agent par défaut du modèle", "Template default agent name")}
|
||||
placeholder="Main"
|
||||
value={defaultAgentName}
|
||||
onChange={(e) => setDefaultAgentName(e.target.value)}
|
||||
@ -231,26 +230,26 @@ export function TemplateEditor({
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<label htmlFor="te-profile" className="text-xs font-medium text-muted">
|
||||
Modèle AI par défaut
|
||||
{tr("Profil IA par défaut", "Default AI profile")}
|
||||
</label>
|
||||
{profiles.length > 0 ? (
|
||||
<SmallDropdown
|
||||
id="te-profile"
|
||||
aria-label="template default profile"
|
||||
aria-label={tr("Profil par défaut du modèle", "Template default profile")}
|
||||
value={defaultProfileId}
|
||||
onChange={setDefaultProfileId}
|
||||
disabled={busy}
|
||||
className="w-full"
|
||||
size="md"
|
||||
options={[
|
||||
{ value: "", label: "Aucun modèle par défaut" },
|
||||
{ value: "", label: tr("Aucun profil par défaut", "No default profile") },
|
||||
...profiles.map((p) => ({ value: p.id, label: profileLabel(p) })),
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
id="te-profile"
|
||||
aria-label="template default profile"
|
||||
aria-label={tr("Profil par défaut du modèle", "Template default profile")}
|
||||
placeholder="profile-id"
|
||||
value={defaultProfileId}
|
||||
onChange={(e) => setDefaultProfileId(e.target.value)}
|
||||
@ -261,24 +260,24 @@ export function TemplateEditor({
|
||||
|
||||
<div className="flex min-w-0 flex-col gap-1">
|
||||
<label htmlFor="te-system-network" className="text-xs font-medium text-muted">
|
||||
Permissions système
|
||||
{tr("Permissions système", "System permissions")}
|
||||
</label>
|
||||
<SmallDropdown
|
||||
id="te-system-network"
|
||||
aria-label="template system network permission"
|
||||
aria-label={tr("Permission réseau système du modèle", "Template system network permission")}
|
||||
value={network}
|
||||
onChange={(value) => setNetwork(value as "" | NetworkPolicy)}
|
||||
disabled={busy}
|
||||
className="w-full"
|
||||
size="md"
|
||||
options={NETWORK_OPTIONS}
|
||||
options={networkOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label className="flex min-w-0 items-start gap-2 rounded-md border border-border bg-raised px-3 py-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label="Contactable depuis d’autres projets"
|
||||
aria-label={tr("Contactable depuis d’autres projets", "Contactable from other projects")}
|
||||
checked={defaultContactable}
|
||||
onChange={(e) => setDefaultContactable(e.target.checked)}
|
||||
disabled={busy}
|
||||
@ -286,10 +285,10 @@ export function TemplateEditor({
|
||||
/>
|
||||
<span className="flex min-w-0 flex-col gap-1">
|
||||
<span className="text-sm font-medium text-content">
|
||||
Contactable depuis d’autres projets
|
||||
{tr("Contactable depuis d’autres projets", "Contactable from other projects")}
|
||||
</span>
|
||||
<span className="text-xs text-muted">
|
||||
{TEMPLATE_CROSS_PROJECT_HELP}
|
||||
{tr("Les agents créés avec ce modèle reprendront cette permission.", "Agents created from this template will inherit this permission.")}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
@ -298,23 +297,23 @@ export function TemplateEditor({
|
||||
<section className="flex flex-col gap-3 rounded-md border border-border bg-raised p-3">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h3 className="text-xs font-medium text-muted">
|
||||
Permissions Tools MCP IdeA
|
||||
{tr("Permissions des outils MCP IdeA", "IdeA MCP tool permissions")}
|
||||
</h3>
|
||||
<span className="text-xs text-faint">{allowedTools.length} autorisé(s)</span>
|
||||
<span className="text-xs text-faint">{allowedTools.length} {tr("autorisé(s)", "allowed")}</span>
|
||||
</div>
|
||||
{allMcpTools.length === 0 ? (
|
||||
<p className="text-sm text-muted">Aucun tool MCP IdeA disponible.</p>
|
||||
<p className="text-sm text-muted">{tr("Aucun outil MCP IdeA disponible.", "No IdeA MCP tool available.")}</p>
|
||||
) : (
|
||||
<>
|
||||
{allowedTools.length === 0 && (
|
||||
<p className="text-sm text-muted">
|
||||
Aucun tool MCP IdeA autorisé.
|
||||
{tr("Aucun outil MCP IdeA autorisé.", "No IdeA MCP tool allowed.")}
|
||||
</p>
|
||||
)}
|
||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
||||
{toolGroups.map((group) => (
|
||||
<div key={group.id} className="flex min-w-0 flex-col gap-2">
|
||||
<h4 className="text-xs font-semibold text-content">{group.label}</h4>
|
||||
<h4 className="text-xs font-semibold text-content">{mcpToolGroupLabel(group, tr)}</h4>
|
||||
{group.tools.map((tool) => (
|
||||
<label
|
||||
key={tool}
|
||||
@ -322,13 +321,13 @@ export function TemplateEditor({
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
aria-label={`template MCP tool ${tool}`}
|
||||
aria-label={`${tr("Outil MCP du modèle", "Template MCP tool")} ${tool}`}
|
||||
checked={allowed.has(tool)}
|
||||
onChange={() => toggleTool(tool)}
|
||||
disabled={busy}
|
||||
className="h-4 w-4 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="truncate">{mcpToolLabel(tool)}</span>
|
||||
<span className="truncate">{mcpToolLabel(tool, tr)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
@ -341,16 +340,16 @@ export function TemplateEditor({
|
||||
|
||||
<div className="flex shrink-0 items-center justify-end gap-2 border-t border-border px-4 py-3">
|
||||
<Button type="button" variant="ghost" onClick={onClose} disabled={busy}>
|
||||
Cancel
|
||||
{tr("Annuler", "Cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="primary"
|
||||
aria-label="Save template"
|
||||
aria-label={tr("Enregistrer le modèle", "Save template")}
|
||||
disabled={!canSave}
|
||||
loading={busy}
|
||||
>
|
||||
Save
|
||||
{tr("Enregistrer", "Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user