From b0982036e017c16978230fec0762e5730ea739a3 Mon Sep 17 00:00:00 2001 From: bernd2022 <104787072+bernd2022@users.noreply.github.com> Date: Fri, 22 May 2026 15:13:12 +0200 Subject: [PATCH 1/2] feat(support): add support issue templates (DEV-4571) (#1110) * feat(support): add support issue templates (DEV-4571) * fix(support): clear english template content and show edit timestamp * feat(support): add template copy workflow in customer search * feat(support): add "only own" filter for templates with persistent toggle * refactor(support): rename templates to support-templates and use react-icons * refactor(support): replace remaining emoji icons with react-icons * feat(support): group own templates first with section headers * feat(support): show remaining character count on template inputs * fix(support): respect own-filter when auto-selecting template in picker --- src/App.tsx | 5 + src/components/compliance/note-list.tsx | 9 +- src/components/modal.tsx | 14 +- .../bilingual-content-editor.tsx | 112 ++++ .../support-templates/char-remaining-hint.tsx | 14 + .../template-array-picker-modal.tsx | 77 +++ .../support-templates/template-composer.tsx | 92 ++++ .../support-templates/template-list.tsx | 219 ++++++++ .../template-picker-modal.tsx | 493 ++++++++++++++++++ .../support-templates/token-picker-panel.tsx | 46 ++ src/hooks/support-dashboard.hook.ts | 1 + src/hooks/support-templates.hook.ts | 134 +++++ .../support-dashboard-issue.screen.tsx | 150 +++++- src/screens/support-dashboard.screen.tsx | 71 ++- src/screens/support-templates.screen.tsx | 212 ++++++++ src/util/template-placeholders.ts | 378 ++++++++++++++ 16 files changed, 2008 insertions(+), 19 deletions(-) create mode 100644 src/components/support-templates/bilingual-content-editor.tsx create mode 100644 src/components/support-templates/char-remaining-hint.tsx create mode 100644 src/components/support-templates/template-array-picker-modal.tsx create mode 100644 src/components/support-templates/template-composer.tsx create mode 100644 src/components/support-templates/template-list.tsx create mode 100644 src/components/support-templates/template-picker-modal.tsx create mode 100644 src/components/support-templates/token-picker-panel.tsx create mode 100644 src/hooks/support-templates.hook.ts create mode 100644 src/screens/support-templates.screen.tsx create mode 100644 src/util/template-placeholders.ts diff --git a/src/App.tsx b/src/App.tsx index 163c09d5..ad813ffc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,6 +78,7 @@ const SupportDashboardScreen = lazy(() => import('./screens/support-dashboard.sc const SupportDashboardIssueScreen = lazy(() => import('./screens/support-dashboard-issue.screen')); const SupportDashboardCreateScreen = lazy(() => import('./screens/support-dashboard-create.screen')); const NotesScreen = lazy(() => import('./screens/notes.screen')); +const TemplatesScreen = lazy(() => import('./screens/support-templates.screen')); const RealunitScreen = lazy(() => import('./screens/realunit.screen')); const RealunitHoldersScreen = lazy(() => import('./screens/realunit-holders.screen')); const RealunitQuotesScreen = lazy(() => import('./screens/realunit-quotes.screen')); @@ -464,6 +465,10 @@ export const Routes = [ path: 'notes', element: withSuspense(), }, + { + path: 'templates', + element: withSuspense(), + }, { path: 'realunit', element: ( diff --git a/src/components/compliance/note-list.tsx b/src/components/compliance/note-list.tsx index 22bcba8f..02e4b5a1 100644 --- a/src/components/compliance/note-list.tsx +++ b/src/components/compliance/note-list.tsx @@ -1,4 +1,5 @@ import { useState } from 'react'; +import { MdDelete, MdEdit } from 'react-icons/md'; import { useNavigate } from 'react-router-dom'; import { ConfirmDialog } from 'src/components/confirm-dialog'; import { SupportNoteInfo, useCompliance } from 'src/hooks/compliance.hook'; @@ -101,19 +102,19 @@ export function NoteList({ notes, showUserDataIdLink, emptyMessage, onChange }:
)} diff --git a/src/components/modal.tsx b/src/components/modal.tsx index 4609adb6..69ed95e3 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -7,9 +7,17 @@ interface ModalProps extends PropsWithChildren { onClose?: () => void; variant?: 'fullscreen' | 'dialog'; className?: string; + maxWidthClass?: string; } -export function Modal({ isOpen, onClose, children, variant = 'fullscreen', className = '' }: ModalProps): JSX.Element | null { +export function Modal({ + isOpen, + onClose, + children, + variant = 'fullscreen', + className = '', + maxWidthClass = 'max-w-screen-md', +}: ModalProps): JSX.Element | null { const [mounted, setMounted] = useState(false); const [topOffset, setTopOffset] = useState(0); const { modalRootRef, rootRef } = useLayoutContext(); @@ -61,7 +69,7 @@ export function Modal({ isOpen, onClose, children, variant = 'fullscreen', class onClick={(e) => e.target === e.currentTarget && onClose?.()} >
-
{children}
+
{children}
, rootRef.current, @@ -74,7 +82,7 @@ export function Modal({ isOpen, onClose, children, variant = 'fullscreen', class style={{ top: topOffset }} >
-
{children}
+
{children}
, rootRef.current, diff --git a/src/components/support-templates/bilingual-content-editor.tsx b/src/components/support-templates/bilingual-content-editor.tsx new file mode 100644 index 00000000..9297ff53 --- /dev/null +++ b/src/components/support-templates/bilingual-content-editor.tsx @@ -0,0 +1,112 @@ +import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; +import { + TEMPLATE_CONTENT_MAX_LENGTH, + TEMPLATE_LANGUAGES, + TemplateContents, + TemplateLanguage, +} from 'src/hooks/support-templates.hook'; +import { CharRemainingHint } from './char-remaining-hint'; + +export interface BilingualContentEditorHandle { + insertAtActive: (text: string) => void; +} + +interface Props { + contents: TemplateContents; + onChange: (contents: TemplateContents) => void; + disabled?: boolean; + placeholderDe?: string; + placeholderEn?: string; +} + +const LANG_LABELS: Record = { + de: 'Deutsch', + en: 'English', +}; + +export const BilingualContentEditor = forwardRef(function BilingualContentEditor( + { contents, onChange, disabled, placeholderDe, placeholderEn }, + ref, +) { + const refs = useRef>({ de: null, en: null }); + const [activeLang, setActiveLang] = useState('de'); + + function autoResize(ta: HTMLTextAreaElement | null): void { + if (!ta) return; + ta.style.height = 'auto'; + ta.style.height = `${ta.scrollHeight}px`; + } + + // Re-fit on every contents change (typing, token insert, external setContents) + useEffect(() => { + TEMPLATE_LANGUAGES.forEach((lang) => autoResize(refs.current[lang])); + }, [contents]); + + useImperativeHandle( + ref, + () => ({ + insertAtActive(text: string) { + const ta = refs.current[activeLang]; + const currentValue = contents[activeLang] ?? ''; + if (!ta) { + onChange({ ...contents, [activeLang]: currentValue + text }); + return; + } + const start = ta.selectionStart; + const end = ta.selectionEnd; + const next = currentValue.slice(0, start) + text + currentValue.slice(end); + onChange({ ...contents, [activeLang]: next }); + requestAnimationFrame(() => { + ta.focus(); + const pos = start + text.length; + ta.setSelectionRange(pos, pos); + }); + }, + }), + [activeLang, contents, onChange], + ); + + function updateLang(lang: TemplateLanguage, value: string): void { + if (lang === 'de') onChange({ ...contents, de: value }); + else onChange({ ...contents, en: value || undefined }); + } + + return ( +
+ {TEMPLATE_LANGUAGES.map((lang) => { + const value = contents[lang] ?? ''; + const isActive = activeLang === lang; + return ( +
+
+ + {LANG_LABELS[lang]} + {lang === 'en' && (optional)} + + {isActive && ● aktiv} +
+