From eb0c4da7a113bd5fdda822b9155feb63d6d57cb6 Mon Sep 17 00:00:00 2001 From: djnunez-aot <103138766+djnunez-aot@users.noreply.github.com> Date: Fri, 4 Aug 2023 17:23:39 -0400 Subject: [PATCH] Subscribe Form Cards (#1966) --- .../src/met_api/models/widgets_subscribe.py | 2 +- .../src/met_api/resources/widget_subscribe.py | 30 +++-- .../services/widget_subscribe_service.py | 7 +- .../Subscribe/EmailListFormDrawer.tsx | 44 ++++++- .../Subscribe/SubscribeContext.tsx | 8 +- .../Subscribe/SubscribeForm.tsx | 57 +++++---- .../Subscribe/SubscribeInfoBlock.tsx | 116 ++++++++++++++++++ .../Subscribe/SubscribeInfoPaper.tsx | 108 ++++++++++++++++ met-web/src/models/subscription.ts | 20 ++- .../src/services/subscriptionService/index.ts | 14 +-- 10 files changed, 347 insertions(+), 59 deletions(-) create mode 100644 met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoBlock.tsx create mode 100644 met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx diff --git a/met-api/src/met_api/models/widgets_subscribe.py b/met-api/src/met_api/models/widgets_subscribe.py index 9af5417cd..11a1017e2 100644 --- a/met-api/src/met_api/models/widgets_subscribe.py +++ b/met-api/src/met_api/models/widgets_subscribe.py @@ -40,7 +40,7 @@ def get_all_by_type(cls, type_, widget_id): return db.session.query(cls).filter_by(type=type_, widget_id=widget_id).all() @classmethod - def update_widget_events_bulk(cls, update_mappings: list) -> list[WidgetSubscribe]: + def update_widget_subscribes_bulk(cls, update_mappings: list) -> list[WidgetSubscribe]: """Save widget subscribe sorting.""" db.session.bulk_update_mappings(WidgetSubscribe, update_mappings) db.session.commit() diff --git a/met-api/src/met_api/resources/widget_subscribe.py b/met-api/src/met_api/resources/widget_subscribe.py index c9729aff0..180341b22 100644 --- a/met-api/src/met_api/resources/widget_subscribe.py +++ b/met-api/src/met_api/resources/widget_subscribe.py @@ -33,7 +33,7 @@ """ -@cors_preflight('GET, POST, OPTIONS, DELETE') +@cors_preflight('GET, POST, OPTIONS') @API.route('') class WidgetSubscribe(Resource): """Resource for managing a Widget Subscribe.""" @@ -59,17 +59,6 @@ def post(widget_id): except BusinessException as err: return str(err), err.status_code - @staticmethod - @cross_origin(origins=allowedorigins()) - def delete(widget_id, subscribe_id): - """Delete an subscribe .""" - try: - WidgetSubscribeService().delete_subscribe(subscribe_id, widget_id) - response, status = {}, HTTPStatus.OK - except BusinessException as err: - response, status = str(err), err.status_code - return response, status - @cors_preflight('GET,POST,OPTIONS') @API.route('//items', methods=['GET', 'DELETE', 'OPTIONS']) @@ -124,3 +113,20 @@ def patch(widget_id): return WidgetSubscribeSchema().dump(sort_widget_subscribe), HTTPStatus.OK except BusinessException as err: return str(err), err.status_code + + +@cors_preflight('DELETE') +@API.route('/', methods=['DELETE']) +class WidgetEvent(Resource): + """Resource for managing a Widget Events.""" + + @staticmethod + @cross_origin(origins=allowedorigins()) + def delete(widget_id, subscribe_id): + """Delete an subscribe .""" + try: + WidgetSubscribeService().delete_subscribe(subscribe_id, widget_id) + response, status = {}, HTTPStatus.OK + except BusinessException as err: + response, status = str(err), err.status_code + return response, status diff --git a/met-api/src/met_api/services/widget_subscribe_service.py b/met-api/src/met_api/services/widget_subscribe_service.py index 3a8eab822..0224e9b7f 100644 --- a/met-api/src/met_api/services/widget_subscribe_service.py +++ b/met-api/src/met_api/services/widget_subscribe_service.py @@ -115,13 +115,16 @@ def update_subscribe_item(widget_id, subscribe_id, item_id, request_json): raise BusinessException( error='Invalid widgets and subscribe', status_code=HTTPStatus.BAD_REQUEST) + subscribe_item: SubscribeItemsModel = SubscribeItemsModel.find_by_id( item_id) - if subscribe_item.widget_subscribes_id != subscribe_id: + if subscribe_item.widget_subscribe_id != subscribe_id: raise BusinessException( error='Invalid widgets and subscribe', status_code=HTTPStatus.BAD_REQUEST) - WidgetSubscribeService._update_from_dict(subscribe_item, request_json) + + WidgetSubscribeService._update_from_dict( + subscribe_item, request_json) subscribe_item.commit() return SubscribeItemsModel.find_by_id(item_id) diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx index 861fb2b10..8203f7cd8 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/EmailListFormDrawer.tsx @@ -14,11 +14,10 @@ import { Subscribe_TYPE, SubscribeForm } from 'models/subscription'; import RichTextEditor from 'components/common/RichTextEditor'; import { openNotification } from 'services/notificationService/notificationSlice'; import { getTextFromDraftJsContentState } from 'components/common/RichTextEditor/utils'; -import { postSubscribeForm } from 'services/subscriptionService'; +import { patchSubscribeForm, postSubscribeForm, PatchSubscribeProps } from 'services/subscriptionService'; const schema = yup .object({ - description: yup.string(), call_to_action_type: yup.string(), call_to_action_text: yup .string() @@ -39,23 +38,54 @@ const EmailListDrawer = () => { richEmailListDescription, setRichEmailListDescription, setSubscribe, + subscribeToEdit, + loadSubscribe, } = useContext(SubscribeContext); const [isCreating, setIsCreating] = useState(false); const [initialRichDescription, setInitialRichDescription] = useState(''); + const subscribeItem = subscribeToEdit ? subscribeToEdit.subscribe_items[0] : null; const dispatch = useAppDispatch(); const methods = useForm({ resolver: yupResolver(schema), }); useEffect(() => { - methods.setValue('description', ''); methods.setValue('call_to_action_type', 'link'); methods.setValue('call_to_action_text', 'Click here to sign up'); setInitialRichDescription(richEmailListDescription); }, []); + useEffect(() => { + methods.setValue('call_to_action_type', subscribeItem ? subscribeItem.call_to_action_type : 'link'); + methods.setValue( + 'call_to_action_text', + subscribeItem ? subscribeItem.call_to_action_text : 'Click here to sign up', + ); + setInitialRichDescription(subscribeItem ? subscribeItem.description : richEmailListDescription); + }, [subscribeToEdit]); + const { handleSubmit } = methods; + const updateEmailListForm = async (data: EmailList) => { + const validatedData = await schema.validate(data); + const { call_to_action_type, call_to_action_text } = validatedData; + if (subscribeToEdit && subscribeItem && widget) { + const subscribeUpdatesToPatch = { + description: richEmailListDescription, + call_to_action_type: call_to_action_type, + call_to_action_text: call_to_action_text, + } as PatchSubscribeProps; + + await patchSubscribeForm(widget.id, subscribeToEdit.id, subscribeItem.id, { + ...subscribeUpdatesToPatch, + }); + + loadSubscribe(); + + dispatch(openNotification({ severity: 'success', text: 'EmailListForm was successfully updated' })); + } + }; + const createEmailListForm = async (data: EmailList) => { const validatedData = await schema.validate(data); const { call_to_action_type, call_to_action_text } = validatedData; @@ -73,12 +103,18 @@ const EmailListDrawer = () => { ], }); - setSubscribe((prevWidgetForms: SubscribeForm[]) => [...prevWidgetForms, createdWidgetForm]); + setSubscribe((prevWidgetForms: SubscribeForm[]) => { + const filteredForms = prevWidgetForms.filter((form) => form.type !== Subscribe_TYPE.EMAIL_LIST); + return [...filteredForms, createdWidgetForm]; + }); } dispatch(openNotification({ severity: 'success', text: 'Email list form was successfully created' })); }; const saveForm = async (data: EmailList) => { + if (subscribeToEdit) { + return updateEmailListForm(data); + } return createEmailListForm(data); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx index ceaf9b0bb..ca9fdf22e 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeContext.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { useAppDispatch } from 'hooks'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { Widget, WidgetType } from 'models/widget'; -import { Subscribe, Subscribe_TYPE, SubscribeTypeLabel, SubscribeForm } from 'models/subscription'; +import { Subscribe_TYPE, SubscribeTypeLabel, SubscribeForm } from 'models/subscription'; import { getSubscriptionsForms, sortWidgetSubscribeForms } from 'services/subscriptionService'; import { openNotification } from 'services/notificationService/notificationSlice'; @@ -19,7 +19,7 @@ export interface SubscribeContextProps { setSubscribe: React.Dispatch>; setSubscribeToEdit: React.Dispatch>; handleSubscribeDrawerOpen: (_Subscribe: SubscribeTypeLabel, _open: boolean) => void; - updateWidgetSubscribeSorting: (widget_Subscribe: Subscribe[]) => void; + updateWidgetSubscribeSorting: (widget_Subscribe: SubscribeForm[]) => void; richEmailListDescription: string; setRichEmailListDescription: React.Dispatch>; richFormSignUpDescription: string; @@ -51,7 +51,7 @@ export const SubscribeContext = createContext({ handleSubscribeDrawerOpen: (_Subscribe: SubscribeTypeLabel, _open: boolean) => { /* empty default method */ }, - updateWidgetSubscribeSorting: (widget_Subscribe: Subscribe[]) => { + updateWidgetSubscribeSorting: (widget_Subscribe: SubscribeForm[]) => { /* empty default method */ }, richEmailListDescription: '', @@ -104,7 +104,7 @@ export const SubscribeProvider = ({ children }: { children: JSX.Element | JSX.El loadSubscribe(); }, [widget]); - const updateWidgetSubscribeSorting = async (resortedWidgetSubscribe: Subscribe[]) => { + const updateWidgetSubscribeSorting = async (resortedWidgetSubscribe: SubscribeForm[]) => { if (!widget) { return; } diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx index 90ebfe038..464c13319 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx @@ -5,10 +5,13 @@ import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { SubscribeContext } from './SubscribeContext'; import { Subscribe_TYPE } from 'models/subscription'; import { WidgetTitle } from '../WidgetTitle'; +import { When } from 'react-if'; +import SubscribeInfoBlock from './SubscribeInfoBlock'; const Form = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); - const { handleSubscribeDrawerOpen, widget } = useContext(SubscribeContext); + const { handleSubscribeDrawerOpen, subscribe, widget } = useContext(SubscribeContext); + const subscribeFormExists = subscribe.length > 0; if (!widget) { return null; @@ -20,31 +23,39 @@ const Form = () => { + + + + + The email list will collect email addresses for a mailing list. A "double-opt-in" email will + be sent to confirm the subscription. Only the email addresses that have been double-opted-in + will be on the list. + + + + + The form sign-up will open the pre-defined form. The text and CTA for both are customizable. + + + + + + handleSubscribeDrawerOpen(Subscribe_TYPE.EMAIL_LIST, true)}> + Email List + + + + handleSubscribeDrawerOpen(Subscribe_TYPE.FORM, true)}> + Form Sign-up + + - + - - The email list will collect email addresses for a mailing list. A "double-opt-in" email will be - sent to confirm the subscription. Only the email addresses that have been double-opted-in will - be on the list. - - - - - The form sign-up will open the pre-defined form. The text and CTA for both are customizable. - - - - handleSubscribeDrawerOpen(Subscribe_TYPE.EMAIL_LIST, true)}> - Email List - - - - handleSubscribeDrawerOpen(Subscribe_TYPE.FORM, true)}> - Form Sign-up - + - + + { + const { subscribe, setSubscribe, isLoadingSubscribe, updateWidgetSubscribeSorting, widget } = + useContext(SubscribeContext); + const dispatch = useAppDispatch(); + const debounceUpdateWidgetSubscribeSorting = useRef( + debounce((widgetSubscribeToSort: SubscribeForm[]) => { + updateWidgetSubscribeSorting(widgetSubscribeToSort); + }, 800), + ).current; + + const moveSubscribeForm = (result: DropResult) => { + if (!result.destination) { + return; + } + + const items = reorder(subscribe, result.source.index, result.destination.index); + + setSubscribe(items); + + debounceUpdateWidgetSubscribeSorting(items); + }; + + if (isLoadingSubscribe) { + return ( + + + + + + ); + } + + const handleRemoveSubscribeForm = (subscribeFormId: number) => { + dispatch( + openNotificationModal({ + open: true, + data: { + header: 'Remove SubscribeForm', + subText: [ + { + text: 'You will be removing this subscribeForm from the engagement.', + }, + { + text: 'Do you want to remove this subscribeForm?', + }, + ], + handleConfirm: () => { + removeSubscribeForm(subscribeFormId); + }, + }, + type: 'confirm', + }), + ); + }; + + const removeSubscribeForm = async (subscribeFormId: number) => { + try { + if (widget) { + await deleteSubscribeForm(widget.id, subscribeFormId); + const newSubscribe = subscribe.filter((subscribeForm) => subscribeForm.id !== subscribeFormId); + setSubscribe([...newSubscribe]); + dispatch(openNotification({ severity: 'success', text: 'The subscribeForm was removed successfully' })); + } + } catch (error) { + dispatch( + openNotification({ severity: 'error', text: 'An error occurred while trying to remove subscribeForm' }), + ); + } + }; + + return ( + + + + {subscribe.map((subscribeForm: SubscribeForm, index) => { + return ( + + + + + + + + + + + ); + })} + + + + ); +}; + +export default SubscribeInfoBlock; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx new file mode 100644 index 000000000..be9727e98 --- /dev/null +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeInfoPaper.tsx @@ -0,0 +1,108 @@ +import React, { useContext } from 'react'; +import { MetParagraph, MetWidgetPaper } from 'components/common'; +import { Grid, IconButton } from '@mui/material'; +import DragIndicatorIcon from '@mui/icons-material/DragIndicator'; +import HighlightOffIcon from '@mui/icons-material/HighlightOff'; +import EditIcon from '@mui/icons-material/Edit'; +import { When } from 'react-if'; +import { SubscribeForm } from 'models/subscription'; +import { SubscribeContext } from './SubscribeContext'; +import { Editor } from 'react-draft-wysiwyg'; +import { getEditorStateFromRaw } from 'components/common/RichTextEditor/utils'; +import { styled } from '@mui/system'; + +const EditorGrid = styled(Grid)` + padding-top: 0px !important; +`; +export interface SubscribeInfoPaperProps { + subscribeForm: SubscribeForm; + removeSubscribeForm: (_subscribeId: number) => void; +} + +const SubscribeInfoPaper = ({ subscribeForm, removeSubscribeForm, ...rest }: SubscribeInfoPaperProps) => { + const subscribeItem = subscribeForm.subscribe_items[0]; + const { setSubscribeToEdit, handleSubscribeDrawerOpen } = useContext(SubscribeContext); + + function capitalizeFirstLetter(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + return ( + + + + + + + + + + Email List + + + + Description: + + + + + + + + {capitalizeFirstLetter(subscribeItem.call_to_action_type)} + + + + {subscribeItem.call_to_action_text} + + + + + + + { + setSubscribeToEdit(subscribeForm); + handleSubscribeDrawerOpen(subscribeForm.type, true); + }} + /> + + + + removeSubscribeForm(subscribeForm.id)} + sx={{ padding: 1, margin: 0 }} + color="inherit" + aria-label="delete-icon" + > + + + + + + + ); +}; + +export default SubscribeInfoPaper; diff --git a/met-web/src/models/subscription.ts b/met-web/src/models/subscription.ts index ff32b1bab..f42407c4c 100644 --- a/met-web/src/models/subscription.ts +++ b/met-web/src/models/subscription.ts @@ -26,10 +26,24 @@ export const Subscribe_TYPE: { [x: string]: SubscribeTypeLabel } = { }; export interface SubscribeForm { + id: number; + title: string; + type: SubscribeTypeLabel; + sort_index: number; widget_id: number; + created_date: string; + updated_date: string; + subscribe_items: SubscribeFormItem[]; +} + +export interface SubscribeFormItem { + id: number; title?: string; - description?: string; - call_to_action_type?: string; - call_to_action_text?: string; + description: string; + call_to_action_type: 'link' | 'button'; + call_to_action_text: string; form_type: SubscribeTypeLabel; + created_date: string; + updated_date: string; + widget_subscribe: number; } diff --git a/met-web/src/services/subscriptionService/index.ts b/met-web/src/services/subscriptionService/index.ts index 6b811938e..afb213cc5 100644 --- a/met-web/src/services/subscriptionService/index.ts +++ b/met-web/src/services/subscriptionService/index.ts @@ -78,15 +78,9 @@ export const postSubscribeForm = async (widget_id: number, data: PostSubscribePr }; export interface PatchSubscribeProps { - widget_id: number; - title?: string; - type: SubscribeTypeLabel; - items: { - description?: string; - call_to_action_type?: string; - call_to_action_text?: string; - form_type: SubscribeTypeLabel; - }[]; + description?: string; + call_to_action_type?: string; + call_to_action_text?: string; } export const patchSubscribeForm = async ( @@ -133,7 +127,7 @@ export const deleteSubscribeForm = async (widget_id: number, subscribe_id: numbe } }; -export const sortWidgetSubscribeForms = async (widget_id: number, data: Subscribe[]): Promise => { +export const sortWidgetSubscribeForms = async (widget_id: number, data: SubscribeForm[]): Promise => { try { const url = replaceUrl(Endpoints.Subscription.SORT_FORMS, 'widget_id', String(widget_id)); const response = await http.PatchRequest(url, data);