diff --git a/src/dispatch/__init__.py b/src/dispatch/__init__.py index 4f9fc2738f83..021559539f51 100644 --- a/src/dispatch/__init__.py +++ b/src/dispatch/__init__.py @@ -73,6 +73,7 @@ ) from dispatch.forms.type.models import FormsType # noqa lgtm[py/unused-import] from dispatch.forms.models import Forms # noqa lgtm[py/unused-import] + from dispatch.email_templates.models import EmailTemplates # noqa lgtm[py/unused-import] except Exception: diff --git a/src/dispatch/api.py b/src/dispatch/api.py index e532d9de2b03..27b47843ecab 100644 --- a/src/dispatch/api.py +++ b/src/dispatch/api.py @@ -41,6 +41,7 @@ from dispatch.project.views import router as project_router from dispatch.forms.views import router as forms_router from dispatch.forms.type.views import router as forms_type_router +from dispatch.email_templates.views import router as email_template_router from dispatch.signal.views import router as signal_router @@ -233,7 +234,7 @@ def get_organization_path(organization: OrganizationSlug): authenticated_organization_api_router.include_router( forms_type_router, prefix="/forms_type", tags=["forms_type"] ) - +authenticated_organization_api_router.include_router(email_template_router, prefix="/email_template", tags=["email_template"]) @api_router.get("/healthcheck", include_in_schema=False) def healthcheck(): diff --git a/src/dispatch/database/revisions/tenant/versions/2024-03-11_91bd05855ad1.py b/src/dispatch/database/revisions/tenant/versions/2024-03-11_91bd05855ad1.py new file mode 100644 index 000000000000..0f70337b1937 --- /dev/null +++ b/src/dispatch/database/revisions/tenant/versions/2024-03-11_91bd05855ad1.py @@ -0,0 +1,41 @@ +"""Adds email template table + +Revision ID: 91bd05855ad1 +Revises: 27e6558e26a8 +Create Date: 2024-03-11 10:40:08.313520 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "91bd05855ad1" +down_revision = "27e6558e26a8" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "email_templates", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("email_template_type", sa.String(), nullable=True), + sa.Column("welcome_text", sa.String(), nullable=True), + sa.Column("welcome_body", sa.String(), nullable=True), + sa.Column("components", sa.String(), nullable=True), + sa.Column("enabled", sa.Boolean(), nullable=True), + sa.Column("project_id", sa.Integer(), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("updated_at", sa.DateTime(), nullable=True), + sa.ForeignKeyConstraint(["project_id"], ["project.id"], ondelete="CASCADE"), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email_template_type", "project_id"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("email_templates") + # ### end Alembic commands ### diff --git a/src/dispatch/email_templates/enums.py b/src/dispatch/email_templates/enums.py new file mode 100644 index 000000000000..16ad719a9612 --- /dev/null +++ b/src/dispatch/email_templates/enums.py @@ -0,0 +1,5 @@ +from dispatch.enums import DispatchEnum + + +class EmailTemplateTypes(DispatchEnum): + welcome = "Incident Welcome Email" diff --git a/src/dispatch/email_templates/models.py b/src/dispatch/email_templates/models.py new file mode 100644 index 000000000000..5982676979b9 --- /dev/null +++ b/src/dispatch/email_templates/models.py @@ -0,0 +1,49 @@ +from datetime import datetime +from pydantic import Field +from typing import Optional, List + +from sqlalchemy import Column, Integer, String, Boolean, UniqueConstraint + +from dispatch.database.core import Base +from dispatch.models import DispatchBase, TimeStampMixin, PrimaryKey, Pagination, ProjectMixin +from dispatch.project.models import ProjectRead + + +class EmailTemplates(TimeStampMixin, ProjectMixin, Base): + __table_args__ = (UniqueConstraint("email_template_type", "project_id"),) + # Columns + id = Column(Integer, primary_key=True) + email_template_type = Column(String, nullable=True) + welcome_text = Column(String, nullable=True) + welcome_body = Column(String, nullable=True) + components = Column(String, nullable=True) + enabled = Column(Boolean, default=True) + + +# Pydantic models +class EmailTemplatesBase(DispatchBase): + email_template_type: Optional[str] = Field(None, nullable=True) + welcome_text: Optional[str] = Field(None, nullable=True) + welcome_body: Optional[str] = Field(None, nullable=True) + components: Optional[str] = Field(None, nullable=True) + enabled: Optional[bool] + + +class EmailTemplatesCreate(EmailTemplatesBase): + project: Optional[ProjectRead] + + +class EmailTemplatesUpdate(EmailTemplatesBase): + id: PrimaryKey = None + + +class EmailTemplatesRead(EmailTemplatesBase): + id: PrimaryKey + project: Optional[ProjectRead] + created_at: Optional[datetime] = None + updated_at: Optional[datetime] = None + + +class EmailTemplatesPagination(Pagination): + items: List[EmailTemplatesRead] + total: int diff --git a/src/dispatch/email_templates/service.py b/src/dispatch/email_templates/service.py new file mode 100644 index 000000000000..279834939e73 --- /dev/null +++ b/src/dispatch/email_templates/service.py @@ -0,0 +1,78 @@ +import logging +from typing import List, Optional + +from sqlalchemy.orm import Session + +from .models import EmailTemplates, EmailTemplatesUpdate, EmailTemplatesCreate +from dispatch.project import service as project_service + +log = logging.getLogger(__name__) + + +def get(*, email_template_id: int, db_session: Session) -> Optional[EmailTemplates]: + """Gets an email template by its id.""" + return ( + db_session.query(EmailTemplates) + .filter(EmailTemplates.id == email_template_id) + .one_or_none() + ) + + +def get_by_type(*, email_template_type: str, project_id: int, db_session: Session) -> Optional[EmailTemplates]: + """Gets an email template by its type.""" + return ( + db_session.query(EmailTemplates) + .filter(EmailTemplates.project_id == project_id) + .filter(EmailTemplates.email_template_type == email_template_type) + .filter(EmailTemplates.enabled == True) # noqa + .first() + ) + + +def get_all(*, db_session: Session) -> List[Optional[EmailTemplates]]: + """Gets all email templates.""" + return db_session.query(EmailTemplates) + + +def create(*, email_template_in: EmailTemplatesCreate, db_session: Session) -> EmailTemplates: + """Creates email template data.""" + project = project_service.get_by_name_or_raise( + db_session=db_session, project_in=email_template_in.project + ) + + email_template = EmailTemplates( + **email_template_in.dict(exclude={"project"}), project=project + ) + + db_session.add(email_template) + db_session.commit() + return email_template + + +def update( + *, + email_template: EmailTemplates, + email_template_in: EmailTemplatesUpdate, + db_session: Session, +) -> EmailTemplates: + """Updates an email template.""" + new_template = email_template.dict() + update_data = email_template_in.dict(skip_defaults=True) + + for field in new_template: + if field in update_data: + setattr(email_template, field, update_data[field]) + + db_session.commit() + return email_template + + +def delete(*, db_session, email_template_id: int): + """Deletes an email template.""" + email_template = ( + db_session.query(EmailTemplates) + .filter(EmailTemplates.id == email_template_id) + .one_or_none() + ) + db_session.delete(email_template) + db_session.commit() diff --git a/src/dispatch/email_templates/views.py b/src/dispatch/email_templates/views.py new file mode 100644 index 000000000000..5bc4abb3099f --- /dev/null +++ b/src/dispatch/email_templates/views.py @@ -0,0 +1,110 @@ +import logging +from fastapi import APIRouter, HTTPException, status, Depends +from pydantic.error_wrappers import ErrorWrapper, ValidationError + +from sqlalchemy.exc import IntegrityError + +from dispatch.auth.permissions import ( + SensitiveProjectActionPermission, + PermissionsDependency, +) +from dispatch.database.core import DbSession +from dispatch.auth.service import CurrentUser +from dispatch.database.service import search_filter_sort_paginate, CommonParameters +from dispatch.models import PrimaryKey +from dispatch.exceptions import ExistsError + +from .models import EmailTemplatesRead, EmailTemplatesUpdate, EmailTemplatesPagination, EmailTemplatesCreate +from .service import get, create, update, delete + +log = logging.getLogger(__name__) +router = APIRouter() + + +@router.get("", response_model=EmailTemplatesPagination) +def get_email_templates(commons: CommonParameters): + """Get all email templates, or only those matching a given search term.""" + return search_filter_sort_paginate(model="EmailTemplates", **commons) + + +@router.get("/{email_template_id}", response_model=EmailTemplatesRead) +def get_email_template(db_session: DbSession, email_template_id: PrimaryKey): + """Get an email template by its id.""" + email_template = get(db_session=db_session, email_template_id=email_template_id) + if not email_template: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=[{"msg": "An email template with this id does not exist."}], + ) + return email_template + + +@router.post("", response_model=EmailTemplatesRead) +def create_email_template( + db_session: DbSession, + email_template_in: EmailTemplatesCreate, + current_user: CurrentUser, +): + """Create a new email template.""" + try: + return create( + db_session=db_session, email_template_in=email_template_in + ) + except IntegrityError: + raise ValidationError( + [ + ErrorWrapper( + ExistsError(msg="An email template with this type already exists."), loc="name" + ) + ], + model=EmailTemplatesRead, + ) from None + + +@router.put( + "/{email_template_id}", + response_model=EmailTemplatesRead, + dependencies=[Depends(PermissionsDependency([SensitiveProjectActionPermission]))], +) +def update_email_template( + db_session: DbSession, + email_template_id: PrimaryKey, + email_template_in: EmailTemplatesUpdate, +): + """Update a search filter.""" + email_template = get(db_session=db_session, email_template_id=email_template_id) + if not email_template: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=[{"msg": "An email template with this id does not exist."}], + ) + try: + email_template = update( + db_session=db_session, email_template=email_template, email_template_in=email_template_in + ) + except IntegrityError: + raise ValidationError( + [ + ErrorWrapper( + ExistsError(msg="An email template with this type already exists."), loc="name" + ) + ], + model=EmailTemplatesUpdate, + ) from None + return email_template + + +@router.delete( + "/{email_template_id}", + response_model=None, + dependencies=[Depends(PermissionsDependency([SensitiveProjectActionPermission]))], +) +def delete_email_template(db_session: DbSession, email_template_id: PrimaryKey): + """Delete an email template, returning only an HTTP 200 OK if successful.""" + email_template = get(db_session=db_session, email_template_id=email_template_id) + if not email_template: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=[{"msg": "An email template with this id does not exist."}], + ) + delete(db_session=db_session, email_template_id=email_template_id) diff --git a/src/dispatch/incident/messaging.py b/src/dispatch/incident/messaging.py index 19be4b0f1b87..6c862712abc3 100644 --- a/src/dispatch/incident/messaging.py +++ b/src/dispatch/incident/messaging.py @@ -4,13 +4,19 @@ :copyright: (c) 2019 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ + import logging +from typing import Optional + from dispatch.decorators import timer from dispatch.config import DISPATCH_UI_URL from dispatch.conversation.enums import ConversationCommands from dispatch.database.core import SessionLocal, resolve_attr from dispatch.document import service as document_service +from dispatch.email_templates.models import EmailTemplates +from dispatch.email_templates import service as email_template_service +from dispatch.email_templates.enums import EmailTemplateTypes from dispatch.event import service as event_service from dispatch.incident.enums import IncidentStatus from dispatch.incident.models import Incident, IncidentRead @@ -30,7 +36,6 @@ INCIDENT_NOTIFICATION_COMMON, INCIDENT_OPEN_TASKS, INCIDENT_PARTICIPANT_SUGGESTED_READING_ITEM, - INCIDENT_PARTICIPANT_WELCOME_MESSAGE, INCIDENT_PRIORITY_CHANGE, INCIDENT_REVIEW_DOCUMENT, INCIDENT_SEVERITY_CHANGE, @@ -38,6 +43,7 @@ INCIDENT_TYPE_CHANGE, INCIDENT_COMPLETED_FORM_MESSAGE, MessageType, + generate_welcome_message, ) from dispatch.participant import service as participant_service from dispatch.participant_role import service as participant_role_service @@ -67,7 +73,11 @@ def get_suggested_documents(db_session, incident: Incident) -> list: def send_welcome_ephemeral_message_to_participant( - participant_email: str, incident: Incident, db_session: SessionLocal + *, + participant_email: str, + incident: Incident, + db_session: SessionLocal, + welcome_template: Optional[EmailTemplates] = None, ): """Sends an ephemeral welcome message to the participant.""" if not incident.conversation: @@ -135,7 +145,7 @@ def send_welcome_ephemeral_message_to_participant( incident.conversation.channel_id, participant_email, "Incident Welcome Message", - INCIDENT_PARTICIPANT_WELCOME_MESSAGE, + generate_welcome_message(welcome_template), MessageType.incident_participant_welcome, **message_kwargs, ) @@ -144,7 +154,11 @@ def send_welcome_ephemeral_message_to_participant( def send_welcome_email_to_participant( - participant_email: str, incident: Incident, db_session: SessionLocal + *, + participant_email: str, + incident: Incident, + db_session: SessionLocal, + welcome_template: Optional[EmailTemplates] = None, ): """Sends a welcome email to the participant.""" # we load the incident instance @@ -209,7 +223,7 @@ def send_welcome_email_to_participant( plugin.instance.send( participant_email, notification_text, - INCIDENT_PARTICIPANT_WELCOME_MESSAGE, + generate_welcome_message(welcome_template), MessageType.incident_participant_welcome, **message_kwargs, ) @@ -219,9 +233,7 @@ def send_welcome_email_to_participant( log.debug(f"Welcome email sent to {participant_email}.") -def send_completed_form_email( - participant_email: str, form: Forms, db_session: SessionLocal -): +def send_completed_form_email(participant_email: str, form: Forms, db_session: SessionLocal): """Sends an email to notify about a completed incident form.""" plugin = plugin_service.get_active_instance( db_session=db_session, project_id=form.project.id, plugin_type="email" @@ -270,14 +282,33 @@ def send_completed_form_email( @timer def send_incident_welcome_participant_messages( - participant_email: str, incident: Incident, db_session: SessionLocal + participant_email: str, + incident: Incident, + db_session: SessionLocal, ): """Sends welcome messages to the participant.""" + # check to see if there is an override welcome message template + welcome_template = email_template_service.get_by_type( + db_session=db_session, + project_id=incident.project_id, + email_template_type=EmailTemplateTypes.welcome, + ) + # we send the welcome ephemeral message - send_welcome_ephemeral_message_to_participant(participant_email, incident, db_session) + send_welcome_ephemeral_message_to_participant( + participant_email=participant_email, + incident=incident, + db_session=db_session, + welcome_template=welcome_template, + ) # we send the welcome email - send_welcome_email_to_participant(participant_email, incident, db_session) + send_welcome_email_to_participant( + participant_email=participant_email, + incident=incident, + db_session=db_session, + welcome_template=welcome_template, + ) log.debug(f"Welcome participant messages sent {participant_email}.") diff --git a/src/dispatch/messaging/strings.py b/src/dispatch/messaging/strings.py index 4eea22d27074..4dad13923673 100644 --- a/src/dispatch/messaging/strings.py +++ b/src/dispatch/messaging/strings.py @@ -1,12 +1,13 @@ import copy -from typing import List +from typing import List, Optional from dispatch.messaging.email.filters import env from dispatch.conversation.enums import ConversationButtonActions from dispatch.incident.enums import IncidentStatus from dispatch.case.enums import CaseStatus from dispatch.enums import Visibility +from dispatch.email_templates.models import EmailTemplates from dispatch import config from dispatch.enums import DispatchEnum, DocumentResourceTypes, DocumentResourceReferenceTypes @@ -915,3 +916,41 @@ def render_message_template(message_template: List[dict], **kwargs): data.append(d) return data + + +def generate_welcome_message(welcome_message: EmailTemplates) -> Optional[List[dict]]: + """Generates the welcome message.""" + if welcome_message is None: + return INCIDENT_PARTICIPANT_WELCOME_MESSAGE + + participant_welcome = { + "title": welcome_message.welcome_text, + "title_link": "{{ticket_weblink}}", + "text": welcome_message.welcome_body, + } + + component_mapping = { + "Title": INCIDENT_TITLE, + "Description": INCIDENT_DESCRIPTION, + "Visibility": INCIDENT_VISIBILITY, + "Status": INCIDENT_STATUS, + "Type": INCIDENT_TYPE, + "Severity": INCIDENT_SEVERITY, + "Priority": INCIDENT_PRIORITY, + "Reporter": INCIDENT_REPORTER, + "Commander": INCIDENT_COMMANDER, + "Investigation Document": INCIDENT_INVESTIGATION_DOCUMENT, + "Storage": INCIDENT_STORAGE, + "Conference": INCIDENT_CONFERENCE, + "Slack Commands": INCIDENT_CONVERSATION_COMMANDS_REFERENCE_DOCUMENT, + "FAQ Document": INCIDENT_FAQ_DOCUMENT, + } + + message = [participant_welcome] + + for component in component_mapping.keys(): + # if the component type is in welcome_message.components, then add it + if component in welcome_message.components: + message.append(component_mapping[component]) + + return message diff --git a/src/dispatch/static/dispatch/src/email_templates/DeleteDialog.vue b/src/dispatch/static/dispatch/src/email_templates/DeleteDialog.vue new file mode 100644 index 000000000000..86210e31e225 --- /dev/null +++ b/src/dispatch/static/dispatch/src/email_templates/DeleteDialog.vue @@ -0,0 +1,35 @@ + + + diff --git a/src/dispatch/static/dispatch/src/email_templates/NewEditSheet.vue b/src/dispatch/static/dispatch/src/email_templates/NewEditSheet.vue new file mode 100644 index 000000000000..883a1522c408 --- /dev/null +++ b/src/dispatch/static/dispatch/src/email_templates/NewEditSheet.vue @@ -0,0 +1,161 @@ + + + + + diff --git a/src/dispatch/static/dispatch/src/email_templates/Table.vue b/src/dispatch/static/dispatch/src/email_templates/Table.vue new file mode 100644 index 000000000000..456f8549816b --- /dev/null +++ b/src/dispatch/static/dispatch/src/email_templates/Table.vue @@ -0,0 +1,169 @@ + + + diff --git a/src/dispatch/static/dispatch/src/email_templates/api.js b/src/dispatch/static/dispatch/src/email_templates/api.js new file mode 100644 index 000000000000..f00c4f82448f --- /dev/null +++ b/src/dispatch/static/dispatch/src/email_templates/api.js @@ -0,0 +1,25 @@ +import API from "@/api" + +const resource = "/email_template" + +export default { + getAll(options) { + return API.get(`${resource}`, { params: { ...options } }) + }, + + get(emailTemplateId) { + return API.get(`${resource}/${emailTemplateId}`) + }, + + create(payload) { + return API.post(`${resource}`, payload) + }, + + update(emailTemplateId, payload) { + return API.put(`${resource}/${emailTemplateId}`, payload) + }, + + delete(emailTemplateId) { + return API.delete(`${resource}/${emailTemplateId}`) + }, +} diff --git a/src/dispatch/static/dispatch/src/email_templates/store.js b/src/dispatch/static/dispatch/src/email_templates/store.js new file mode 100644 index 000000000000..cc56d5847494 --- /dev/null +++ b/src/dispatch/static/dispatch/src/email_templates/store.js @@ -0,0 +1,168 @@ +import { getField, updateField } from "vuex-map-fields" +import { debounce } from "lodash" + +import SearchUtils from "@/search/utils" +import EmailTemplatesApi from "@/email_templates/api" + +const getDefaultSelectedState = () => { + return { + id: null, + email_template_type: null, + welcome_text: null, + welcome_body: null, + components: "[]", + enabled: true, + project: null, + created_at: null, + updated_at: null, + loading: false, + } +} + +const state = { + selected: { + ...getDefaultSelectedState(), + }, + dialogs: { + showCreateEdit: false, + showRemove: false, + }, + table: { + rows: { + items: [], + total: null, + }, + options: { + q: "", + page: 1, + itemsPerPage: 25, + sortBy: ["email_template_type"], + descending: [false], + filters: { + project: [], + }, + }, + loading: false, + }, +} + +const getters = { + getField, +} + +const actions = { + getAll: debounce(({ commit, state }) => { + commit("SET_TABLE_LOADING", "primary") + let params = SearchUtils.createParametersFromTableOptions( + { ...state.table.options }, + "EmailTemplates" + ) + return EmailTemplatesApi.getAll(params) + .then((response) => { + commit("SET_TABLE_LOADING", false) + commit("SET_TABLE_ROWS", response.data) + }) + .catch(() => { + commit("SET_TABLE_LOADING", false) + }) + }, 500), + createEditShow({ commit }, emailTemplate) { + commit("SET_DIALOG_CREATE_EDIT", true) + if (emailTemplate) { + commit("SET_SELECTED", emailTemplate) + } + }, + removeShow({ commit }, emailTemplate) { + commit("SET_DIALOG_DELETE", true) + commit("SET_SELECTED", emailTemplate) + }, + closeCreateEdit({ commit }) { + commit("SET_DIALOG_CREATE_EDIT", false) + commit("RESET_SELECTED") + }, + closeRemove({ commit }) { + commit("SET_DIALOG_DELETE", false) + commit("RESET_SELECTED") + }, + save({ commit, dispatch }) { + commit("SET_SELECTED_LOADING", true) + if (!state.selected.id) { + return EmailTemplatesApi.create(state.selected) + .then(() => { + commit("SET_SELECTED_LOADING", false) + dispatch("closeCreateEdit") + dispatch("getAll") + commit( + "notification_backend/addBeNotification", + { text: "Email template created successfully.", type: "success" }, + { root: true } + ) + }) + .catch(() => { + commit("SET_SELECTED_LOADING", false) + }) + } else { + return EmailTemplatesApi.update(state.selected.id, state.selected) + .then(() => { + commit("SET_SELECTED_LOADING", false) + dispatch("closeCreateEdit") + dispatch("getAll") + commit( + "notification_backend/addBeNotification", + { text: "Email template updated successfully.", type: "success" }, + { root: true } + ) + }) + .catch(() => { + commit("SET_SELECTED_LOADING", false) + }) + } + }, + remove({ commit, dispatch }) { + return EmailTemplatesApi.delete(state.selected.id).then(function () { + dispatch("closeRemove") + dispatch("getAll") + commit( + "notification_backend/addBeNotification", + { text: "Email template deleted successfully.", type: "success" }, + { root: true } + ) + }) + }, +} + +const mutations = { + updateField, + SET_SELECTED(state, value) { + state.selected = Object.assign(state.selected, value) + }, + SET_SELECTED_LOADING(state, value) { + state.selected.loading = value + }, + SET_TABLE_LOADING(state, value) { + state.table.loading = value + }, + SET_TABLE_ROWS(state, value) { + state.table.rows = value + }, + SET_DIALOG_CREATE_EDIT(state, value) { + state.dialogs.showCreateEdit = value + }, + SET_DIALOG_DELETE(state, value) { + state.dialogs.showRemove = value + }, + RESET_SELECTED(state) { + // do not reset project + let project = state.selected.project + state.selected = { ...getDefaultSelectedState() } + state.selected.project = project + }, +} + +export default { + namespaced: true, + state, + getters, + actions, + mutations, +} diff --git a/src/dispatch/static/dispatch/src/router/config.js b/src/dispatch/static/dispatch/src/router/config.js index 17758925e111..2c502728e49a 100644 --- a/src/dispatch/static/dispatch/src/router/config.js +++ b/src/dispatch/static/dispatch/src/router/config.js @@ -433,6 +433,12 @@ export const protectedRoute = [ meta: { title: "Cost Models", subMenu: "project", group: "general" }, component: () => import("@/cost_model/Table.vue"), }, + { + path: "emailTemplates", + name: "emailTemplatesTable", + meta: { title: "Email Templates", subMenu: "project", group: "general" }, + component: () => import("@/email_templates/Table.vue"), + }, { path: "incidentTypes", name: "IncidentTypeTable", diff --git a/src/dispatch/static/dispatch/src/store.js b/src/dispatch/static/dispatch/src/store.js index 1aebfaf51ce1..55c4339952af 100644 --- a/src/dispatch/static/dispatch/src/store.js +++ b/src/dispatch/static/dispatch/src/store.js @@ -9,6 +9,7 @@ import case_type from "@/case/type/store" import cost_model from "@/cost_model/store" import definition from "@/definition/store" import document from "@/document/store" +import email_templates from "@/email_templates/store" import entity from "@/entity/store" import entity_type from "@/entity_type/store" import forms from "@/forms/store" @@ -60,6 +61,7 @@ export default createStore({ case_type, definition, document, + email_templates, entity, entity_type, forms,