diff --git a/src/assets/images/Polls.svg b/src/assets/images/Polls.svg new file mode 100644 index 0000000000..8dbba865e0 --- /dev/null +++ b/src/assets/images/Polls.svg @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/src/assets/images/icons/Poll.svg b/src/assets/images/icons/Poll.svg new file mode 100644 index 0000000000..7fc91dbbc7 --- /dev/null +++ b/src/assets/images/icons/Poll.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/assets/images/icons/SideDrawer/WaPolls.tsx b/src/assets/images/icons/SideDrawer/WaPolls.tsx new file mode 100644 index 0000000000..062ca16a0f --- /dev/null +++ b/src/assets/images/icons/SideDrawer/WaPolls.tsx @@ -0,0 +1,9 @@ +const SvgComponent = ({ color }: { color: string }) => ( + + + +); +export default SvgComponent; diff --git a/src/common/HelpData.tsx b/src/common/HelpData.tsx index 4bd0ea9b3f..3b8cdb40e9 100644 --- a/src/common/HelpData.tsx +++ b/src/common/HelpData.tsx @@ -129,3 +129,8 @@ export const templateStatusInfo: HelpDataProps = { ), link: 'https://docs.gupshup.io/docs/message-template-approvals-statuses', }; + +export const pollsInfo: HelpDataProps = { + heading: 'An overview of all the polls created to date', + link: 'https://glific.github.io/docs/docs/WhatsApp%20Groups%20Automation/Sending%20Polls%20To%20WhatsApp%20Groups', +}; diff --git a/src/components/UI/ListIcon/ListIcon.tsx b/src/components/UI/ListIcon/ListIcon.tsx index ec88fbc180..654fa6fe1e 100644 --- a/src/components/UI/ListIcon/ListIcon.tsx +++ b/src/components/UI/ListIcon/ListIcon.tsx @@ -28,6 +28,7 @@ import WaChatIcon from 'assets/images/icons/SideDrawer/WaGroupChat'; import WaCollectionIcon from 'assets/images/icons/SideDrawer/WaGroupCollection'; import WaGroupIcon from 'assets/images/icons/SideDrawer/WhatsAppGroupIcon'; import Assistant from 'assets/images/icons/SideDrawer/Assistant'; +import WaPolls from 'assets/images/icons/SideDrawer/WaPolls'; import styles from './ListIcon.module.css'; import FiberNewIcon from '@mui/icons-material/FiberNew'; import { Badge } from '@mui/material'; @@ -74,6 +75,7 @@ export const ListIcon = ({ icon = '', selected = false, count }: ListIconProps) waGroup: WaGroupIcon, assistant: Assistant, discord: DiscordIcon, + waPolls: WaPolls, }; const iconImage = stringsToIcons[icon] && ( diff --git a/src/components/simulator/Simulator.module.css b/src/components/simulator/Simulator.module.css index 39908c1e32..6e6193ae51 100644 --- a/src/components/simulator/Simulator.module.css +++ b/src/components/simulator/Simulator.module.css @@ -29,6 +29,7 @@ background: white; width: 200px; margin-top: 4px; + overflow-x: hidden; } .StickerReceivedMessage { diff --git a/src/components/simulator/Simulator.tsx b/src/components/simulator/Simulator.tsx index 3dedc41e13..faa811ee55 100644 --- a/src/components/simulator/Simulator.tsx +++ b/src/components/simulator/Simulator.tsx @@ -52,6 +52,7 @@ import styles from './Simulator.module.css'; import { LocationRequestTemplate } from 'containers/Chat/ChatMessages/ChatMessage/LocationRequestTemplate/LocationRequestTemplate'; import { BackdropLoader } from 'containers/Flow/FlowTranslation'; import { SIMULATOR_RELEASE_SUBSCRIPTION } from 'graphql/subscriptions/PeriodicInfo'; +import { PollMessage } from 'containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage'; export interface SimulatorProps { setShowSimulator?: any; @@ -63,6 +64,7 @@ export interface SimulatorProps { interactiveMessage?: any; showHeader?: boolean; hasResetButton?: boolean; + pollContent?: any; } interface Sender { @@ -124,6 +126,7 @@ const Simulator = ({ interactiveMessage, showHeader = true, hasResetButton = false, + pollContent, }: SimulatorProps) => { const [inputMessage, setInputMessage] = useState(''); const [simulatedMessages, setSimulatedMessage] = useState(); @@ -268,7 +271,13 @@ const Simulator = ({ }); }; - const renderMessage = (messageObject: any, direction: string, index: number, isInteractive: boolean = false) => { + const renderMessage = ( + messageObject: any, + direction: string, + index: number, + isInteractive: boolean, + isPollContent: boolean = false + ) => { const { insertedAt, type, media, location, interactiveContent, bspMessageId, templateType } = messageObject; const messageType = isInteractive ? templateType : type; @@ -320,26 +329,31 @@ const Simulator = ({ } } + let messageBody: any = ( + <> + + + > + ); + if (isInteractiveContentPresent && direction !== 'send') { + messageBody = template; + } else if (isPollContent) { + messageBody = ; + } + return ( - {isInteractiveContentPresent && direction !== 'send' ? ( - template - ) : ( - <> - - - > - )} + {messageBody} { if (simulatorMessage.receiver.id === simulatorId) { - return renderMessage(simulatorMessage, 'received', index); + return renderMessage(simulatorMessage, 'received', index, false); } - return renderMessage(simulatorMessage, 'send', index); + return renderMessage(simulatorMessage, 'send', index, false); }) .reverse(); setSimulatedMessage(chatMessage); @@ -366,7 +380,7 @@ const Simulator = ({ const getPreviewMessage = () => { if (message && message.type) { - const previewMessage = renderMessage(message, 'received', 0); + const previewMessage = renderMessage(message, 'received', 0, false); if (['STICKER', 'AUDIO'].includes(message.type)) { setSimulatedMessage(previewMessage); } else if (message.body || message.media?.caption) { @@ -380,6 +394,7 @@ const Simulator = ({ if (interactiveMessage) { const { templateType, interactiveContent } = interactiveMessage; const previewMessage = renderMessage(interactiveMessage, 'received', 0, true); + setSimulatedMessage(previewMessage); if (templateType === LIST) { const { items } = JSON.parse(interactiveContent); @@ -388,6 +403,11 @@ const Simulator = ({ setIsDrawerOpen(false); } } + + if (pollContent) { + const previewMessage = renderMessage(pollContent, 'received', 0, false, true); + setSimulatedMessage(previewMessage); + } }; useEffect(() => { diff --git a/src/config/menu.ts b/src/config/menu.ts index 791c81c6fe..36f347a82a 100644 --- a/src/config/menu.ts +++ b/src/config/menu.ts @@ -51,6 +51,13 @@ const menus = (): Menu[] => [ type: 'sideDrawer', roles: allRoles, }, + { + title: 'Group Polls', + path: '/group/polls', + icon: 'waPolls', + type: 'sideDrawer', + roles: allRoles, + }, ], }, { diff --git a/src/containers/Chat/ChatConversations/MessageType/MessageType.tsx b/src/containers/Chat/ChatConversations/MessageType/MessageType.tsx index 517216ecd4..a2469f8baf 100644 --- a/src/containers/Chat/ChatConversations/MessageType/MessageType.tsx +++ b/src/containers/Chat/ChatConversations/MessageType/MessageType.tsx @@ -10,6 +10,7 @@ import AudioIconDark from 'assets/images/icons/Audio/Dark.svg?react'; import DocumentIconDark from 'assets/images/icons/Document/Dark.svg?react'; import StickerIconDark from 'assets/images/icons/Sticker/Dark.svg?react'; import LocationIconDark from 'assets/images/icons/Location/Dark.svg?react'; +import PollIcon from 'assets/images/icons/Poll.svg?react'; import styles from './MessageType.module.css'; @@ -26,6 +27,7 @@ const lightIcons: any = { DOCUMENT: , STICKER: , LOCATION: , + POLL: , }; const darkIcons: any = { @@ -51,6 +53,7 @@ export const MessageType = ({ type, body = '', color = 'light' }: MessageTypePro QUICK_REPLY: 'Quick Reply', LIST: 'List', LOCATION_REQUEST_MESSAGE: 'Location Request', + POLL: body, }; const option = ( diff --git a/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.module.css b/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.module.css index 1c01b663da..4f9f0b9bc4 100644 --- a/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.module.css +++ b/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.module.css @@ -150,6 +150,7 @@ .Inline { display: flex; } + .SendBy { display: flex; align-items: flex-end; @@ -298,3 +299,7 @@ padding-bottom: 8px; font-weight: bold; } + +.NoIcon { + margin: 0 1rem; +} \ No newline at end of file diff --git a/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.tsx b/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.tsx index d563222d66..7341efbfc6 100644 --- a/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.tsx +++ b/src/containers/Chat/ChatMessages/ChatMessage/ChatMessage.tsx @@ -25,6 +25,7 @@ import { QuickReplyTemplate } from '../QuickReplyTemplate/QuickReplyTemplate'; import styles from './ChatMessage.module.css'; import { setNotification } from 'common/notification'; import { LocationRequestTemplate } from './LocationRequestTemplate/LocationRequestTemplate'; +import { PollMessage } from './PollMessage/PollMessage'; export interface ChatMessageProps { id: number; @@ -56,6 +57,9 @@ export interface ChatMessageProps { groups?: boolean; status?: string; contact?: any; + poll?: any; + pollContent?: any; + showIcon?: boolean; } export const ChatMessage = ({ @@ -81,6 +85,9 @@ export const ChatMessage = ({ groups, status, contact, + poll, + pollContent, + showIcon = true, }: ChatMessageProps) => { const [showSaveMessageDialog, setShowSaveMessageDialog] = useState(false); const Ref = useRef(null); @@ -303,6 +310,33 @@ export const ChatMessage = ({ contactName = {contact?.name}; } + let messageBody: any; + if (isInteractiveContentPresent && !isSender) { + messageBody = template; + } else if (type === 'POLL') { + const pollContentJson = pollContent ? JSON.parse(pollContent) : {}; + + messageBody = ( + <> + {contactName} + + > + ); + } else { + messageBody = ( + <> + {contactName} + + {dateAndSendBy} + > + ); + } return ( {daySeparatorContent} @@ -335,73 +369,61 @@ export const ChatMessage = ({ ) : null} - - {iconLeft && icon} + + {showIcon && iconLeft && icon} {ErrorIcon} - {isInteractiveContentPresent && !isSender ? ( - template - ) : ( - <> - {contactName} - - {dateAndSendBy} - > - )} + {messageBody} - - {({ TransitionProps }) => ( - - - setShowSaveMessageDialog(true)}> - {t('Add to speed sends')} - - {type !== 'TEXT' && ( - - - downloadMedia()} - data-testid="downloadMedia" - > - {t('Download media')} - - - )} - - - )} - + ]} + anchorEl={anchorEl} + placement={placement} + transition + data-testid="popup" + > + {({ TransitionProps }) => ( + + + setShowSaveMessageDialog(true)}> + {t('Add to speed sends')} + + {type !== 'TEXT' && ( + + + downloadMedia()} + data-testid="downloadMedia" + > + {t('Download media')} + + + )} + + + )} + + )} - {iconLeft ? null : icon} + {iconLeft ? null : showIcon && icon} {sendBy} diff --git a/src/containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage.module.css b/src/containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage.module.css new file mode 100644 index 0000000000..f30ba21ebc --- /dev/null +++ b/src/containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage.module.css @@ -0,0 +1,111 @@ +.ChatMessage { + min-width: 250px; + color: #4a4a4a !important; + font-size: 14px; +} + +.SimpulatorMessage { + min-width: 180px; + font-size: 12px; +} + +.Sender { + color: #fff !important; +} + +.Text { + font-weight: 500; + line-height: 19.6px; +} + +.TextLarge { + font-size: 1rem; + margin-bottom: 1rem; +} + +.SelectText { + font-weight: 400; + line-height: 15px; + margin: 0 !important; +} + +.SelectTextDark { + composes: SelectText; + color: #6a6d6b; +} + +.SelectTextLight { + composes: SelectText; + color: #b3b6b4; +} + +.ChatMessage p { + font-size: 10px !important; + margin-top: 0.5rem !important; + margin-bottom: 0.8rem !important; +} + +.SimpulatorMessage p { + font-size: 8px; + margin: 0.3rem 0 !important; +} + +.Options { + display: flex; + flex-direction: column; + gap: 1rem; + padding-bottom: 1rem; +} + +.Option { + font-weight: 500; + line-height: 20px; + letter-spacing: -0.25px; + display: flex; + gap: 0.5rem; + flex-direction: column; +} + +.Option span { + display: flex; + align-items: center; + justify-content: space-between; +} + +.OptionName { + display: flex; + gap: 0.2rem; +} + +.OptionNameLarge { + composes: OptionName; + gap: 0.5rem; + font-size: 1rem; +} + +.Vote { + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: -0.25px; + text-align: left; +} + +.LinearProgressChat { + height: 8px; + border-radius: 5px; +} + +.LinearProgressSimulator { + height: 6px; + border-radius: 5px; +} + +.PlaceHolder { + width: 100%; + border-radius: 4px; + background-color: #dfece2c3; + color: #0c1f144b; + padding: 0.3rem; + font-size: 12px; +} \ No newline at end of file diff --git a/src/containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage.tsx b/src/containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage.tsx new file mode 100644 index 0000000000..f279eac61c --- /dev/null +++ b/src/containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage.tsx @@ -0,0 +1,62 @@ +import { LinearProgress } from '@mui/material'; +import styles from './PollMessage.module.css'; +import RadioButtonUncheckedIcon from '@mui/icons-material/RadioButtonUnchecked'; +import { Fragment } from 'react/jsx-runtime'; + +interface PollMessageProps { + isSender?: boolean; + isSimulator?: boolean; + view?: boolean; + pollContent: { + poll: any; + pollContentJson: any; + }; +} + +export const PollMessage = ({ isSender = false, isSimulator = false, view = false, pollContent }: PollMessageProps) => { + const { poll, pollContentJson } = pollContent; + const { text, options } = pollContentJson; + const maxVotes = Math.max(...pollContentJson.options.map((option: any) => option.votes)); + + return ( + + {text} + {!view && ( + + {poll?.allowMultipleAnswer ? 'Select one or more' : 'Select one'} + + )} + + {options.map((option: any, index: number) => { + const percentage = maxVotes > 0 ? (option.votes / maxVotes) * 100 : 0; + return ( + + {option.name ? ( + + + + + {option.name} + + {option.votes} + + + + + ) : ( + isSimulator && Option {index + 1} + )} + + ); + })} + + + ); +}; diff --git a/src/containers/Chat/ChatMessages/ChatMessages.tsx b/src/containers/Chat/ChatMessages/ChatMessages.tsx index ae43b3d5bc..90e7d98b4d 100644 --- a/src/containers/Chat/ChatMessages/ChatMessages.tsx +++ b/src/containers/Chat/ChatMessages/ChatMessages.tsx @@ -549,6 +549,7 @@ export const ChatMessages = ({ entityId, collectionId, phoneId }: ChatMessagesPr onClick={() => showEditDialog(message.id)} focus={index === 0} jumpToMessage={jumpToMessage} + showIcon={!groups} daySeparator={showDaySeparator( reverseConversation[index].insertedAt, reverseConversation[index + 1] ? reverseConversation[index + 1].insertedAt : null diff --git a/src/containers/List/List.tsx b/src/containers/List/List.tsx index 54742b58ea..f04e42c7f1 100644 --- a/src/containers/List/List.tsx +++ b/src/containers/List/List.tsx @@ -107,7 +107,7 @@ export interface ListProps { dialogMessage?: string | any; pageLink: string; columns: Function; - listIcon: React.ReactNode; + listIcon?: React.ReactNode; helpData?: HelpDataProps; columnStyles: Array; secondaryButton?: any; diff --git a/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx b/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx index a63f2fb3c7..dd85358814 100644 --- a/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx +++ b/src/containers/WaGroups/GroupChatInterface/GroupInterface.test.tsx @@ -32,6 +32,8 @@ cache.writeQuery({ messageNumber: 3, status: 'sent', type: 'TEXT', + poll: null, + pollContent: {}, }, ], waGroup: { @@ -69,6 +71,8 @@ cache.writeQuery({ messageNumber: 2, status: 'sent', type: 'TEXT', + poll: null, + pollContent: {}, }, ], waGroup: { @@ -106,6 +110,8 @@ cache.writeQuery({ messageNumber: 4, status: 'sent', type: 'TEXT', + poll: null, + pollContent: {}, }, { __typename: 'WaMessage', @@ -122,6 +128,8 @@ cache.writeQuery({ messageNumber: 3, status: 'sent', type: 'TEXT', + poll: null, + pollContent: {}, }, ], waGroup: { diff --git a/src/containers/WaGroups/WaPolls/WaPollOptions/WaPollOptions.module.css b/src/containers/WaGroups/WaPolls/WaPollOptions/WaPollOptions.module.css new file mode 100644 index 0000000000..5494979456 --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPollOptions/WaPollOptions.module.css @@ -0,0 +1,46 @@ +.Container { + background-color: #f8faf5; + border: 1px solid #cccccc; + border-radius: 4px; + margin: 1rem 0; + padding: 1rem; +} + +.Title { + color: #555555; + font-weight: 500; + line-height: 18px; + font-size: 16px; + margin-bottom: 1rem; +} + +.OptionField { + display: flex; + gap: 1rem; + align-items: center; +} + +.TextField { + width: 100%; + background-color: #ffffff; + border-radius: 12px; +} + +.Options { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.Options button { + width: 40%; + align-self: flex-end; +} + +.RemoveIcon { + cursor: pointer; +} + +.EmojiButton { + margin-right: -1rem; +} diff --git a/src/containers/WaGroups/WaPolls/WaPollOptions/WaPollOptions.tsx b/src/containers/WaGroups/WaPolls/WaPollOptions/WaPollOptions.tsx new file mode 100644 index 0000000000..b5fe9cd279 --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPollOptions/WaPollOptions.tsx @@ -0,0 +1,190 @@ +import EmojiEmotionsOutlinedIcon from '@mui/icons-material/EmojiEmotionsOutlined'; +import { ClickAwayListener, FormControl, FormHelperText, IconButton, TextField, Typography } from '@mui/material'; +import { useState } from 'react'; +import CrossIcon from 'assets/images/icons/Cross.svg?react'; +import EmojiPicker from 'components/UI/EmojiPicker/EmojiPicker'; +import { Button } from 'components/UI/Form/Button/Button'; +import styles from './WaPollOptions.module.css'; + +interface WaPollOptionsProps { + form: { field: any; errors: any; touched: any; values: any; setFieldValue: any }; + options: string[]; + isEditing: boolean; + setPreviewData: any; +} + +const emojiStyles = { + position: 'absolute', + bottom: '25px', + right: '-150px', + zIndex: 100, +}; + +export const WaPollOptions = ({ + form: { values, setFieldValue, errors, touched }, + isEditing, + setPreviewData, +}: WaPollOptionsProps) => { + const handleAddOption = () => { + const lastId = values.options[values.options.length - 1]?.id; + const newOptions = [...values.options, { name: '', id: lastId + 1 }]; + setFieldValue('options', newOptions); + setPreviewData((prev: any) => ({ + ...prev, + pollContentJson: { + ...prev.pollContentJson, + options: newOptions, + }, + })); + }; + + const handleInput = (value: any, id: any) => { + const newOptions = values.options.map((option: any) => (option.id === id ? { ...option, name: value } : option)); + setFieldValue('options', newOptions); + setPreviewData((prev: any) => ({ + ...prev, + pollContentJson: { + ...prev.pollContentJson, + options: newOptions, + }, + })); + }; + + const handleEmojiAdd = (emoji: any, id: number) => { + const newOptions = values.options.map((option: any) => + option.id === id ? { ...option, name: option.name + emoji.native } : option + ); + + setFieldValue('options', newOptions); + setPreviewData((prev: any) => ({ + ...prev, + pollContentJson: { + ...prev.pollContentJson, + options: newOptions, + }, + })); + }; + + const handleRemoveClick = (id: any) => { + const newOptions = values.options.filter((option: any) => option.id !== id); + setFieldValue('options', newOptions); + setPreviewData((prev: any) => ({ + ...prev, + pollContentJson: { + ...prev.pollContentJson, + options: newOptions, + }, + })); + }; + + return ( + + + Poll Options + + + {values.options.map((option: any, ind: number) => ( + + ))} + + {errors['options'] && typeof errors['options'] === 'string' && errors['options']} + + + {values.options.length < 12 && !isEditing && ( + + Add Option + + )} + + + ); +}; + +interface PollOptionProps { + option: any; + options: any; + errors: any; + touched: any; + handleInput: any; + handleRemoveClick: any; + handleEmojiAdd: any; + isEditing: boolean; + ind: number; +} + +const PollOption = ({ + option, + options, + errors, + touched, + handleInput, + handleRemoveClick, + handleEmojiAdd, + isEditing, + ind, +}: PollOptionProps) => { + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const hasError = errors && typeof errors !== 'string' && touched && errors[ind] && touched[ind]; + + return ( + + + handleInput(event.target.value, option?.id)} + disabled={isEditing} + slotProps={{ + input: { + endAdornment: !isEditing && ( + setShowEmojiPicker(false)}> + + setShowEmojiPicker(!showEmojiPicker)} + > + + + {showEmojiPicker && ( + handleEmojiAdd(emoji, option.id)} + displayStyle={emojiStyles} + /> + )} + + + ), + }, + }} + /> + + {options.length !== 2 && ( + handleRemoveClick(option.id)} + /> + )} + + {hasError ? {errors[ind]?.name || ''} : null} + + ); +}; diff --git a/src/containers/WaGroups/WaPolls/WaPolls.module.css b/src/containers/WaGroups/WaPolls/WaPolls.module.css new file mode 100644 index 0000000000..d3ebe260eb --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPolls.module.css @@ -0,0 +1,7 @@ +.AllowMultiple { + color: #555555; + font-weight: 400; + line-height: 18px; + font-size: 16px; + padding: 1rem 0; +} diff --git a/src/containers/WaGroups/WaPolls/WaPolls.test.tsx b/src/containers/WaGroups/WaPolls/WaPolls.test.tsx new file mode 100644 index 0000000000..61bce8e5c9 --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPolls.test.tsx @@ -0,0 +1,123 @@ +import { MockedProvider } from '@apollo/client/testing'; +import { MemoryRouter, Route, Routes } from 'react-router'; +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'; +import * as Notification from 'common/notification'; +import { WaPollMocks } from 'mocks/WaPolls'; +import WaPolls from './WaPolls'; +import WaPollsList from './WaPollsList/WaPollsList'; + +const notificationSpy = vi.spyOn(Notification, 'setNotification'); +const mockUseLocationValue: any = { + pathname: '/', + search: '', + hash: '', + state: null, +}; + +vi.mock('../../../components/UI/EmojiPicker/EmojiPicker', () => ({ + default: ({ onEmojiSelect }: { onEmojiSelect: (emoji: any) => void }) => ( + + + onEmojiSelect({ + native: '😃', + }) + } + > + Mock Emoji Picker + + + ), +})); + +vi.mock('react-router-dom', async () => ({ + ...((await vi.importActual('react-router-dom')) as {}), + useLocation: () => { + return mockUseLocationValue; + }, + Navigate: ({ to }: any) => Navigated to {to}, +})); + +beforeEach(() => { + cleanup(); +}); + +describe('Create', () => { + test('it should create a whatsapp poll', async () => { + render( + + + + } /> + } /> + } /> + + + + ); + + await waitFor(() => { + expect(screen.getByText('Add a new Poll')).toBeInTheDocument(); + }); + + const inputs = screen.getAllByRole('textbox'); + + fireEvent.change(inputs[0], { target: { value: 'Poll Title' } }); + fireEvent.change(inputs[1], { target: { value: 'Poll Content' } }); + + fireEvent.change(screen.getByPlaceholderText('Option 1'), { target: { value: 'Option 1' } }); + fireEvent.change(screen.getByPlaceholderText('Option 2'), { target: { value: 'Option 2' } }); + + fireEvent.click(screen.getAllByTestId('emoji-picker')[0]); + + expect(screen.getByTestId('emoji-container')).toBeInTheDocument(); + fireEvent.click(screen.getByTestId('mock-emoji-picker')); + + fireEvent.click(screen.getByTestId('add-btn')); + + fireEvent.change(screen.getByPlaceholderText('Option 3'), { target: { value: 'Option 3' } }); + + fireEvent.click(screen.getAllByTestId('cross-icon')[1]); + + fireEvent.click(screen.getByText('Allow multiple options')); + + fireEvent.click(screen.getByTestId('submitActionButton')); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + }); + }); +}); + +describe('Copy', () => { + test('it should copy a whatsapp poll', async () => { + mockUseLocationValue.state = 'copy'; + + const copyFlow = ( + + + + } /> + + + + ); + + render(copyFlow); + + await waitFor(() => { + expect(screen.getByText('Copy Poll')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getAllByRole('textbox')[0]).toHaveValue('Copy of Poll Title'); + }); + + fireEvent.click(screen.getByTestId('submitActionButton')); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/containers/WaGroups/WaPolls/WaPolls.tsx b/src/containers/WaGroups/WaPolls/WaPolls.tsx new file mode 100644 index 0000000000..539375fe23 --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPolls.tsx @@ -0,0 +1,207 @@ +import { Typography } from '@mui/material'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useLocation, useParams } from 'react-router-dom'; +import * as Yup from 'yup'; + +import PollsIcon from 'assets/images/Polls.svg?react'; +import { Checkbox } from 'components/UI/Form/Checkbox/Checkbox'; +import { Input } from 'components/UI/Form/Input/Input'; +import Simulator from 'components/simulator/Simulator'; +import { FormLayout } from 'containers/Form/FormLayout'; +import { COPY_POLL, CREATE_POLL, DELETE_POLL } from 'graphql/mutations/WaPolls'; +import { GET_POLL } from 'graphql/queries/WaPolls'; + +import { WaPollOptions } from './WaPollOptions/WaPollOptions'; +import styles from './WaPolls.module.css'; + +const queries = { + getItemQuery: GET_POLL, + createItemQuery: CREATE_POLL, + deleteItemQuery: DELETE_POLL, + updateItemQuery: COPY_POLL, +}; +const pollsIcon = ; + +export const WaPolls = () => { + const [label, setLabel] = useState(''); + const [content, setContent] = useState(''); + const [options, setOptions] = useState([ + { id: 0, name: '' }, + { id: 1, name: '' }, + ]); + const [allowMultiple, setAllowMultiple] = useState(false); + const [previewData, setPreviewData] = useState({ + poll: { allowMultipleAnswer: false }, + pollContentJson: { + text: '', + options: [ + { id: 0, name: '' }, + { id: 1, name: '' }, + ], + }, + }); + const { t } = useTranslation(); + const params = useParams(); + const location = useLocation(); + const mode = location.state; + let isEditing = false; + let isCopyState = false; + if (params.id) { + isEditing = true; + } + if (mode === 'copy') { + isCopyState = true; + isEditing = false; + } + + const states = { + label, + content, + options, + allowMultiple, + }; + + const setPayload = (payload: any) => { + let payloadCopy = { ...payload }; + const poll_content = JSON.stringify({ + options: payload.options, + text: payload.content, + }); + payloadCopy = { + ...payloadCopy, + poll_content, + allow_multiple_answer: payload.allowMultiple, + }; + delete payloadCopy.options; + delete payloadCopy.content; + delete payloadCopy.allowMultiple; + + return payloadCopy; + }; + const setStates = (states: any) => { + const { label, pollContent, allowMultipleAnswer } = states; + let text; + let options; + + if (pollContent) { + const pollContentJson = JSON.parse(pollContent); + text = pollContentJson?.text; + options = pollContentJson?.options; + } + + let labelValue = label; + if (isCopyState) { + labelValue = `Copy of ${label}`; + } + + setLabel(labelValue); + setAllowMultiple(allowMultipleAnswer); + setContent(text); + setOptions(options); + + setPreviewData({ + poll: { allowMultipleAnswer }, + pollContentJson: { text, options }, + }); + }; + + const FormSchema = Yup.object().shape({ + label: Yup.string().required(t('Title is required.')).max(50, t('Title is too long.')), + content: Yup.string().required('Content is required.').max(255, 'Content is too long.'), + options: Yup.array() + .of( + Yup.object().shape({ + name: Yup.string().required('Option is required.').max(100, 'Please enter not more than 100 characters'), + }) + ) + .test('unique-values', 'Values must be unique', (array: any) => { + const values = array.map((item: any) => item.name); + return new Set(values).size === values.length; + }), + }); + + const dialogMessage = "You won't be able to use this poll again."; + + const formFields = [ + { + component: Input, + name: 'label', + type: 'text', + label: t('Title'), + disabled: isEditing, + }, + + { + component: Input, + name: 'content', + type: 'text', + label: 'Content', + textArea: true, + rows: 6, + disabled: isEditing, + onChange: (value: any) => { + setPreviewData({ + ...previewData, + pollContentJson: { + ...previewData.pollContentJson, + text: value, + }, + }); + }, + }, + { + component: WaPollOptions, + name: 'options', + isEditing, + setPreviewData, + }, + { + component: Checkbox, + name: 'allowMultiple', + title: ( + + Allow multiple options + + ), + darkCheckbox: true, + disabled: isEditing, + handleChange: (value: boolean) => { + setPreviewData({ + ...previewData, + poll: { + allowMultipleAnswer: value, + }, + }); + }, + }, + ]; + + return ( + <> + + + + + > + ); +}; + +export default WaPolls; diff --git a/src/containers/WaGroups/WaPolls/WaPollsList/ViewPollDialog/ViewPollDialog.tsx b/src/containers/WaGroups/WaPolls/WaPollsList/ViewPollDialog/ViewPollDialog.tsx new file mode 100644 index 0000000000..1ea8b3e1f4 --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPollsList/ViewPollDialog/ViewPollDialog.tsx @@ -0,0 +1,37 @@ +import { useQuery } from '@apollo/client'; +import { DialogBox } from 'components/UI/DialogBox/DialogBox'; +import { Loading } from 'components/UI/Layout/Loading/Loading'; +import { PollMessage } from 'containers/Chat/ChatMessages/ChatMessage/PollMessage/PollMessage'; +import { GET_POLL } from 'graphql/queries/WaPolls'; + +interface ViewPollProps { + id: string | null; + onClose: any; +} + +export const ViewPoll = ({ id, onClose }: ViewPollProps) => { + const { data, loading } = useQuery(GET_POLL, { + variables: { + id: id, + }, + }); + + const poll = data?.waPoll?.waPoll; + const pollContentJson = poll?.pollContent ? JSON.parse(poll?.pollContent) : {}; + + if (loading) { + return ; + } + + return ( + + + + ); +}; diff --git a/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.module.css b/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.module.css new file mode 100644 index 0000000000..be8204cbb0 --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.module.css @@ -0,0 +1,26 @@ +.Label { + width: 300px; +} + +.Content { + width: 400px; +} + +.Actions { + width: 30%; + min-width: 200px; + text-align: end; +} + +.LabelText { + font-weight: 500; + font-size: 17px; + line-height: 20px; + color: #191c1a; + display: flex; + align-items: center; +} + +.DialogText { + text-align: center; +} diff --git a/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.test.tsx b/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.test.tsx new file mode 100644 index 0000000000..ab95c6e3b8 --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.test.tsx @@ -0,0 +1,103 @@ +import { MockedProvider } from '@apollo/client/testing'; +import { MemoryRouter, Route, Routes } from 'react-router'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import * as Notification from 'common/notification'; +import { WaPollListMocks } from 'mocks/WaPolls'; +import WaPolls from '../WaPolls'; +import WaPollsList from './WaPollsList'; + +const wrapper = ( + + + + } /> + } /> + + + +); + +const notificationSpy = vi.spyOn(Notification, 'setNotification'); +const mockedUsedNavigate = vi.fn(); +vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), + useNavigate: () => mockedUsedNavigate, +})); + +test('it should render the WaPollsList component', async () => { + render(wrapper); + + await waitFor(() => { + expect(screen.getByText('Group Polls')).toBeInTheDocument(); + }); + + await waitFor(() => { + expect(screen.getByText('poll 1')).toBeInTheDocument(); + }); +}); + +test('it should copy the uuid', async () => { + render(wrapper); + + await waitFor(() => { + expect(screen.getByText('Group Polls')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('copy-icon')[0]); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + }); +}); + +test('it should open the view dialog box', async () => { + render(wrapper); + + await waitFor(() => { + expect(screen.getByText('Group Polls')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('view-icon')[0]); + + await waitFor(() => { + expect(screen.getByTestId('dialogBox')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('CloseIcon')); +}); + +test('it navigates to create a copy', async () => { + render(wrapper); + + await waitFor(() => { + expect(screen.getByText('Group Polls')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('duplicate-icon')[0]); + + await waitFor(() => { + expect(screen.getByText('Copy Poll')).toBeInTheDocument(); + }); +}); + +test('it should delete the poll', async () => { + render(wrapper); + + await waitFor(() => { + expect(screen.getByText('Group Polls')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getAllByTestId('delete-icon')[0]); + fireEvent.click(screen.getByTestId('CloseIcon')); + fireEvent.click(screen.getAllByTestId('delete-icon')[0]); + + await waitFor(() => { + expect(screen.getByTestId('dialogBox')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('ok-button')); + + await waitFor(() => { + expect(notificationSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.tsx b/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.tsx new file mode 100644 index 0000000000..b140963a3f --- /dev/null +++ b/src/containers/WaGroups/WaPolls/WaPollsList/WaPollsList.tsx @@ -0,0 +1,163 @@ +import { useMutation } from '@apollo/client'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router'; + +import CopyAllOutlined from 'assets/images/icons/Flow/Copy.svg?react'; +import DeleteIcon from 'assets/images/icons/Delete/Red.svg?react'; +import DuplicateIcon from 'assets/images/icons/Duplicate.svg?react'; +import ViewIcon from 'assets/images/icons/ViewLight.svg?react'; +import { copyToClipboardMethod } from 'common/utils'; +import { pollsInfo } from 'common/HelpData'; +import { setErrorMessage, setNotification } from 'common/notification'; +import { DialogBox } from 'components/UI/DialogBox/DialogBox'; +import { List } from 'containers/List/List'; +import { DELETE_POLL } from 'graphql/mutations/WaPolls'; +import { GET_POLLS, GET_POLLS_COUNT } from 'graphql/queries/WaPolls'; + +import styles from './WaPollsList.module.css'; +import { ViewPoll } from './ViewPollDialog/ViewPollDialog'; + +const queries = { + countQuery: GET_POLLS_COUNT, + filterItemsQuery: GET_POLLS, + deleteItemQuery: DELETE_POLL, +}; + +const getLabel = (label: string) => {label}; + +const getContent = (content: string) => { + return {content.length < 100 ? content : `${content.slice(0, 100)}...`}; +}; +export const WaPollsList = () => { + const [deleteWaPollId, setDeleteWaPollId] = useState(null); + const [viewWaPollId, setViewWaPollId] = useState(null); + const [refreshList, setRefreshList] = useState(false); + + const { t } = useTranslation(); + const navigate = useNavigate(); + + const [deletePoll, { loading }] = useMutation(DELETE_POLL); + + const columnNames = [{ name: 'label', label: 'Title' }, { label: 'Content' }, { label: t('Actions') }]; + const title = t('Group Polls'); + const dialogMessage = t("You won't be able to use this collection again."); + const columnStyles = [styles.Label, styles.Content, styles.Actions]; + + const getColumns = ({ label, pollContent, id }: any) => { + const content = pollContent ? JSON.parse(pollContent) : {}; + return { + label: getLabel(label), + content: getContent(content.text), + }; + }; + + const columnAttributes = { + columns: getColumns, + columnStyles, + }; + + const handleCopy = (id: any) => { + navigate(`/group/polls/${id}/edit`, { state: 'copy' }); + }; + + const handleDelete = () => { + deletePoll({ + variables: { deleteWaPollId }, + onCompleted: () => { + setNotification('Poll deleted successfully', 'success'); + setDeleteWaPollId(null); + setRefreshList(!refreshList); + }, + onError: (error) => setErrorMessage(error), + }); + }; + + const copyUuid = (_id: string, item: any) => { + if (item.uuid) { + copyToClipboardMethod(item.uuid); + } else { + setNotification('Sorry! UUID not found', 'warning'); + } + }; + + const getRestrictedAction = () => { + const action: any = { edit: false, delete: false }; + return action; + }; + + const additionalAction = () => [ + { + label: t('Copy UUID'), + icon: , + parameter: 'id', + dialog: copyUuid, + }, + { + label: t('View'), + icon: , + parameter: 'id', + dialog: (id: any) => setViewWaPollId(id), + insideMore: false, + }, + { + label: t('Copy Poll'), + icon: , + parameter: 'id', + insideMore: false, + dialog: handleCopy, + }, + { + label: t('Delete'), + icon: , + parameter: 'id', + dialog: (id: any) => setDeleteWaPollId(id), + insideMore: false, + }, + ]; + + const deletedialog = ( + setDeleteWaPollId(null)} + alignButtons="center" + colorOk={'warning'} + buttonOk={'Delete'} + buttonOkLoading={loading} + disableOk={loading} + > + + This action is permanent and cannot be undone. Deleting this poll will remove all associated responses and data + from the platform. + + + ); + + return ( + <> + + {deleteWaPollId && deletedialog} + {viewWaPollId && setViewWaPollId(null)} />} + > + ); +}; + +export default WaPollsList; diff --git a/src/graphql/mutations/WaPolls.ts b/src/graphql/mutations/WaPolls.ts new file mode 100644 index 0000000000..2d7dd7c66b --- /dev/null +++ b/src/graphql/mutations/WaPolls.ts @@ -0,0 +1,39 @@ +import { gql } from '@apollo/client'; + +export const CREATE_POLL = gql` + mutation CreateWaPoll($input: WaPollInput!) { + createWaPoll(input: $input) { + errors { + message + } + waPoll { + id + } + } + } +`; + +export const COPY_POLL = gql` + mutation CopyWaPoll($input: WaPollInput, $id: ID!) { + copyWaPoll(input: $input, id: $id) { + waPoll { + id + label + } + } + } +`; + +export const DELETE_POLL = gql` + mutation DeleteWaPoll($deleteWaPollId: ID!) { + deleteWaPoll(id: $deleteWaPollId) { + errors { + message + } + waPoll { + id + label + } + } + } +`; diff --git a/src/graphql/queries/WaGroups.ts b/src/graphql/queries/WaGroups.ts index 11727ffcab..44b662a741 100644 --- a/src/graphql/queries/WaGroups.ts +++ b/src/graphql/queries/WaGroups.ts @@ -49,6 +49,12 @@ export const GROUP_SEARCH_QUERY = gql` type insertedAt } + pollContent + poll { + id + pollContent + allowMultipleAnswer + } } } } diff --git a/src/graphql/queries/WaPolls.ts b/src/graphql/queries/WaPolls.ts new file mode 100644 index 0000000000..fd7856701e --- /dev/null +++ b/src/graphql/queries/WaPolls.ts @@ -0,0 +1,35 @@ +import { gql } from '@apollo/client'; + +export const GET_POLLS = gql` + query WaPolls($filter: WaPollFilter, $opts: Opts) { + poll: waPolls(filter: $filter, opts: $opts) { + allowMultipleAnswer + id + label + pollContent + uuid + } + } +`; + +export const GET_POLL = gql` + query WaPoll($id: ID!) { + waPoll(id: $id) { + waPoll { + id + label + pollContent + allowMultipleAnswer + } + errors { + message + } + } + } +`; + +export const GET_POLLS_COUNT = gql` + query RootQueryType($filter: WaPollFilter) { + countPoll: countWaPolls(filter: $filter) + } +`; diff --git a/src/graphql/subscriptions/Groups.ts b/src/graphql/subscriptions/Groups.ts index 9ced03d8dd..af19cfc956 100644 --- a/src/graphql/subscriptions/Groups.ts +++ b/src/graphql/subscriptions/Groups.ts @@ -34,7 +34,12 @@ export const WA_MESSAGE_RECEIVED_SUBSCRIPTION = gql` type insertedAt } - + pollContent + poll { + id + pollContent + allowMultipleAnswer + } errors } } @@ -75,6 +80,12 @@ export const WA_MESSAGE_SENT_SUBSCRIPTION = gql` insertedAt } status + pollContent + poll { + id + pollContent + allowMultipleAnswer + } } } `; @@ -124,6 +135,12 @@ export const UPDATE_WA_MESSAGE_STATUS = gql` updateWaMessageStatus(organizationId: $organizationId) { id messageNumber + pollContent + poll { + id + pollContent + allowMultipleAnswer + } errors waGroup { id diff --git a/src/i18n/en/en.json b/src/i18n/en/en.json index 15e2641d2f..802d96e95f 100644 --- a/src/i18n/en/en.json +++ b/src/i18n/en/en.json @@ -532,5 +532,7 @@ "Last name is required.": "Last name is required.", "Failed": "Failed", "Members": "Members", + "Copy Poll": "Copy Poll", + "Group Polls": "Group Polls", "Name cannot be more than 250 characters.": "Name cannot be more than 250 characters." } diff --git a/src/mocks/Chat.tsx b/src/mocks/Chat.tsx index 6242609a7c..715085aac9 100644 --- a/src/mocks/Chat.tsx +++ b/src/mocks/Chat.tsx @@ -793,11 +793,7 @@ export const sendMessageInWaGroup = { }, }, result: { - data: { - sendMessageInWaGroup: { - errors: null, - }, - }, + errors: [new Error('Error!')], }, }; diff --git a/src/mocks/Groups.tsx b/src/mocks/Groups.tsx index c4de3c925e..2cd98e7398 100644 --- a/src/mocks/Groups.tsx +++ b/src/mocks/Groups.tsx @@ -69,7 +69,16 @@ export const waGroup = { media: null, messageNumber: 4, status: 'sent', - type: 'TEXT', + type: 'POLL', + poll: { + __typename: 'WaPoll', + allowMultipleAnswer: false, + id: '4', + pollContent: + '{"text": "this is a poll", "options": [{ "votes": 1, "name": "option 1", "id": 0 },{ "votes": 1, "name": "option 2", "id": 1 },{ "votes": 0, "name": "okay option 4", "id": 2 }]}', + }, + pollContent: + '{"text": "this is a poll", "options": [{ "votes": 1, "name": "option 1", "id": 0 },{ "votes": 1, "name": "option 2", "id": 1 },{ "votes": 0, "name": "okay option 4", "id": 2 }]}', }, { __typename: 'WaMessage', @@ -86,6 +95,8 @@ export const waGroup = { messageNumber: 3, status: 'sent', type: 'TEXT', + poll: null, + pollContent: '{}', }, ], }, @@ -126,7 +137,16 @@ const sampleMessage = { media: null, messageNumber: 1, status: 'received', - type: 'TEXT', + type: 'POLL', + poll: { + __typename: 'WaPoll', + allowMultipleAnswer: false, + id: '4', + pollContent: + '{"text": "this is a poll", "options": [{ "votes": 1, "name": "option 1", "id": 0 },{ "votes": 1, "name": "option 2", "id": 1 },{ "votes": 0, "name": "okay option 4", "id": 2 }]}', + }, + pollContent: + '{"text": "this is a poll", "options": [{ "votes": 1, "name": "option 1", "id": 0 },{ "votes": 1, "name": "option 2", "id": 1 },{ "votes": 0, "name": "okay option 4", "id": 2 }]}', }; const sampleSearchQueryResult = [ diff --git a/src/mocks/WaPolls.tsx b/src/mocks/WaPolls.tsx new file mode 100644 index 0000000000..b34ef6ec26 --- /dev/null +++ b/src/mocks/WaPolls.tsx @@ -0,0 +1,163 @@ +import { COPY_POLL, CREATE_POLL, DELETE_POLL } from 'graphql/mutations/WaPolls'; +import { filterRolesQuery } from './Role'; +import { GET_POLL, GET_POLLS, GET_POLLS_COUNT } from 'graphql/queries/WaPolls'; + +const createPoll = { + request: { + query: CREATE_POLL, + variables: { + input: { + label: 'Poll Title', + poll_content: '{"options":[{"id":0,"name":"Option 1😃"},{"name":"Option 3","id":2}],"text":"Poll Content"}', + allow_multiple_answer: true, + }, + }, + }, + result: { + data: { + createWaPoll: { + errors: null, + waPoll: { + id: '8', + }, + }, + }, + }, +}; + +const getPoll = { + request: { + query: GET_POLL, + variables: { + id: '1', + }, + }, + result: { + data: { + waPoll: { + __typename: 'WaPollResult', + errors: null, + waPoll: { + allowMultipleAnswer: true, + id: '8', + label: 'Poll Title', + pollContent: + '{"text":"Poll Content","options":[{"name":"Option 1","id":0},{"name":"Option 3","id":0.6781005888671008}]}', + }, + }, + }, + }, +}; + +const copyPoll = { + request: { + query: COPY_POLL, + variables: { + id: '1', + input: { + label: 'Copy of Poll Title', + poll_content: + '{"options":[{"name":"Option 1","id":0},{"name":"Option 3","id":0.6781005888671008}],"text":"Poll Content"}', + allow_multiple_answer: true, + }, + }, + }, + result: { + data: { + copyWaPoll: { + waPoll: { + id: '2', + label: 'Copy of Poll Title', + }, + }, + }, + }, +}; + +const listPollsQuery = { + request: { + query: GET_POLLS, + variables: { filter: {}, opts: { limit: 50, offset: 0, order: 'ASC', orderWith: 'label' } }, + }, + result: { + data: { + poll: [ + { + allowMultipleAnswer: false, + id: '1', + label: 'poll 1', + pollContent: '{"text":"text","options":[{"name":"1","id":0},{"name":"2","id":1}]}', + uuid: '6fa459ea-ee8a-3ca4-894e-db77e160355e', + }, + { + allowMultipleAnswer: true, + id: '8', + label: 'Poll Title', + pollContent: + '{"text":"Poll Content","options":[{"name":"Option 1","id":0},{"name":"Option 3","id":0.6781005888671008}]}', + uuid: '550e8400-e29b-41d4-a716-446655440000', + }, + { + allowMultipleAnswer: true, + id: '5', + label: 'polllll', + pollContent: + '{"text":"How frequently do you participate in our activities or events, and what influences your level of involvement?","options":[{"name":"Everyday","id":0},{"name":"Once a week","id":1},{"name":"Monthly","id":2},{"name":"Never","id":3}]}', + uuid: '123e4567-e89b-12d3-a456-426614174000', + }, + { + allowMultipleAnswer: false, + id: '3', + label: 'titek 1', + pollContent: '{"text":"sdcfd","options":[{"name":"dfv","id":0},{"name":"sdf","id":1}]}', + uuid: '9b2d9b1e-1c3e-4f5a-8b2d-9b1e1c3e4f5a', + }, + { + allowMultipleAnswer: false, + id: '4', + label: 'we', + pollContent: '{"text":"s","options":[{"name":"dds","id":0},{"name":"ss","id":1}]}', + uuid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + }, + ], + }, + }, +}; + +const countPollsQuery = { + request: { + query: GET_POLLS_COUNT, + variables: { filter: {} }, + }, + result: { + data: { + countPoll: 5, + }, + }, +}; + +const deletePollQuery = { + request: { + query: DELETE_POLL, + variables: { deleteWaPollId: '1' }, + }, + result: { + data: { + deleteWaPoll: { + errors: null, + waPoll: null, + }, + }, + }, +}; + +export const WaPollListMocks = [ + filterRolesQuery, + listPollsQuery, + listPollsQuery, + countPollsQuery, + countPollsQuery, + getPoll, + deletePollQuery, +]; +export const WaPollMocks = [filterRolesQuery, createPoll, getPoll, copyPoll]; diff --git a/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx b/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx index 295fedc816..3e75b027ca 100644 --- a/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx +++ b/src/routes/AuthenticatedRoute/AuthenticatedRoute.tsx @@ -61,6 +61,8 @@ const InteractiveMessage = lazy(() => import('containers/InteractiveMessage/Inte const RoleList = lazy(() => import('containers/Role/RoleList/RoleList')); const Role = lazy(() => import('containers/Role/Role')); const Assistant = lazy(() => import('containers/Assistants/Assistants')); +const WaPollsCreate = lazy(() => import('containers/WaGroups/WaPolls/WaPolls')); +const WaPollsList = lazy(() => import('containers/WaGroups/WaPolls/WaPollsList/WaPollsList')); const routeStaff = ( @@ -145,6 +147,10 @@ const routeAdmin = ( } /> } /> + } /> + } /> + } /> + } /> );
+ {poll?.allowMultipleAnswer ? 'Select one or more' : 'Select one'} +
+ This action is permanent and cannot be undone. Deleting this poll will remove all associated responses and data + from the platform. +