Skip to content

Commit

Permalink
Ot.save html templates (#356)
Browse files Browse the repository at this point in the history
* first draft

* save button and sub form

* working but need to reskin

* formatting

* centered the div boss

* add template testing, route url slash

* Update maps.json conflict

* curly bracket
  • Loading branch information
CerberusLatrans authored May 27, 2024
1 parent 8c7383b commit 9f879f5
Show file tree
Hide file tree
Showing 30 changed files with 215 additions and 51 deletions.
12 changes: 11 additions & 1 deletion src/api/protectedApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
AddSitesRequest,
NameSiteEntryRequest,
SendEmailRequest,
AddTemplateRequest,
} from '../components/forms/ducks/types';
import {
ActivityRequest,
Expand Down Expand Up @@ -143,6 +144,7 @@ export interface ProtectedApiClient {
readonly loadEmailTemplateContent: (
templateName: string,
) => Promise<LoadTemplateResponse>;
readonly addTemplate: (request: AddTemplateRequest) => Promise<void>;
readonly reportSiteForIssues: (
siteId: number,
request: ReportSiteRequest,
Expand Down Expand Up @@ -177,7 +179,8 @@ export enum AdminApiClientRoutes {
GET_STEWARDSHIP_REPORT_CSV = '/api/v1/protected/report/csv/adoption',
ADD_SITES = '/api/v1/protected/sites/add_sites',
SEND_EMAIL = '/api/v1/protected/neighborhoods/send_email',
GET_TEMPLATE_NAMES = 'api/v1/protected/emailer/template_names',
GET_TEMPLATE_NAMES = '/api/v1/protected/emailer/template_names',
ADD_TEMPLATE = '/api/v1/protected/emailer/add_template',
}

const baseTeamRoute = '/api/v1/protected/teams/';
Expand Down Expand Up @@ -620,6 +623,12 @@ const loadEmailTemplateContent = (
).then((res) => res.data);
};

const addTemplate = (request: AddTemplateRequest): Promise<void> => {
return AppAxiosInstance.post(AdminApiClientRoutes.ADD_TEMPLATE, request).then(
(res) => res.data,
);
};

const reportSiteForIssues = (
siteId: number,
request: ReportSiteRequest,
Expand Down Expand Up @@ -682,6 +691,7 @@ const Client: ProtectedApiClient = Object.freeze({
uploadImage,
getEmailTemplateNames,
loadEmailTemplateContent,
addTemplate,
reportSiteForIssues,
});

Expand Down
31 changes: 31 additions & 0 deletions src/api/test/protectedApiClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2212,6 +2212,37 @@ describe('Admin Protected Client Routes', () => {

expect(result).toEqual(response);
});
describe('addTemplate', () => {
it('makes the right request with a name', async () => {
const response = '';

nock(BASE_URL)
.post(AdminApiClientRoutes.ADD_TEMPLATE)
.reply(200, response);

const result = await ProtectedApiClient.addTemplate({
name: 'name',
template: 'body',
});

expect(result).toEqual(response);
});

it('makes an unauthorized request', async () => {
const response = 'Must be an admin';

nock(BASE_URL)
.post(AdminApiClientRoutes.ADD_TEMPLATE)
.reply(400, response);

const result = await ProtectedApiClient.addTemplate({
name: 'name',
template: 'body',
}).catch((err) => err.response.data);

expect(result).toEqual(response);
});
});
});

describe('uploadImage', () => {
Expand Down
5 changes: 5 additions & 0 deletions src/components/forms/ducks/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,8 @@ export interface SendEmailFormValues {
export interface SendEmailRequest extends SendEmailFormValues {
readonly emails: string[];
}

export interface AddTemplateRequest {
readonly name: string;
readonly template: string;
}
37 changes: 32 additions & 5 deletions src/components/forms/sendEmailForm/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { Form, Input, Switch, message } from 'antd';
import { SubmitButton } from '../../../components/themedComponents';
import { Form, Input, Switch, message, Button } from 'antd';
import {
Flex,
SubmitButton,
WhiteButton,
} from '../../../components/themedComponents';
import ProtectedApiClient from '../../../api/protectedApiClient';
import {
SendEmailFormValues,
Expand All @@ -13,6 +17,7 @@ import { site } from '../../../constants';
import { useTranslation } from 'react-i18next';
import { n } from '../../../utils/stringFormat';
import DOMPurify from 'isomorphic-dompurify';
import SaveMenu from '../../saveMenu';

const PreviewSwitch = styled(Switch)`
display: flex;
Expand All @@ -32,6 +37,14 @@ const EmailPreview = styled.div`
overflow-y: scroll; // required for resizing
`;

const WhiteSaveButton = styled(WhiteButton)`
height: 40px;
`;

const EmailFlex = styled(Flex)`
gap: 4px;
`;

interface SendEmailFormProps {
readonly emails: string[];
readonly sendEmailForm: FormInstance<SendEmailRequest>;
Expand All @@ -48,6 +61,7 @@ const SendEmailForm: React.FC<SendEmailFormProps> = ({

const [showPreview, setShowPreview] = useState<boolean>(false);
const [bodyContent, setBodyContent] = useState<string>('');
const [showSave, setShowSave] = useState(false);
const [sanitizedBodyContent, setSanitizedBodyContent] = useState<string>('');

const togglePreview = (isShowPreview: boolean) => {
Expand All @@ -73,6 +87,7 @@ const SendEmailForm: React.FC<SendEmailFormProps> = ({
message.error(t('response_error', { error: err.response.data })),
);
};

return (
<Form
name="sendEmail"
Expand Down Expand Up @@ -110,9 +125,21 @@ const SendEmailForm: React.FC<SendEmailFormProps> = ({
dangerouslySetInnerHTML={{ __html: sanitizedBodyContent }}
/>
)}
<SubmitButton type="primary" htmlType="submit">
{t('send')}
</SubmitButton>
<EmailFlex>
<SubmitButton type="primary" htmlType="submit">
{t('send')}
</SubmitButton>
<WhiteSaveButton
type="text"
size="large"
onClick={() => {
setShowSave(!showSave);
}}
>
{t('save')}
</WhiteSaveButton>
{showSave && <SaveMenu templateBody={bodyContent}></SaveMenu>}
</EmailFlex>
</Form>
);
};
Expand Down
86 changes: 86 additions & 0 deletions src/components/saveMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { useState } from 'react';
import styled from 'styled-components';
import { Input, Button, Typography, message } from 'antd';
import { SaveTwoTone } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import { site } from '../../constants';
import { n } from '../../utils/stringFormat';
import { AddTemplateRequest } from '../forms/ducks/types';
import ProtectedApiClient from '../../api/protectedApiClient';
import { LIGHT_GREEN } from '../../utils/colors';
import DOMPurify from 'isomorphic-dompurify';

const SaveMenuContainer = styled.div`
display: flex;
max-width: 400px;
height: 40px;
gap: 2px;
align-self: end;
`;

const SaveButton = styled(Button)`
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
&:hover {
background-color: #e7ffc7;
}
`;

const NameInput = styled(Input)`
height: 40px;
`;

const LargeSaveTwoTone = styled(SaveTwoTone)`
font-size: 20px;
`;

interface SaveMenuProps {
templateBody: string;
}

const SaveMenu: React.FC<SaveMenuProps> = ({ templateBody }) => {
const { t } = useTranslation(n(site, ['forms']), {
keyPrefix: 'volunteer_emailer',
nsMode: 'fallback',
});

const [templateName, setTemplateName] = useState(``);

const onClickSave = (name: string, template: string) => {
if (name.length === 0) {
message.error(t('name_required'));
return;
}
template = DOMPurify.sanitize(template);
const addTemplateRequest: AddTemplateRequest = {
name,
template,
};
ProtectedApiClient.addTemplate(addTemplateRequest)
.then(() => {
message.success(t('save_success'));
})
.catch((err) =>
message.error(t('response_error', { error: err.response.data })),
);
};
return (
<SaveMenuContainer>
<NameInput
defaultValue={templateName}
value={templateName}
onChange={(e) => setTemplateName(e.target.value)}
placeholder={t('name_template')}
/>
<SaveButton onClick={() => onClickSave(templateName, templateBody)}>
<LargeSaveTwoTone twoToneColor={LIGHT_GREEN} />
</SaveButton>
</SaveMenuContainer>
);
};

export default SaveMenu;
6 changes: 5 additions & 1 deletion src/i18n/en/forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,11 @@
},
"body_required": "The email body is required",
"body_placeholder": "Email Body",
"send": "Send Email"
"send": "Send Email",
"save": "Save as Template",
"name_template": "Name your template",
"name_required": "A template name is required",
"save_success": "Template saved!"
},
"report_site": {
"options": {
Expand Down
2 changes: 1 addition & 1 deletion src/i18n/en/maps.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,4 @@
"privateStreet": "Private streets that volunteers shouldn't enter!"
}
}
}
}
2 changes: 1 addition & 1 deletion src/i18n/es/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
"standard": "Estándar",
"super_admin": "Superadministrador"
}
}
}
2 changes: 1 addition & 1 deletion src/i18n/es/content.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
"nov": "Noviembre",
"dec": "Diciembre"
}
}
}
15 changes: 8 additions & 7 deletions src/i18n/es/faq.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@
},
"answers": {
"one": [
"Haz clic en 'Registrarse' en la esquina superior derecha para crear una cuenta.",
"Una vez que hayas iniciado sesión, ingresa tu dirección en la barra de búsqueda del mapa para encontrar árboles adoptables cerca de ti.",
"Haz clic en el icono del árbol que te gustaría adoptar, ya sea el triángulo azul para un árbol recién plantado o el círculo verde para un árbol más antiguo.",
"Haz clic en 'Registrarse' en la esquina superior derecha para crear una cuenta.",
"Una vez que hayas iniciado sesión, ingresa tu dirección en la barra de búsqueda del mapa para encontrar árboles adoptables cerca de ti.",
"Haz clic en el icono del árbol que te gustaría adoptar, ya sea el triángulo azul para un árbol recién plantado o el círculo verde para un árbol más antiguo.",
"Haz clic en 'Más información'.",
"Haz clic en el botón 'Adoptar' en la siguiente pantalla para adoptar ese árbol. Se agregará a la lista 'Mis Árboles' para que puedas encontrar fácilmente tu árbol y registrar las actividades de cuidado del árbol.",
"Registra cada actividad haciendo clic en 'Mis Árboles', encontrando el árbol que adoptaste y enviando la actividad de cuidado del árbol en la página de ese árbol. ¡Es fácil y nos ayuda a seguir el éxito del programa Adopta un Árbol!"],
"Haz clic en el botón 'Adoptar' en la siguiente pantalla para adoptar ese árbol. Se agregará a la lista 'Mis Árboles' para que puedas encontrar fácilmente tu árbol y registrar las actividades de cuidado del árbol.",
"Registra cada actividad haciendo clic en 'Mis Árboles', encontrando el árbol que adoptaste y enviando la actividad de cuidado del árbol en la página de ese árbol. ¡Es fácil y nos ayuda a seguir el éxito del programa Adopta un Árbol!"
],
"two": {
"one": "Ve a <homeLink>map.treeboston.org/home</homeLink> y haz clic en \"Mis árboles\".",
"two": [
"En el lado derecho de la pantalla, haz clic en \"Más información\" debajo del árbol que te interesa.",
"En el lado derecho de la pantalla, haz clic en \"Más información\" debajo del árbol que te interesa.",
"Deberías ver esto en tu pantalla donde puedes ingresar lo que hiciste y cuándo:"
]
},
Expand All @@ -35,4 +36,4 @@
"seven": "Si tu árbol ha sido retirado o hay un hueco cerca de ti y te gustaría un árbol allí, debes enviar una <boston311Link>solicitud 311</boston311Link> a la ciudad.",
"eight": "Por favor, no plantes nada dentro de los alcorques de los árboles. Puedes poner mantillo alrededor del árbol si quieres y quitar las malas hierbas."
}
}
}
2 changes: 1 addition & 1 deletion src/i18n/es/forgotPassword.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"header": "¿Has olvidado tu contraseña?",
"reset_instructions": "Por favor, ingresa tu correo electrónico y te enviaremos las instrucciones para restablecer la contraseña a la dirección de correo electrónico de esta cuenta.",
"send_reset_email": "Si hay una cuenta registrada bajo {{email}}, te hemos enviado un enlace para restablecer tu contraseña."
}
}
2 changes: 1 addition & 1 deletion src/i18n/es/forgotPasswordReset.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
"header": "¿Has olvidado tu contraseña?",
"reset_success": "Contraseña restablecida con éxito!",
"reset_error": "No se pudo restablecer la contraseña."
}
}
4 changes: 2 additions & 2 deletions src/i18n/es/forms.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"report_required": "¡Debes seleccionar un informe!",
"report_type_placeholder": "Selecciona el informe para descargar.",
"export_data": [
"Exportar datos de los últimos",
"Exportar datos de los últimos",
"días (deja vacío para exportar todos los datos)."
],
"load_report": "Cargar informe"
Expand Down Expand Up @@ -122,4 +122,4 @@
"date_label": "Fecha de la actividad",
"activity_label": "Actividades de cuidado"
}
}
}
2 changes: 1 addition & 1 deletion src/i18n/es/home.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,4 @@
"settings": "Ajustes",
"reports": "Informes"
}
}
}
16 changes: 8 additions & 8 deletions src/i18n/es/landing.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"questionDirections": {
"questions": "¿Todavía tienes preguntas?",
"directions": [
"Visita nuestra página de preguntas frecuentes en <faqLink>FAQ</faqLink> para ver si tu pregunta está respondida.",
"Visita nuestra página de preguntas frecuentes en <faqLink>FAQ</faqLink> para ver si tu pregunta está respondida.",
"Si no, contáctanos en <contactUsLink>[email protected]</contactUsLink>."
]
}
Expand All @@ -23,18 +23,18 @@
"adoptionDirections": {
"header": "Cómo Adoptar un Árbol",
"body": [
"Haz clic en 'Registrarse' en la esquina superior derecha para crear una cuenta.",
"Una vez que hayas iniciado sesión, ingresa tu dirección en la barra de búsqueda del mapa para encontrar árboles adoptables cerca de ti.",
"Haz clic en el ícono del árbol que te gustaría adoptar, ya sea el triángulo azul para un árbol recién plantado o el círculo verde para un árbol más antiguo.",
"Haz clic en 'Más información'.",
"Haz clic en el botón 'Adoptar' en la siguiente pantalla para adoptar ese árbol. Se agregará a la lista 'Mis Árboles' para que puedas encontrar fácilmente tu árbol y registrar tus actividades de cuidado.",
"Haz clic en 'Registrarse' en la esquina superior derecha para crear una cuenta.",
"Una vez que hayas iniciado sesión, ingresa tu dirección en la barra de búsqueda del mapa para encontrar árboles adoptables cerca de ti.",
"Haz clic en el ícono del árbol que te gustaría adoptar, ya sea el triángulo azul para un árbol recién plantado o el círculo verde para un árbol más antiguo.",
"Haz clic en 'Más información'.",
"Haz clic en el botón 'Adoptar' en la siguiente pantalla para adoptar ese árbol. Se agregará a la lista 'Mis Árboles' para que puedas encontrar fácilmente tu árbol y registrar tus actividades de cuidado.",
"Registra cada actividad haciendo clic en 'Mis Árboles', encuentra el árbol que adoptaste y envía la actividad de cuidado en la página de ese árbol. ¡Es fácil!"
]
},
},
"navMenu": {
"myTrees": "Mis Árboles",
"admins": "Administradores",
"accountSettings": "Configuración de la Cuenta",
"logOut": "Cerrar Sesión"
}
}
}
Loading

0 comments on commit 9f879f5

Please sign in to comment.