diff --git a/met-api/src/met_api/models/widget.py b/met-api/src/met_api/models/widget.py index b554abeda..9c55a0e63 100644 --- a/met-api/src/met_api/models/widget.py +++ b/met-api/src/met_api/models/widget.py @@ -4,6 +4,7 @@ """ from __future__ import annotations from datetime import datetime +from typing import Optional from sqlalchemy.sql.schema import ForeignKey @@ -65,6 +66,7 @@ def __create_new_widget_entity(widget): updated_date=datetime.utcnow(), created_by=widget.get('created_by', None), updated_by=widget.get('updated_by', None), + title=widget.get('title', None), ) @classmethod @@ -87,3 +89,15 @@ def update_widgets(cls, update_mappings: list) -> None: """Update widgets..""" db.session.bulk_update_mappings(Widget, update_mappings) db.session.commit() + + @classmethod + def update_widget(cls, engagement_id, widget_id, widget_data: dict) -> Optional[Widget]: + """Update widget.""" + query = Widget.query.filter_by(id=widget_id, engagement_id=engagement_id) + widget: Widget = query.first() + if not widget: + return None + widget_data['updated_date'] = datetime.utcnow() + query.update(widget_data) + db.session.commit() + return widget diff --git a/met-api/src/met_api/resources/widget.py b/met-api/src/met_api/resources/widget.py index 70a518565..26d90dabe 100644 --- a/met-api/src/met_api/resources/widget.py +++ b/met-api/src/met_api/resources/widget.py @@ -89,8 +89,8 @@ def patch(engagement_id): return {'message': err.error}, err.status_code -@cors_preflight('DELETE') -@API.route('/engagement//widget/') +@cors_preflight('DELETE, PATCH') +@API.route('//engagements/') class EngagementWidget(Resource): """Resource for managing widgets with engagements.""" @@ -107,6 +107,25 @@ def delete(engagement_id, widget_id): except ValueError as err: return str(err), HTTPStatus.INTERNAL_SERVER_ERROR + @staticmethod + @cross_origin(origins=allowedorigins()) + @_jwt.requires_auth + def patch(engagement_id, widget_id): + """Update widget.""" + try: + user_id = TokenInfo.get_id() + widget_data = request.get_json() + valid_format, errors = schema_utils.validate(widget_data, 'widget_update') + if not valid_format: + return {'message': schema_utils.serialize(errors)}, HTTPStatus.BAD_REQUEST + + updated_widget = WidgetService().update_widget(engagement_id, widget_id, widget_data, user_id) + return updated_widget, HTTPStatus.OK + except (KeyError, ValueError) as err: + return str(err), HTTPStatus.INTERNAL_SERVER_ERROR + except ValidationError as err: + return str(err.messages), HTTPStatus.INTERNAL_SERVER_ERROR + @cors_preflight('POST,OPTIONS') @API.route('//items') diff --git a/met-api/src/met_api/schemas/schemas/widget_update.json b/met-api/src/met_api/schemas/schemas/widget_update.json new file mode 100644 index 000000000..1e5955ed3 --- /dev/null +++ b/met-api/src/met_api/schemas/schemas/widget_update.json @@ -0,0 +1,23 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://met.gov.bc.ca/.well_known/schemas/widget_update", + "type": "object", + "title": "The root schema", + "description": "The root schema comprises the entire JSON document.", + "default": {}, + "examples": [ + { + "title": "Who is Listening" + } + ], + "required": ["title"], + "properties": { + "title": { + "$id": "#/properties/title", + "type": "string", + "title": "Widget title", + "description": "The title of the widget.", + "examples": ["Who is Listening"] + } + } +} diff --git a/met-api/src/met_api/schemas/widget.py b/met-api/src/met_api/schemas/widget.py index d86147648..c2aec64d7 100644 --- a/met-api/src/met_api/schemas/widget.py +++ b/met-api/src/met_api/schemas/widget.py @@ -14,6 +14,7 @@ class Meta: # pylint: disable=too-few-public-methods unknown = EXCLUDE id = fields.Int(data_key='id') + title = fields.Str(data_key='title') widget_type_id = fields.Int(data_key='widget_type_id', required=True) engagement_id = fields.Int(data_key='engagement_id', required=True) created_by = fields.Str(data_key='created_by') diff --git a/met-api/src/met_api/services/widget_service.py b/met-api/src/met_api/services/widget_service.py index 52383a950..15b744fe4 100644 --- a/met-api/src/met_api/services/widget_service.py +++ b/met-api/src/met_api/services/widget_service.py @@ -65,6 +65,16 @@ def sort_widget(engagement_id, widgets: list, user_id=None): WidgetModel.update_widgets(widget_sort_mappings) + @staticmethod + def update_widget(engagement_id, widget_id: list, widget_data: dict, user_id=None): + """Sort widgets.""" + WidgetService._verify_widget(widget_id) + + widget_data['updated_by'] = user_id + + updated_widget = WidgetModel.update_widget(engagement_id, widget_id, widget_data) + return WidgetSchema().dump(updated_widget) + @staticmethod def _validate_widget_ids(engagement_id, widgets): """Validate if widget ids belong to the engagement.""" @@ -76,6 +86,14 @@ def _validate_widget_ids(engagement_id, widgets): error='Invalid widgets.', status_code=HTTPStatus.BAD_REQUEST) + @staticmethod + def _verify_widget(widget_id): + """Verify if widget exists.""" + widget = WidgetModel.get_widget_by_id(widget_id) + if not widget: + raise KeyError('Widget ' + widget_id + ' does not exist') + return widget + @staticmethod def create_widget_items_bulk(widget_items: list, user_id): """Create widget items in bulk.""" diff --git a/met-web/src/apiManager/apiSlices/widgets/index.ts b/met-web/src/apiManager/apiSlices/widgets/index.ts index 326b7b7c8..f5cf5e12a 100644 --- a/met-web/src/apiManager/apiSlices/widgets/index.ts +++ b/met-web/src/apiManager/apiSlices/widgets/index.ts @@ -25,6 +25,14 @@ export const widgetsApi = createApi({ }), invalidatesTags: ['Widgets'], }), + updateWidget: builder.mutation }>({ + query: ({ engagementId, id, data }) => ({ + url: `widgets/${id}/engagements/${engagementId}`, + method: 'PATCH', + body: data, + }), + invalidatesTags: ['Widgets'], + }), sortWidgets: builder.mutation({ query: ({ engagementId, widgets }) => ({ url: `widgets/engagement/${engagementId}/sort_index`, @@ -35,7 +43,7 @@ export const widgetsApi = createApi({ }), deleteWidget: builder.mutation({ query: ({ engagementId, widgetId }) => ({ - url: `widgets/engagement/${engagementId}/widget/${widgetId}`, + url: `widgets/${widgetId}/engagements/${engagementId}`, method: 'DELETE', }), invalidatesTags: (_result, _error, arg) => [{ type: 'Widgets', id: arg.widgetId }], @@ -47,5 +55,10 @@ export const widgetsApi = createApi({ // Export hooks for usage in functional components, which are // auto-generated based on the defined endpoints -export const { useLazyGetWidgetsQuery, useCreateWidgetMutation, useSortWidgetsMutation, useDeleteWidgetMutation } = - widgetsApi; +export const { + useLazyGetWidgetsQuery, + useCreateWidgetMutation, + useSortWidgetsMutation, + useDeleteWidgetMutation, + useUpdateWidgetMutation, +} = widgetsApi; diff --git a/met-web/src/apiManager/endpoints/index.ts b/met-web/src/apiManager/endpoints/index.ts index fb0ff1448..31cb26f15 100644 --- a/met-web/src/apiManager/endpoints/index.ts +++ b/met-web/src/apiManager/endpoints/index.ts @@ -84,7 +84,7 @@ const Endpoints = { Widgets: { GET_LIST: `${AppConfig.apiUrl}/widgets/engagement/engagement_id`, CREATE: `${AppConfig.apiUrl}/widgets/engagement/engagement_id`, - DELETE: `${AppConfig.apiUrl}/widgets/engagement/engagement_id/widget/widget_id`, + DELETE: `${AppConfig.apiUrl}/widgets/widget_id/engagements/engagement_id`, SORT: `${AppConfig.apiUrl}/widgets/engagement/engagement_id/sort_index`, }, Widget_items: { diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx index 6e86ba413..c1390f397 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentForm.tsx @@ -1,36 +1,41 @@ import React, { useContext } from 'react'; import { Divider, Grid } from '@mui/material'; -import { MetHeader3, PrimaryButton } from 'components/common'; +import { PrimaryButton } from 'components/common'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import CreateFolderForm from './CreateFolderForm'; import DocumentsBlock from './DocumentsBlock'; +import { WidgetTitle } from '../WidgetTitle'; +import { DocumentsContext } from './DocumentsContext'; const DocumentForm = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); + const { widget } = useContext(DocumentsContext); + + if (!widget) { + return null; + } return ( - <> - - - Documents - - + + + + + - - - + + + - - - + + + - - - handleWidgetDrawerOpen(false)}>{`Close`} - + + + handleWidgetDrawerOpen(false)}>{`Close`} - + ); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx index e5f049e1b..578470ce4 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Documents/DocumentOptionCard.tsx @@ -12,6 +12,7 @@ import { openNotification } from 'services/notificationService/notificationSlice import { optionCardStyle } from '../constants'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; +const Title = 'Documents'; const DocumentOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); const { savedEngagement } = useContext(ActionContext); @@ -31,6 +32,7 @@ const DocumentOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Document, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -82,7 +84,7 @@ const DocumentOptionCard = () => { xs={8} > - Documents + {Title} Add documents and folders diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx index 3d95b543a..4c4690c44 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Events/EventsOptionCard.tsx @@ -12,6 +12,7 @@ import { optionCardStyle } from '../constants'; import { WidgetTabValues } from '../type'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; +const Title = 'Events'; const EventsOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const EventsOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Events, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const EventsOptionCard = () => { xs={8} > - Events + {Title} diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx index 8ae4deda1..14a976b6e 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Events/Form.tsx @@ -1,18 +1,23 @@ import React, { useContext } from 'react'; import { Grid, Divider } from '@mui/material'; -import { PrimaryButton, MetHeader3, WidgetButton } from 'components/common'; +import { PrimaryButton, WidgetButton } from 'components/common'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { EventsContext } from './EventsContext'; import EventsInfoBlock from './EventsInfoBlock'; +import { WidgetTitle } from '../WidgetTitle'; const Form = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); - const { setInPersonFormTabOpen, setVirtualSessionFormTabOpen } = useContext(EventsContext); + const { setInPersonFormTabOpen, setVirtualSessionFormTabOpen, widget } = useContext(EventsContext); + + if (!widget) { + return null; + } return ( - Events + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx index f4e56711f..e70054147 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Map/Form.tsx @@ -2,7 +2,6 @@ import React, { useContext, useState, useEffect } from 'react'; import Divider from '@mui/material/Divider'; import { Grid, Typography, Stack, IconButton } from '@mui/material'; import { - MetHeader3, MetLabel, PrimaryButton, SecondaryButton, @@ -26,6 +25,7 @@ import LinkIcon from '@mui/icons-material/Link'; import { When } from 'react-if'; import HighlightOffIcon from '@mui/icons-material/HighlightOff'; import * as turf from '@turf/turf'; +import { WidgetTitle } from '../WidgetTitle'; const schema = yup .object({ @@ -163,10 +163,14 @@ const Form = () => { ); } + if (!widget) { + return null; + } + return ( - Map + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx index 3d906580c..6e34da68b 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Map/MapOptionCard.tsx @@ -12,6 +12,7 @@ import { WidgetTabValues } from '../type'; import LocationOnIcon from '@mui/icons-material/LocationOn'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; +const Title = 'Map'; const MapOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const MapOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Map, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const MapOptionCard = () => { xs={8} > - Map + {Title} Add a map with the project location diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx index 78dbe3208..fba3305db 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesForm.tsx @@ -1,12 +1,13 @@ import React, { useContext, useEffect, useState } from 'react'; import { Autocomplete, Checkbox, Divider, FormControl, FormControlLabel, Grid, TextField } from '@mui/material'; -import { MetHeader3, MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; +import { MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; import { EngagementPhases } from 'models/engagementPhases'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; import { postWidgetItem } from 'services/widgetService'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { WidgetType } from 'models/widget'; +import { WidgetTitle } from '../WidgetTitle'; interface ISelectOptions { id: EngagementPhases; @@ -80,72 +81,68 @@ const PhasesForm = () => { }; return ( - <> - + + + + + + - The EA Process - + Engagement Phase + ( + + )} + isOptionEqualToValue={(option: ISelectOptions, value: ISelectOptions) => option.id == value.id} + getOptionLabel={(option: ISelectOptions) => option.label} + onChange={(_e: React.SyntheticEvent, option: ISelectOptions | null) => { + setSelectedOption(option); + setIsStandalone(false); + }} + /> - - - Engagement Phase - ( - + + { + setSelectedOption(null); + setIsStandalone(checked); }} /> - )} - isOptionEqualToValue={(option: ISelectOptions, value: ISelectOptions) => - option.id == value.id } - getOptionLabel={(option: ISelectOptions) => option.label} - onChange={(_e: React.SyntheticEvent, option: ISelectOptions | null) => { - setSelectedOption(option); - setIsStandalone(false); - }} + label="This engagement is a stand-alone engagement" /> - - - - { - setSelectedOption(null); - setIsStandalone(checked); - }} - /> - } - label="This engagement is a stand-alone engagement" - /> - - + + + + + + saveWidgetItem()} + >{`Save & Close`} - - - saveWidgetItem()} - >{`Save & Close`} - - - handleWidgetDrawerOpen(false)}>{`Cancel`} - + + handleWidgetDrawerOpen(false)}>{`Cancel`} - + ); }; diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx index d0d6c3487..256ae2245 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Phases/PhasesOptionCard.tsx @@ -12,6 +12,7 @@ import { Else, If, Then } from 'react-if'; import ChatBubbleOutlineOutlinedIcon from '@mui/icons-material/ChatBubbleOutlineOutlined'; import { optionCardStyle } from '../constants'; +const Title = 'Environmental Assessment Process'; const PhasesOptionCard = () => { const { savedEngagement } = useContext(ActionContext); const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -31,6 +32,7 @@ const PhasesOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Phases, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -82,7 +84,7 @@ const PhasesOptionCard = () => { xs={8} > - Environmental Assessment Process + {Title} 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 022b095b8..6afe23239 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeForm.tsx @@ -1,27 +1,26 @@ import React, { useContext } from 'react'; -import { Grid, Divider, FormControlLabel, Checkbox } from '@mui/material'; -import { PrimaryButton, MetHeader3, WidgetButton, MetParagraph, MetLabel } from 'components/common'; +import { Grid, Divider } from '@mui/material'; +import { PrimaryButton, WidgetButton, MetParagraph } from 'components/common'; import { WidgetDrawerContext } from '../WidgetDrawerContext'; -import BorderColorIcon from '@mui/icons-material/BorderColor'; import { SubscribeContext } from './SubscribeContext'; import { Subscribe_TYPE } from 'models/subscription'; +import { WidgetTitle } from '../WidgetTitle'; const Form = () => { const { handleWidgetDrawerOpen } = useContext(WidgetDrawerContext); - const { handleSubscribeDrawerOpen } = useContext(SubscribeContext); + const { handleSubscribeDrawerOpen, widget } = useContext(SubscribeContext); + + if (!widget) { + return null; + } return ( - - - Sign-up for updates - - - + - } label={Hide title} /> + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx index 8b5572b6f..5e63f540e 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Subscribe/SubscribeOptionCard.tsx @@ -12,6 +12,7 @@ import { optionCardStyle } from '../constants'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; import { WidgetTabValues } from '../type'; +const Title = 'Sign Up for Updates'; const SubscribeOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const SubscribeOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Subscribe, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -85,7 +87,7 @@ const SubscribeOptionCard = () => { xs={8} > - Sign Up for Updates + {Title} Offer members of the public to sign up for updates diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx index ebc9b8bef..c06de1f57 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Video/Form.tsx @@ -1,14 +1,7 @@ import React, { useContext, useEffect } from 'react'; import Divider from '@mui/material/Divider'; import { Grid } from '@mui/material'; -import { - MetDescription, - MetHeader3, - MetLabel, - MidScreenLoader, - PrimaryButton, - SecondaryButton, -} from 'components/common'; +import { MetDescription, MetLabel, MidScreenLoader, PrimaryButton, SecondaryButton } from 'components/common'; import { useForm, FormProvider, SubmitHandler } from 'react-hook-form'; import { yupResolver } from '@hookform/resolvers/yup'; import * as yup from 'yup'; @@ -19,6 +12,7 @@ import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { VideoContext } from './VideoContext'; import { patchVideo, postVideo } from 'services/widgetService/VideoService'; import { updatedDiff } from 'deep-object-diff'; +import { WidgetTitle } from '../WidgetTitle'; const schema = yup .object({ @@ -120,7 +114,7 @@ const Form = () => { } }; - if (isLoadingVideoWidget) { + if (isLoadingVideoWidget || !widget) { return ( @@ -133,7 +127,7 @@ const Form = () => { return ( - Video + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx index 9d50addc5..2abe5d671 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/Video/VideoOptionCard.tsx @@ -12,6 +12,7 @@ import { WidgetTabValues } from '../type'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; import MovieIcon from '@mui/icons-material/Movie'; +const Title = 'Video'; const VideoOptionCard = () => { const { widgets, loadWidgets, handleWidgetDrawerOpen, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -32,6 +33,7 @@ const VideoOptionCard = () => { await createWidget({ widget_type_id: WidgetType.Video, engagement_id: savedEngagement.id, + title: Title, }).unwrap(); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const VideoOptionCard = () => { xs={8} > - Video + {Title} Add a link to a hosted video and link preview diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx index 677f6f932..cc46412f9 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningForm.tsx @@ -1,6 +1,6 @@ import React, { useContext, useState, useEffect } from 'react'; import { Autocomplete, Grid, TextField, Divider } from '@mui/material'; -import { MetLabel, PrimaryButton, SecondaryButton, MetHeader3 } from 'components/common'; +import { MetLabel, PrimaryButton, SecondaryButton } from 'components/common'; import { Contact } from 'models/contact'; import { useAppDispatch } from 'hooks'; import { openNotification } from 'services/notificationService/notificationSlice'; @@ -9,6 +9,7 @@ import { WidgetDrawerContext } from '../WidgetDrawerContext'; import { WidgetType } from 'models/widget'; import ContactBlock from './ContactBlock'; import { WhoIsListeningContext } from './WhoIsListeningContext'; +import { WidgetTitle } from '../WidgetTitle'; const WhoIsListeningForm = () => { const { handleWidgetDrawerOpen, widgets, loadWidgets } = useContext(WidgetDrawerContext); @@ -86,7 +87,7 @@ const WhoIsListeningForm = () => { <> - Who is Listening + diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx index 68da307b1..712a0d431 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WhoIsListening/WhoIsListeningOptionCard.tsx @@ -12,6 +12,7 @@ import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined'; import { useCreateWidgetMutation } from 'apiManager/apiSlices/widgets'; import { optionCardStyle } from '../constants'; +const Title = 'Who is Listening'; const WhoIsListeningOptionCard = () => { const { savedEngagement } = useContext(ActionContext); const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext); @@ -31,6 +32,7 @@ const WhoIsListeningOptionCard = () => { await createWidget({ widget_type_id: WidgetType.WhoIsListening, engagement_id: savedEngagement.id, + title: Title, }); await loadWidgets(); dispatch( @@ -83,7 +85,7 @@ const WhoIsListeningOptionCard = () => { xs={8} > - Who is Listening + {Title} Add contacts to this engagement diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx index 8dcef7729..43f6af81f 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetCardSwitch.tsx @@ -19,7 +19,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -32,7 +32,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -45,7 +45,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -58,7 +58,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -71,7 +71,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -84,7 +84,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} @@ -97,7 +97,7 @@ export const WidgetCardSwitch = ({ widget, removeWidget }: WidgetCardSwitchProps { removeWidget(widget.id); }} diff --git a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx index 616f0861a..249b5fe28 100644 --- a/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx +++ b/met-web/src/components/engagement/form/EngagementWidgets/WidgetDrawerContext.tsx @@ -9,6 +9,7 @@ import { useDeleteWidgetMutation, useSortWidgetsMutation } from 'apiManager/apiS export interface WidgetDrawerContextProps { widgets: Widget[]; + setWidgets: React.Dispatch>; widgetDrawerOpen: boolean; handleWidgetDrawerOpen: (_open: boolean) => void; widgetDrawerTabValue: string; @@ -25,6 +26,9 @@ export type EngagementParams = { export const WidgetDrawerContext = createContext({ widgets: [], + setWidgets: () => { + return; + }, isWidgetsLoading: false, widgetDrawerOpen: false, handleWidgetDrawerOpen: (_open: boolean) => { @@ -103,6 +107,7 @@ export const WidgetDrawerProvider = ({ children }: { children: JSX.Element | JSX { + const [editing, setEditing] = React.useState(false); + const [title, setTitle] = React.useState(widget.title); + const [updateWidget] = useUpdateWidgetMutation(); + const dispatch = useAppDispatch(); + const { setWidgets } = useContext(WidgetDrawerContext); + const [isSaving, setIsSaving] = React.useState(false); + + const saveTitle = async () => { + if (title === widget.title) { + setEditing(false); + return; + } + try { + setIsSaving(true); + const response = await updateWidget({ + id: widget.id, + engagementId: widget.engagement_id, + data: { + title, + }, + }).unwrap(); + setWidgets((prevWidgets) => { + const updatedWidget = prevWidgets.find((prevWidget) => prevWidget.id === widget.id); + if (updatedWidget) { + updatedWidget.title = response?.title || ''; + } + return [...prevWidgets]; + }); + dispatch(openNotification({ severity: 'success', text: 'Widget title successfully updated' })); + setIsSaving(false); + setEditing(false); + } catch (error) { + setIsSaving(false); + dispatch(openNotification({ severity: 'error', text: 'Error occurred while updating widget title' })); + } + }; + + const handleTitleChange = (text: string) => { + if (!text) { + return; + } + + setTitle(text); + }; + + return ( + + + + handleTitleChange(e.target.value)} + inputProps={{ maxLength: 100 }} + fullWidth + /> + + + + + + { + saveTitle(); + }} + > + + + + + + + + + {widget.title} + { + setEditing(true); + }} + > + + + + + + ); +}; diff --git a/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx b/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx index 3b281a785..d23cdbf2c 100644 --- a/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/DocumentWidget.tsx @@ -50,7 +50,7 @@ const DocumentWidget = ({ widget }: DocumentWidgetProps) => { <> - Documents + {widget.title} {documents.map((document: DocumentItem) => { diff --git a/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx b/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx index ac9875124..f126705f2 100644 --- a/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/Events/EventsWidget.tsx @@ -73,7 +73,7 @@ const EventsWidget = ({ widget }: EventsWidgetProps) => { xs={12} paddingBottom={0} > - Events + {widget.title} {events.map((event: Event) => { diff --git a/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx b/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx index cf9545994..8d21e90ff 100644 --- a/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/Map/MapWidget.tsx @@ -79,7 +79,7 @@ const MapWidget = ({ widget }: MapWidgetProps) => { xs={12} paddingBottom={0} > - Map + {widget.title} diff --git a/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx b/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx index e6ae67fdb..8217f4613 100644 --- a/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx +++ b/met-web/src/components/engagement/view/widgets/PhasesWidget/PhasesWidgetMobile/PhasesWidgetMobile.tsx @@ -65,7 +65,7 @@ export const PhasesWidgetMobile = () => { - The EA Process + {phasesWidget.title} diff --git a/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx b/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx index 6a07a999a..1a22c53bf 100644 --- a/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx +++ b/met-web/src/components/engagement/view/widgets/PhasesWidget/index.tsx @@ -45,7 +45,7 @@ export const PhasesWidget = () => { - The Environmental Assessment Process + {phasesWidget.title} diff --git a/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx b/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx index c11cb0eb7..e22da3084 100644 --- a/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/Subscribe/SubscribeWidget.tsx @@ -9,8 +9,9 @@ import { createEmailVerification } from 'services/emailVerificationService'; import { createSubscription } from 'services/subscriptionService'; import { EmailVerificationType } from 'models/emailVerification'; import { SubscriptionType } from 'constants/subscriptionType'; +import { Widget } from 'models/widget'; -function SubscribeWidget() { +const SubscribeWidget = ({ widget }: { widget: Widget }) => { const dispatch = useAppDispatch(); const { savedEngagement, engagementMetadata } = useContext(ActionContext); const defaultType = engagementMetadata.project_id ? SubscriptionType.PROJECT : SubscriptionType.ENGAGEMENT; @@ -171,7 +172,7 @@ function SubscribeWidget() { /> - Sign Up for Updates + {widget.title} @@ -188,6 +189,6 @@ function SubscribeWidget() { ); -} +}; export default SubscribeWidget; diff --git a/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx b/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx index c2e414e37..be4b87d5a 100644 --- a/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx +++ b/met-web/src/components/engagement/view/widgets/Video/VideoWidgetView.tsx @@ -78,7 +78,7 @@ const VideoWidgetView = ({ widget }: VideoWidgetProps) => { xs={12} paddingBottom={0} > - Video + {widget.title} diff --git a/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx b/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx index 9196728e3..fc543c33a 100644 --- a/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx +++ b/met-web/src/components/engagement/view/widgets/WhoIsListeningWidget.tsx @@ -73,7 +73,7 @@ const WhoIsListeningWidget = ({ widget }: WhoIsListeningWidgetProps) => { return ( - Who is Listening + {widget.title} {contacts.map((contact) => { diff --git a/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx b/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx index ee2de56ea..0924ffa79 100644 --- a/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx +++ b/met-web/src/components/engagement/view/widgets/WidgetSwitch.tsx @@ -28,7 +28,7 @@ export const WidgetSwitch = ({ widget }: WidgetSwitchProps) => { - + diff --git a/met-web/src/models/widget.tsx b/met-web/src/models/widget.tsx index 9bbdcde90..403348ec5 100644 --- a/met-web/src/models/widget.tsx +++ b/met-web/src/models/widget.tsx @@ -10,6 +10,7 @@ export interface Widget { widget_type_id: number; engagement_id: number; items: WidgetItem[]; + title: string; } export enum WidgetType { diff --git a/met-web/tests/unit/components/engagement/engagement.test.tsx b/met-web/tests/unit/components/engagement/engagement.test.tsx index 686afaf62..f8aba194f 100644 --- a/met-web/tests/unit/components/engagement/engagement.test.tsx +++ b/met-web/tests/unit/components/engagement/engagement.test.tsx @@ -41,6 +41,7 @@ const widgetItem: WidgetItem = { const whoIsListeningWidget: Widget = { id: 1, + title: 'Who is Listening', widget_type_id: WidgetType.WhoIsListening, engagement_id: 1, items: [widgetItem], @@ -48,6 +49,7 @@ const whoIsListeningWidget: Widget = { const engagementPhasesWidget: Widget = { id: 2, + title: 'Engagement Phases', widget_type_id: WidgetType.Phases, engagement_id: 1, items: [], diff --git a/met-web/tests/unit/components/factory.ts b/met-web/tests/unit/components/factory.ts index 46310bb77..f7f9397c8 100644 --- a/met-web/tests/unit/components/factory.ts +++ b/met-web/tests/unit/components/factory.ts @@ -88,7 +88,7 @@ const mockEventItem: EventItem = { const mockEvent: Event = { id: 1, - title: 'Jace', + title: 'Events', type: 'OPENHOUSE', sort_index: 1, widget_id: 1, @@ -106,6 +106,7 @@ const eventWidgetItem: WidgetItem = { const eventWidget: Widget = { id: 1, + title: 'Events', widget_type_id: WidgetType.Events, engagement_id: 1, items: [eventWidgetItem], @@ -120,6 +121,7 @@ const mapWidgetItem: WidgetItem = { const mapWidget: Widget = { id: 1, + title: 'Map', widget_type_id: WidgetType.Map, engagement_id: 1, items: [mapWidgetItem], diff --git a/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx b/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx index 251e7d70c..d0a19b3fa 100644 --- a/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/DocumentWidget.test.tsx @@ -50,6 +50,7 @@ const mockFolder: DocumentItem = { const documentWidget: Widget = { id: 1, + title: 'Documents', widget_type_id: WidgetType.Document, engagement_id: 1, items: [], @@ -74,6 +75,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(documentWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(documentWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -135,6 +137,7 @@ describe('Document widget in engagement page tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Document, engagement_id: engagement.id, + title: documentWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Create Folder')).toBeVisible(); diff --git a/met-web/tests/unit/components/widgets/EventsWidget.test.tsx b/met-web/tests/unit/components/widgets/EventsWidget.test.tsx index 2ac900cba..c51894ca4 100644 --- a/met-web/tests/unit/components/widgets/EventsWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/EventsWidget.test.tsx @@ -62,6 +62,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(eventWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(eventWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -118,6 +119,7 @@ describe('Event Widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Events, engagement_id: draftEngagement.id, + title: mockEvent.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Add In-Person Event')).toBeVisible(); diff --git a/met-web/tests/unit/components/widgets/MapWidget.test.tsx b/met-web/tests/unit/components/widgets/MapWidget.test.tsx index af79d1638..2d2dc1afe 100644 --- a/met-web/tests/unit/components/widgets/MapWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/MapWidget.test.tsx @@ -62,6 +62,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(mapWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(mapWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -118,6 +119,7 @@ describe('Map Widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Map, engagement_id: draftEngagement.id, + title: mapWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Upload Shapefile')).toBeVisible(); diff --git a/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx b/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx index d853f11c6..90e5e0185 100644 --- a/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/PhasesWidget.test.tsx @@ -30,6 +30,7 @@ const phaseWidgetItem: WidgetItem = { const phasesWidget: Widget = { id: 2, + title: 'Environmental Assessment Process', widget_type_id: WidgetType.Phases, engagement_id: 1, items: [], @@ -58,6 +59,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(phasesWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(phasesWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -112,6 +114,7 @@ describe('Phases widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.Phases, engagement_id: draftEngagement.id, + title: phasesWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); diff --git a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx index 7795d18c7..15b6b7137 100644 --- a/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx +++ b/met-web/tests/unit/components/widgets/WhoIsListeningWidget.test.tsx @@ -44,6 +44,7 @@ const contactWidgetItem: WidgetItem = { const whoIsListeningWidget: Widget = { id: 1, + title: 'Who is Listening', widget_type_id: WidgetType.WhoIsListening, engagement_id: 1, items: [contactWidgetItem], @@ -107,6 +108,7 @@ const mockCreateWidget = jest.fn(() => Promise.resolve(whoIsListeningWidget)); jest.mock('apiManager/apiSlices/widgets', () => ({ ...jest.requireActual('apiManager/apiSlices/widgets'), useCreateWidgetMutation: () => [mockCreateWidget], + useUpdateWidgetMutation: () => [jest.fn(() => Promise.resolve(whoIsListeningWidget))], useDeleteWidgetMutation: () => [jest.fn(() => Promise.resolve())], useSortWidgetsMutation: () => [jest.fn(() => Promise.resolve())], })); @@ -164,6 +166,7 @@ describe('Who is Listening widget tests', () => { expect(mockCreateWidget).toHaveBeenNthCalledWith(1, { widget_type_id: WidgetType.WhoIsListening, engagement_id: draftEngagement.id, + title: whoIsListeningWidget.title, }); expect(getWidgetsMock).toHaveBeenCalledTimes(2); expect(screen.getByText('Add This Contact')).toBeVisible();