Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DESENG-675 Make widget component "reusable" #2579

Merged
merged 3 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.MD
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## August 21, 2024

- **Feature** Reusable widget component [🎟️ DESENG-675](https://citz-gdx.atlassian.net/browse/DESENG-675)

## August 15, 2024

- **Feature** Add engagement configuration summary [🎟️ DESENG-667](https://citz-gdx.atlassian.net/browse/DESENG-667)
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,4 @@ Examples of when to Request Changes
- `StatusIcon`: A simple component that displays a status icon based on the status string passed to it.
- `FormStep`: A wrapper around a form component that accepts a completion criteria and displays the user's progress. Accepts a `step` prop that is a number (from 1 to 9) that represents the current step in the form. This will be rendered as an icon with a checkmark if the step is complete, and a number if it's the current step or if it's incomplete.
- `SystemMessage`: An informational message that can be displayed to the user. Accepts a `type` prop that can be "error", "warning", "info", or "success", which affects the display of the message.
- `WidgetPicker`: A modular widget picker component that can be placed anywhere in the engagement editing area. In order to align widgets in the backend with the frontend, a "location" prop is required. Add new locations to the `WidgetLocation` enum.
24 changes: 24 additions & 0 deletions met-api/migrations/versions/c2a384ddfe6a_add_widget_location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Add locations to widgets

Revision ID: c2a384ddfe6a
Revises: 901a6724bca2
Create Date: 2024-08-21 16:04:25.726651

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'c2a384ddfe6a'
down_revision = '901a6724bca2'
branch_labels = None
depends_on = None


def upgrade():
op.add_column('widget', sa.Column('location', sa.Integer(), nullable=True))


def downgrade():
op.drop_column('widget', 'location')
2 changes: 2 additions & 0 deletions met-api/src/met_api/models/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class Widget(BaseModel): # pylint: disable=too-few-public-methods
title = db.Column(db.String(100), comment='Custom title for the widget.')
items = db.relationship('WidgetItem', backref='widget', cascade='all, delete', order_by='WidgetItem.sort_index')
sort_index = db.Column(db.Integer, nullable=False, default=1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it still make sense to have a sort_index on a widget if its position is determined by its location?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid question - I kept it so we could:

  • use the old widget picker
  • allow for sorting in the future, which the PO says is a possibility

That said, once we know the fate of the old picker and widgets going forward, a clean-up ticket would be warranted

location = db.Column(db.Integer, nullable=False)

@classmethod
def get_widget_by_id(cls, widget_id):
Expand Down Expand Up @@ -67,6 +68,7 @@ def __create_new_widget_entity(widget):
created_by=widget.get('created_by', None),
updated_by=widget.get('updated_by', None),
title=widget.get('title', None),
location=widget.get('location', None),
)

@classmethod
Expand Down
1 change: 1 addition & 0 deletions met-api/src/met_api/schemas/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ class Meta: # pylint: disable=too-few-public-methods
updated_date = fields.Str(data_key='updated_date')
sort_index = fields.Int(data_key='sort_index')
items = fields.List(fields.Nested(WidgetItemSchema))
location = fields.Int(data_key='location')
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useContext, useEffect } from 'react';
import { WidgetDrawerContext } from 'components/engagement/form/EngagementWidgets/WidgetDrawerContext';
import { Grid, Skeleton } from '@mui/material';
import { If, Else, Then } from 'react-if';
import { useAppDispatch } from 'hooks';
import { colors } from 'styles/Theme';
import { WidgetCardSwitch } from 'components/engagement/form/EngagementWidgets/WidgetCardSwitch';
import { openNotificationModal } from 'services/notificationModalService/notificationModalSlice';
import { WidgetLocation } from 'models/widget';

export const WidgetPickerButton = ({ location }: { location: WidgetLocation }) => {
const { widgets, deleteWidget, handleWidgetDrawerOpen, isWidgetsLoading, setWidgetLocation } =
useContext(WidgetDrawerContext);
const dispatch = useAppDispatch();

useEffect(() => {
setWidgetLocation(location);
return () => setWidgetLocation(0);
}, []);

const removeWidget = (widgetId: number) => {
dispatch(
openNotificationModal({
open: true,
data: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
data: {
data: {
style: 'warning',

style: 'warning',
header: 'Remove Widget',
subText: [
{ text: 'You will be removing this widget from the engagement.' },
{ text: 'Do you want to remove this widget?' },
],
handleConfirm: () => {
deleteWidget(widgetId);
},
},
type: 'confirm',
}),
);
};

return (
<Grid container spacing={2} direction="column">
<If condition={isWidgetsLoading}>
<Then>
<Grid item xs={12}>
<Skeleton width="100%" height="3em" />
</Grid>
</Then>
<Else>
<Grid item>
{/* Only ever render the first selected widget. This may change in the future. */}
{widgets.length > 0 ? (
<WidgetCardSwitch
singleSelection={true}
key={`${widgets[0].widget_type_id}`}
widget={widgets[0]}
removeWidget={removeWidget}
/>
) : (
<button
onClick={() => handleWidgetDrawerOpen(true)}
style={{
width: '100%',
borderRadius: '8px',
borderColor: colors.surface.blue[90],
borderWidth: '2px',
borderStyle: 'dashed',
backgroundColor: colors.surface.blue[10],
padding: '3rem',
fontSize: '16px',
color: colors.surface.blue[90],
cursor: 'pointer',
}}
>
Optional Content Widgets
</button>
)}
</Grid>
</Else>
</If>
</Grid>
);
};
19 changes: 19 additions & 0 deletions met-web/src/components/engagement/admin/create/widgets/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import WidgetDrawer from 'components/engagement/form/EngagementWidgets/WidgetDrawer';
import { WidgetDrawerProvider } from 'components/engagement/form/EngagementWidgets/WidgetDrawerContext';
import { WidgetPickerButton } from './WidgetPickerButton';
import { WidgetLocation } from 'models/widget';
import { ActionProvider } from 'components/engagement/form/ActionContext';

export const WidgetPicker = ({ location }: { location: WidgetLocation }) => {
return (
<ActionProvider>
<WidgetDrawerProvider>
<WidgetPickerButton location={location} />
<WidgetDrawer />
</WidgetDrawerProvider>
</ActionProvider>
);
};

export default WidgetPicker;
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import ControlledSelect from 'components/common/ControlledInputComponents/Contro
import { postDocument, patchDocument, PatchDocumentRequest } from 'services/widgetService/DocumentService';
import { DOCUMENT_TYPE, DocumentItem } from 'models/document';
import { updatedDiff } from 'deep-object-diff';
import { WidgetLocation } from 'models/widget';

const schema = yup
.object({
Expand Down Expand Up @@ -91,6 +92,7 @@ const AddFileDrawer = () => {
url: data.link,
widget_id: widget.id,
type: 'file',
location: widget.location in WidgetLocation ? widget.location : null,
});
dispatch(
openNotification({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { useAppDispatch } from 'hooks';
import { openNotification } from 'services/notificationService/notificationSlice';
import { postDocument } from 'services/widgetService/DocumentService';
import { WidgetDrawerContext } from '../WidgetDrawerContext';
import { WidgetType } from 'models/widget';
import { WidgetType, WidgetLocation } from 'models/widget';
import { DOCUMENT_TYPE } from 'models/document';

const CreateFolderForm = () => {
Expand Down Expand Up @@ -48,6 +48,7 @@ const CreateFolderForm = () => {
title: folderName,
widget_id: widget.id,
type: DOCUMENT_TYPE.FOLDER,
location: widget.location in WidgetLocation ? widget.location : null,
});
await loadDocuments();
setCreatingFolder(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { DOCUMENT_TYPE, DocumentItem } from 'models/document';
import { saveObject } from 'services/objectStorageService';
import FileUpload from 'components/common/FileUpload';
import { If, Then, Else } from 'react-if';
import { WidgetLocation } from 'models/widget';

const schema = yup
.object({
Expand Down Expand Up @@ -94,6 +95,7 @@ const UploadFileDrawer = () => {
widget_id: widget.id,
type: 'file',
is_uploaded: true,
location: widget.location in WidgetLocation ? widget.location : null,
});
dispatch(
openNotification({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { formEventDates } from './utils';
import dayjs from 'dayjs';
import tz from 'dayjs/plugin/timezone';
import { updatedDiff } from 'deep-object-diff';
import { WidgetLocation } from 'models/widget';

dayjs.extend(tz);

Expand Down Expand Up @@ -113,6 +114,7 @@ const InPersonEventFormDrawer = () => {
end_date: formatToUTC(dateTo),
},
],
location: widget.location in WidgetLocation ? widget.location : null,
});

setEvents((prevWidgetEvents: Event[]) => [...prevWidgetEvents, createdWidgetEvent]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { formatToUTC, formatDate } from 'components/common/dateHelper';
import { formEventDates } from './utils';
import dayjs from 'dayjs';
import tz from 'dayjs/plugin/timezone';
import { WidgetLocation } from 'models/widget';

dayjs.extend(tz);

Expand Down Expand Up @@ -108,6 +109,7 @@ const VirtualSessionFormDrawer = () => {
end_date: formatToUTC(dateTo),
},
],
location: widget.location in WidgetLocation ? widget.location : null,
});

setEvents((prevWidgetEvents: Event[]) => [...prevWidgetEvents, createdWidgetEvent]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { faCircleXmark } from '@fortawesome/pro-regular-svg-icons/faCircleXmark'
import { When } from 'react-if';
import * as turf from '@turf/turf';
import { WidgetTitle } from '../WidgetTitle';
import { WidgetLocation } from 'models/widget';

const schema = yup
.object({
Expand Down Expand Up @@ -98,6 +99,7 @@ const Form = () => {
longitude: longitude,
latitude: latitude,
file: shapefile,
location: widget.location in WidgetLocation ? widget.location : null,
});
dispatch(openNotification({ severity: 'success', text: 'A new map was successfully added' }));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { PollStatus } from 'constants/engagementStatus';
import Alert from '@mui/material/Alert';
import usePollWidgetState from './PollWidget.hook';
import PollAnswerForm from './PollAnswerForm';
import { WidgetLocation } from 'models/widget';

interface DetailsForm {
title: string;
Expand Down Expand Up @@ -79,6 +80,7 @@ const Form = () => {
description: description,
answers: answers,
status: status,
location: widget.location in WidgetLocation ? widget.location : null,
});
dispatch(openNotification({ severity: 'success', text: 'A new Poll was successfully added' }));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { TimelineContext } from './TimelineContext';
import { patchTimeline, postTimeline } from 'services/widgetService/TimelineService';
import { WidgetTitle } from '../WidgetTitle';
import { TimelineEvent } from 'models/timelineWidget';
import { WidgetLocation } from 'models/widget';

interface DetailsForm {
title: string;
Expand Down Expand Up @@ -81,6 +82,7 @@ const Form = () => {
title: title,
description: description,
events: events,
location: widget.location in WidgetLocation ? widget.location : null,
});
dispatch(openNotification({ severity: 'success', text: 'A new timeline was successfully added' }));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { VideoContext } from './VideoContext';
import { patchVideo, postVideo } from 'services/widgetService/VideoService';
import { updatedDiff } from 'deep-object-diff';
import { WidgetTitle } from '../WidgetTitle';
import { WidgetLocation } from 'models/widget';

const schema = yup
.object({
Expand Down Expand Up @@ -61,6 +62,7 @@ const Form = () => {
engagement_id: widget.engagement_id,
video_url: videoUrl,
description: description,
location: widget.location in WidgetLocation ? widget.location : null,
});
dispatch(openNotification({ severity: 'success', text: 'A new video was successfully added' }));
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ const WhoIsListeningForm = () => {
const [selectedContact, setSelectedContact] = useState<Contact | null>(null);
const [savingWidgetItems, setSavingWidgetItems] = useState(false);
const [createWidgetItems] = useCreateWidgetItemsMutation();

const widget = widgets.filter((widget) => widget.widget_type_id === WidgetType.WhoIsListening)[0] || null;

useEffect(() => {
const savedContacts = widget.items
.map((widget_item) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { WidgetTabValues } from '../type';
import { ActionContext } from '../../ActionContext';
import { openNotification } from 'services/notificationService/notificationSlice';
import { useAppDispatch } from 'hooks';
import { WidgetType } from 'models/widget';
import { WidgetType, WidgetLocation } from 'models/widget';
import { Else, If, Then } from 'react-if';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserGroupSimple } from '@fortawesome/pro-regular-svg-icons/faUserGroupSimple';
Expand All @@ -16,7 +16,7 @@ import { optionCardStyle } from '../constants';
const Title = 'Who is Listening';
const WhoIsListeningOptionCard = () => {
const { savedEngagement } = useContext(ActionContext);
const { widgets, loadWidgets, handleWidgetDrawerTabValueChange } = useContext(WidgetDrawerContext);
const { widgets, loadWidgets, handleWidgetDrawerTabValueChange, widgetLocation } = useContext(WidgetDrawerContext);
const dispatch = useAppDispatch();
const [createWidget] = useCreateWidgetMutation();
const [isCreatingWidget, setIsCreatingWidget] = useState(false);
Expand All @@ -34,6 +34,7 @@ const WhoIsListeningOptionCard = () => {
widget_type_id: WidgetType.WhoIsListening,
engagement_id: savedEngagement.id,
title: Title,
location: widgetLocation in WidgetLocation ? widgetLocation : 0,
});
await loadWidgets();
dispatch(
Expand Down
Loading
Loading