Turned the mailtypes ul into a reusable selectable list control.

This commit is contained in:
Colin Dawson 2026-02-13 00:24:46 +00:00
parent 1e16b2676e
commit 745473759d
8 changed files with 113 additions and 74 deletions

View File

@ -11,26 +11,3 @@
grid-template-columns: fit-content(50%) auto;
grid-gap: $gridGap;
}
.mail-types {
padding-left: 0rem;
list-style: none;
width: $mailtemplateNameListWidth;
li {
cursor: pointer;
padding: 0.5rem 0.75rem;
border-radius: 4px;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
&.selected {
background-color: #0078d4;
color: white;
font-weight: 500;
}
}
}

View File

@ -29,6 +29,7 @@
@import "./_expandableCell.scss";
@import "./_errorLogs.scss";
@import "./addTaskButton.scss";
@import "./selectableList.scss";
//Changes needed to make MS Edge behave the same as other browsers
input::-ms-reveal {

View File

@ -0,0 +1,22 @@
.selectable-list {
padding-left: 0rem;
list-style: none;
width: $mailtemplateNameListWidth;
li {
cursor: pointer;
padding: 0.5rem 0.75rem;
border-radius: 4px;
transition: background-color 0.2s ease;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
&.selected {
background-color: #0078d4;
color: white;
font-weight: 500;
}
}
}

View File

@ -0,0 +1,33 @@
import React from "react";
export interface SelectableListProps<T> {
items: T[];
selectedValue?: t | null;
renderLabel: (item: T) => React.ReactNode;
onSelect: (item: T) => void;
}
export const SelectableList = <T,>(
props: SelectableListProps<T>,
): JSX.Element => {
const { items, selectedValue, renderLabel, onSelect } = props;
const listClassName = "selectable-list";
return (
<ul className={listClassName}>
{items.map((item, index) => {
const isSelected = selectedValue === item;
const className = isSelected
? ["selected"].filter(Boolean).join(" ")
: "";
return (
<li key={index} onClick={() => onSelect(item)} className={className}>
{renderLabel(item)}
</li>
);
})}
</ul>
);
};

View File

@ -102,6 +102,7 @@ export const useForm = (initialState: FormState): UseFormReturn => {
const initialDataRef = useRef<FormData>(
JSON.parse(JSON.stringify(initialState.data)),
);
const wasLoadedRef = useRef<boolean>(initialState.loaded);
const setState = useCallback((updates: Partial<FormState>) => {
setStateInternal((prev) => ({ ...prev, ...updates }));
@ -621,6 +622,14 @@ export const useForm = (initialState: FormState): UseFormReturn => {
return cleanup;
}, [setupNavigationGuard]);
useEffect(() => {
if (!wasLoadedRef.current && state.loaded) {
initialDataRef.current = JSON.parse(JSON.stringify(state.data));
}
wasLoadedRef.current = state.loaded;
}, [state.loaded, state.data]);
const api: any = {
state,
schema: schemaRef.current,

View File

@ -1,10 +1,11 @@
import React, { useEffect, useState, useCallback, Suspense } from "react";
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import mailTemplatesService from "../serrvices/mailTemplatesService";
import HOCEmailTemplateEditor from "./EmailTemplateEditor";
import Loading from "../../../../components/common/Loading";
import { Namespaces } from "../../../../i18n/i18n";
import { useTranslation } from "react-i18next";
import { SelectableList } from "../../../../components/common/SelectableList";
interface MailType {
mailType: string;
@ -13,35 +14,27 @@ interface MailType {
const MailTemplatesTabContent: React.FC<{
types: MailType[];
currentMailType: string;
currentMailType: MailType | null;
domainId: string | undefined;
onClick: (e: React.MouseEvent<HTMLElement>) => void;
onClick: (item: MailType) => void;
}> = ({ types, currentMailType, domainId, onClick }) => {
const { t: tMail } = useTranslation(Namespaces.MailTypes);
return (
<div className="two-column-grid">
<div className="fit-content-width">
<ul className="mail-types">
{types.map((x) => {
return (
<li
key={x.mailType}
value={x.mailType}
onClick={onClick}
className={currentMailType === x.mailType ? "selected" : ""}
>
{tMail(x.mailType)}
</li>
);
})}
</ul>
<SelectableList
items={types}
selectedValue={currentMailType}
renderLabel={(x) => tMail(x.mailType)}
onSelect={(item) => onClick(item)}
/>
</div>
<div>
{domainId && currentMailType ? (
<HOCEmailTemplateEditor
domainId={domainId}
currentMailType={currentMailType}
currentMailType={currentMailType.mailType}
/>
) : null}
</div>
@ -51,7 +44,7 @@ const MailTemplatesTabContent: React.FC<{
const MailTemplatesTab: React.FC = () => {
const [loaded, setLoaded] = useState(false);
const [currentMailType, setCurrentMailType] = useState("");
const [currentMailType, setCurrentMailType] = useState<MailType | null>(null);
const [types, setTypes] = useState<MailType[]>([]);
useEffect(() => {
@ -62,7 +55,7 @@ const MailTemplatesTab: React.FC = () => {
setTypes(nextTypes);
if (nextTypes.length > 0) {
setCurrentMailType(nextTypes[0].mailType);
setCurrentMailType(nextTypes[0]);
}
} catch (ex) {
console.error(ex);
@ -74,34 +67,20 @@ const MailTemplatesTab: React.FC = () => {
void loadTypes();
}, []);
const selectTemplate = useCallback(
(emailType: string) => {
if (currentMailType !== emailType) {
setCurrentMailType(emailType);
}
},
[currentMailType],
);
const onClick = (e: React.MouseEvent<HTMLElement>) => {
const value = (e.target as HTMLElement).getAttribute("value");
if (value) {
selectTemplate(value);
}
const onClick = (item: MailType) => {
setCurrentMailType(item);
};
const { domainId } = useParams<{ domainId: string }>();
return (
<Loading loaded={loaded}>
<Suspense fallback={<div>Loading...</div>}>
<MailTemplatesTabContent
types={types}
currentMailType={currentMailType}
domainId={domainId}
onClick={onClick}
/>
</Suspense>
<MailTemplatesTabContent
types={types}
currentMailType={currentMailType}
domainId={domainId}
onClick={onClick}
/>
</Loading>
);
};

View File

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import {
TaskDefinition,
TaskMetadata,
@ -6,6 +6,7 @@ import {
import AddTaskButton from "./AddTaskButton";
import { Namespaces } from "../../../../i18n/i18n";
import { useTranslation } from "react-i18next";
import { SelectableList } from "../../../../components/common/SelectableList";
interface TaskListProps {
tasks: TaskDefinition[];
@ -16,14 +17,30 @@ interface TaskListProps {
const TaskList: React.FC<TaskListProps> = ({ tasks, taskType, onChange }) => {
const { t: tTaskType } = useTranslation(Namespaces.TaskTypes);
const [currentTask, setCurrentTask] = useState<TaskDefinition | null>(null);
const formatNewTaskName = (
displayName: string,
tasks: TaskDefinition<Record<string, unknown>>[],
) => {
return `${tTaskType(displayName)} ${tasks.length + 1}`;
};
const handleAddTask = (selectedType: TaskMetadata) => {
const formattedName = formatNewTaskName(selectedType.displayName, tasks);
const newTask: TaskDefinition = {
type: selectedType.taskType,
config: { name: tTaskType(selectedType.displayName) },
config: {
name: formattedName,
guid: crypto.randomUUID(),
},
};
console.log("Add Task clicked");
if (tasks.length === 0) {
setCurrentTask(newTask);
}
onChange([...tasks, newTask]);
};
@ -31,11 +48,12 @@ const TaskList: React.FC<TaskListProps> = ({ tasks, taskType, onChange }) => {
<div>
<AddTaskButton taskType={taskType} onAdd={handleAddTask} />
<ul>
{tasks.map((task, index) => (
<li key={index}>{task.type}</li>
))}
</ul>
<SelectableList
items={tasks}
selectedValue={currentTask}
renderLabel={(x) => x.config.name as string}
onSelect={(item) => setCurrentTask(item)}
/>
</div>
);
};

View File

@ -30,7 +30,7 @@ export type ReadWorkflowTemplateVersion = {
export interface TaskDefinition<TConfig = Record<string, unknown>> {
type: string;
config?: TConfig;
config: TConfig;
}
export interface CreateWorkflowTemplateVersion extends FormData {