Merge remote-tracking branch 'upstream/v3.0.x-release' into migrate-breakout-invitation

This commit is contained in:
Tainan Felipe 2024-04-03 20:59:26 -03:00
commit 901d2a7fbb
29 changed files with 2691 additions and 104 deletions

View File

@ -71,6 +71,7 @@ object PluginDataChannelMessageDAO {
.filter(_.meetingId === meetingId)
.filter(_.pluginName === pluginName)
.filter(_.dataChannel === dataChannel)
.filter(_.deletedAt.isEmpty)
.map(u => (u.deletedAt))
.update(Some(new java.sql.Timestamp(System.currentTimeMillis())))
).onComplete {

View File

@ -15,7 +15,9 @@ type ActivitiesOverviewObj struct {
Completed int64
DataReceived int64
DataSizeAvg int64
DataSizeMax int64
DataCountAvg int64
DataCountMax int64
}
var ActivitiesOverviewEnabled = false
@ -36,7 +38,9 @@ func ActivitiesOverviewStarted(index string) {
Completed: 0,
DataReceived: 0,
DataSizeAvg: 0,
DataSizeMax: 0,
DataCountAvg: 0,
DataCountMax: 0,
}
}
@ -61,21 +65,6 @@ func ActivitiesOverviewDataReceived(index string) {
}
}
func ActivitiesOverviewDataSize(index string, dataSize int64, dataCount int64) {
if !ActivitiesOverviewEnabled {
return
}
activitiesOverviewMux.Lock()
defer activitiesOverviewMux.Unlock()
if updatedValues, exists := activitiesOverview[index]; exists {
updatedValues.DataSizeAvg = ((updatedValues.DataSizeAvg*updatedValues.DataReceived - 1) + dataSize) / updatedValues.DataReceived
updatedValues.DataCountAvg = ((updatedValues.DataCountAvg * (updatedValues.DataReceived - 1)) + dataCount) / updatedValues.DataReceived
activitiesOverview[index] = updatedValues
}
}
func ActivitiesOverviewCompleted(index string) {
if !ActivitiesOverviewEnabled {
return
@ -89,7 +78,27 @@ func ActivitiesOverviewCompleted(index string) {
activitiesOverview[index] = updatedValues
}
}
func ActivitiesOverviewDataSize(index string, dataSize int64, dataCount int64) {
if !ActivitiesOverviewEnabled {
return
}
activitiesOverviewMux.Lock()
defer activitiesOverviewMux.Unlock()
if updatedValues, exists := activitiesOverview[index]; exists {
updatedValues.DataSizeAvg = ((updatedValues.DataSizeAvg * (updatedValues.DataReceived - 1)) + dataSize) / updatedValues.DataReceived
if dataSize > updatedValues.DataSizeMax {
updatedValues.DataSizeMax = dataSize
}
updatedValues.DataCountAvg = ((updatedValues.DataCountAvg * (updatedValues.DataReceived - 1)) + dataCount) / updatedValues.DataReceived
if dataCount > updatedValues.DataCountMax {
updatedValues.DataCountMax = dataCount
}
activitiesOverview[index] = updatedValues
}
}
func GetActivitiesOverview() map[string]ActivitiesOverviewObj {
@ -114,8 +123,10 @@ func ActivitiesOverviewLogRoutine() {
if strings.HasPrefix(index, "_") ||
item.Started > hasuraConnections*3 ||
item.DataReceived > hasuraConnections*5 ||
item.DataSizeAvg > 3000 ||
item.DataCountAvg > 5 {
item.DataSizeAvg > 4000 ||
item.DataSizeMax > 50000 ||
item.DataCountAvg > 5 ||
(item.DataCountMax > 10 && item.DataCountMax >= hasuraConnections) {
topMessages[index] = item
}
}

View File

@ -31,8 +31,8 @@ func HasuraClient(browserConnection *common.BrowserConnection, cookies []*http.C
//Remove subscriptions from ActivitiesOverview here once Hasura-Reader will ignore "complete" msg for them
browserConnection.ActiveSubscriptionsMutex.RLock()
for _, subscription := range browserConnection.ActiveSubscriptions {
common.ActivitiesOverviewStarted(string(subscription.Type) + "-" + subscription.OperationName)
common.ActivitiesOverviewStarted("_Sum-" + string(subscription.Type))
common.ActivitiesOverviewCompleted(string(subscription.Type) + "-" + subscription.OperationName)
common.ActivitiesOverviewCompleted("_Sum-" + string(subscription.Type))
}
browserConnection.ActiveSubscriptionsMutex.RUnlock()
}()

View File

@ -61,3 +61,4 @@ select_permissions:
filter:
meetingId:
_eq: X-Hasura-MeetingId
allow_aggregations: true

View File

@ -42,3 +42,4 @@ select_permissions:
filter:
meetingId:
_eq: X-Hasura-PresenterInMeeting
allow_aggregations: true

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
import { defineMessages, injectIntl } from 'react-intl';
import Button from '/imports/ui/components/common/button/component';
import ConnectionStatusModalComponent from '/imports/ui/components/connection-status/modal/component';
import ConnectionStatusModalComponent from '/imports/ui/components/connection-status/modal/container';
import ConnectionStatusService from '/imports/ui/components/connection-status/service';
import Icon from '/imports/ui/components/connection-status/icon/component';
import Styled from './styles';
@ -40,17 +40,12 @@ class ConnectionStatusButton extends PureComponent {
setModalIsOpen = (isOpen) => this.setState({ isModalOpen: isOpen });
renderModal(isModalOpen) {
const {
connectionData,
} = this.props;
return (
isModalOpen ?
<ConnectionStatusModalComponent
{...{
isModalOpen,
setModalIsOpen: this.setModalIsOpen,
connectionData,
}}
/> : null
)
@ -85,17 +80,11 @@ class ConnectionStatusButton extends PureComponent {
}
const {
connectionData,
myCurrentStatus,
} = this.props;
const ownConnectionData = connectionData.filter((curr) => curr.user.userId === Auth.userID);
const currentStatus = ownConnectionData && ownConnectionData.length > 0
? ownConnectionData[0].currentStatus
: 'normal';
let color;
switch (currentStatus) {
switch (myCurrentStatus) {
case 'warning':
color = 'success';
break;
@ -114,7 +103,7 @@ class ConnectionStatusButton extends PureComponent {
return (
<Styled.ButtonWrapper>
<Button
customIcon={this.renderIcon(currentStatus)}
customIcon={this.renderIcon(myCurrentStatus)}
label={intl.formatMessage(intlMessages.label)}
hideLabel
aria-label={intl.formatMessage(intlMessages.description)}

View File

@ -3,15 +3,18 @@ import { Meteor } from 'meteor/meteor';
import { withTracker } from 'meteor/react-meteor-data';
import { useSubscription } from '@apollo/client';
import ConnectionStatusButtonComponent from './component';
import Service from '../service';
import { CONNECTION_STATUS_REPORT_SUBSCRIPTION } from '../queries';
import { USER_CURRENT_STATUS_SUBSCRIPTION } from '../queries';
import Auth from '/imports/ui/services/auth';
const connectionStatusButtonContainer = (props) => {
const { data } = useSubscription(CONNECTION_STATUS_REPORT_SUBSCRIPTION);
const { data } = useSubscription(USER_CURRENT_STATUS_SUBSCRIPTION, {
variables: { userId: Auth.userID },
});
const myCurrentStatus = data && data.length > 0
? data[0].currentStatus
: 'normal';
const connectionData = data ? Service.sortConnectionData(data.user_connectionStatusReport) : [];
return <ConnectionStatusButtonComponent connectionData={connectionData} {...props} />;
return <ConnectionStatusButtonComponent myCurrentStatus={myCurrentStatus} {...props} />;
};
export default withTracker(() => {

View File

@ -0,0 +1,18 @@
import React from 'react';
import { useSubscription } from '@apollo/client';
import { CONNECTION_STATUS_REPORT_SUBSCRIPTION } from '../queries';
import Service from '../service';
import Component from './component';
const ConnectionStatusContainer = (props) => {
const { data } = useSubscription(CONNECTION_STATUS_REPORT_SUBSCRIPTION);
const connectionData = data ? Service.sortConnectionData(data.user_connectionStatusReport) : [];
return (
<Component
connectionData={connectionData}
{...props}
/>
);
};
export default ConnectionStatusContainer;

View File

@ -1,7 +1,13 @@
import { gql } from '@apollo/client';
export const CONNECTION_STATUS_REPORT_SUBSCRIPTION = gql`subscription ConnStatusReport {
user_connectionStatusReport {
user_connectionStatusReport(
where: {
_or: [
{ clientNotResponding: { _eq: true } },
{ lastUnstableStatus: { _is_null: false } }
]
}) {
user {
userId
name
@ -17,6 +23,20 @@ export const CONNECTION_STATUS_REPORT_SUBSCRIPTION = gql`subscription ConnStatus
}
}`;
export const USER_CURRENT_STATUS_SUBSCRIPTION = gql`
subscription CurrentUserConnStatus($userId: String!) {
user_connectionStatusReport(
where: {
user: {
userId: { _eq: $userId }
}
}
) {
currentStatus
}
}
`;
export const CONNECTION_STATUS_SUBSCRIPTION = gql`subscription ConnStatus {
user_connectionStatus {
connectionAliveAt

View File

@ -8,6 +8,7 @@ import Auth from '/imports/ui/services/auth';
import { UsersContext } from '../components-data/users-context/context';
import { layoutDispatch, layoutSelectInput } from '../layout/context';
import { POLL_PUBLISH_RESULT, POLL_CANCEL, POLL_CREATE } from './mutations';
import PollCreationPanelContainer from './poll-graphql/component';
import { ACTIONS, PANELS } from '../layout/enums';
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
@ -87,7 +88,7 @@ const PollContainer = (props) => {
);
};
export default withTracker(({ amIPresenter, currentSlideId }) => {
withTracker(({ amIPresenter, currentSlideId }) => {
const isPollSecret = Session.get('secretPoll') || false;
Meteor.subscribe('current-poll', isPollSecret, amIPresenter);
@ -109,3 +110,5 @@ export default withTracker(({ amIPresenter, currentSlideId }) => {
getSplittedQuestionAndOptions: Service.getSplittedQuestionAndOptions,
};
})(PollContainer);
export default PollCreationPanelContainer;

View File

@ -0,0 +1,572 @@
import React, { useEffect, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Session } from 'meteor/session';
import { Meteor } from 'meteor/meteor';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import Header from '/imports/ui/components/common/control-header/component';
import { useMutation, useSubscription } from '@apollo/client';
import { Input } from '../../layout/layoutTypes';
import { layoutDispatch, layoutSelectInput } from '../../layout/context';
import { addNewAlert } from '../../screenreader-alert/service';
import { PANELS, ACTIONS } from '../../layout/enums';
import useMeeting from '/imports/ui/core/hooks/useMeeting';
import { POLL_CANCEL } from './mutation';
import { GetHasCurrentPresentationResponse, getHasCurrentPresentation } from './queries';
import EmptySlideArea from './components/EmptySlideArea';
import { getSplittedQuestionAndOptions, pollTypes, validateInput } from './service';
import Toggle from '/imports/ui/components/common/switch/component';
import Styled from '../styles';
import ResponseChoices from './components/ResponseChoices';
import ResponseTypes from './components/ResponseTypes';
import PollQuestionArea from './components/PollQuestionArea';
import LiveResultContainer from './components/LiveResult';
const POLL_SETTINGS = Meteor.settings.public.poll;
const ALLOW_CUSTOM_INPUT = POLL_SETTINGS.allowCustomResponseInput;
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
const intlMessages = defineMessages({
pollPaneTitle: {
id: 'app.poll.pollPaneTitle',
description: 'heading label for the poll menu',
},
closeLabel: {
id: 'app.poll.closeLabel',
description: 'label for poll pane close button',
},
hidePollDesc: {
id: 'app.poll.hidePollDesc',
description: 'aria label description for hide poll button',
},
quickPollInstruction: {
id: 'app.poll.quickPollInstruction',
description: 'instructions for using pre configured polls',
},
activePollInstruction: {
id: 'app.poll.activePollInstruction',
description: 'instructions displayed when a poll is active',
},
dragDropPollInstruction: {
id: 'app.poll.dragDropPollInstruction',
description: 'instructions for upload poll options via drag and drop',
},
ariaInputCount: {
id: 'app.poll.ariaInputCount',
description: 'aria label for custom poll input field',
},
customPlaceholder: {
id: 'app.poll.customPlaceholder',
description: 'custom poll input field placeholder text',
},
noPresentationSelected: {
id: 'app.poll.noPresentationSelected',
description: 'no presentation label',
},
clickHereToSelect: {
id: 'app.poll.clickHereToSelect',
description: 'open uploader modal button label',
},
questionErr: {
id: 'app.poll.questionErr',
description: 'question text area error label',
},
questionAndOptionsPlaceholder: {
id: 'app.poll.questionAndoptions.label',
description: 'poll input questions and options label',
},
customInputToggleLabel: {
id: 'app.poll.customInput.label',
description: 'poll custom input toogle button label',
},
customInputInstructionsLabel: {
id: 'app.poll.customInputInstructions.label',
description: 'poll custom input instructions label',
},
maxOptionsWarning: {
id: 'app.poll.maxOptionsWarning.label',
description: 'poll max options error',
},
optionErr: {
id: 'app.poll.optionErr',
description: 'poll input error label',
},
tf: {
id: 'app.poll.tf',
description: 'label for true / false poll',
},
a4: {
id: 'app.poll.a4',
description: 'label for A / B / C / D poll',
},
delete: {
id: 'app.poll.optionDelete.label',
description: '',
},
questionLabel: {
id: 'app.poll.question.label',
description: '',
},
optionalQuestionLabel: {
id: 'app.poll.optionalQuestion.label',
description: '',
},
userResponse: {
id: 'app.poll.userResponse.label',
description: '',
},
responseChoices: {
id: 'app.poll.responseChoices.label',
description: '',
},
typedResponseDesc: {
id: 'app.poll.typedResponse.desc',
description: '',
},
responseTypesLabel: {
id: 'app.poll.responseTypes.label',
description: '',
},
addOptionLabel: {
id: 'app.poll.addItem.label',
description: '',
},
startPollLabel: {
id: 'app.poll.start.label',
description: '',
},
secretPollLabel: {
id: 'app.poll.secretPoll.label',
description: '',
},
isSecretPollLabel: {
id: 'app.poll.secretPoll.isSecretLabel',
description: '',
},
true: {
id: 'app.poll.answer.true',
description: '',
},
false: {
id: 'app.poll.answer.false',
description: '',
},
a: {
id: 'app.poll.answer.a',
description: '',
},
b: {
id: 'app.poll.answer.b',
description: '',
},
c: {
id: 'app.poll.answer.c',
description: '',
},
d: {
id: 'app.poll.answer.d',
description: '',
},
e: {
id: 'app.poll.answer.e',
description: '',
},
yna: {
id: 'app.poll.yna',
description: '',
},
yes: {
id: 'app.poll.y',
description: '',
},
no: {
id: 'app.poll.n',
description: '',
},
abstention: {
id: 'app.poll.abstention',
description: '',
},
enableMultipleResponseLabel: {
id: 'app.poll.enableMultipleResponseLabel',
description: 'label for checkbox to enable multiple choice',
},
startPollDesc: {
id: 'app.poll.startPollDesc',
description: '',
},
showRespDesc: {
id: 'app.poll.showRespDesc',
description: '',
},
addRespDesc: {
id: 'app.poll.addRespDesc',
description: '',
},
deleteRespDesc: {
id: 'app.poll.deleteRespDesc',
description: '',
},
on: {
id: 'app.switch.onLabel',
description: 'label for toggle switch on state',
},
off: {
id: 'app.switch.offLabel',
description: 'label for toggle switch off state',
},
removePollOpt: {
id: 'app.poll.removePollOpt',
description: 'screen reader alert for removed poll option',
},
emptyPollOpt: {
id: 'app.poll.emptyPollOpt',
description: 'screen reader for blank poll option',
},
pollingQuestion: {
id: 'app.polling.pollQuestionTitle',
description: 'polling question header',
},
});
interface PollCreationPanelProps {
layoutContextDispatch: (action: {
type: string;
value: string | boolean;
}) => void;
hasPoll: boolean;
hasCurrentPresentation: boolean;
}
const PollCreationPanel: React.FC<PollCreationPanelProps> = ({
layoutContextDispatch,
hasPoll,
hasCurrentPresentation,
}) => {
const [stopPoll] = useMutation(POLL_CANCEL);
const intl = useIntl();
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
const [customInput, setCustomInput] = React.useState(false);
const [question, setQuestion] = useState<string[] | string>('');
const [questionAndOptions, setQuestionAndOptions] = useState<string[] | string>('');
const [optList, setOptList] = useState<Array<{val: string}>>([]);
const [error, setError] = useState<string | null>(null);
const [isMultipleResponse, setIsMultipleResponse] = useState(false);
const [secretPoll, setSecretPoll] = useState(false);
const [warning, setWarning] = useState<string | null>('');
const [isPasting, setIsPasting] = useState(false);
const [type, setType] = useState<string | null>('');
const handleInputChange = (
e: React.ChangeEvent<HTMLInputElement>,
index: number,
) => {
const list = [...optList];
const validatedVal = validateInput(e.target.value).replace(/\s{2,}/g, ' ');
const charsRemovedCount = e.target.value.length - validatedVal.length;
const clearError = validatedVal.length > 0 && type !== pollTypes.Response;
const input = e.target;
const caretStart = e.target.selectionStart ?? 0;
const caretEnd = e.target.selectionEnd ?? 0;
let questionAndOptionsList: string[] = [];
list[index] = { val: validatedVal };
if (questionAndOptions.length > 0) {
const QnO = questionAndOptions as string;
questionAndOptionsList = QnO.split('\n');
questionAndOptionsList[index + 1] = validatedVal;
}
setOptList(list);
setQuestionAndOptions(questionAndOptionsList.length > 0
? questionAndOptionsList.join('\n') : '');
setError(clearError ? null : error);
input.focus();
input.selectionStart = caretStart - charsRemovedCount;
input.selectionEnd = caretEnd - charsRemovedCount;
};
const setQuestionAndOptionsFn = (input: string[] | string) => {
const { splittedQuestion, optionsList } = getSplittedQuestionAndOptions(input);
const optionsListLength = optionsList.length;
let maxOptionsWarning = warning;
const clearWarning = maxOptionsWarning && optionsListLength <= MAX_CUSTOM_FIELDS;
const clearError = input.length > 0 && type === pollTypes.Response;
if (optionsListLength > MAX_CUSTOM_FIELDS && optList[MAX_CUSTOM_FIELDS] === undefined) {
setWarning(intl.formatMessage(intlMessages.maxOptionsWarning));
if (isPasting) {
maxOptionsWarning = intl.formatMessage(intlMessages.maxOptionsWarning);
setIsPasting(false);
}
}
setQuestionAndOptions(input);
setOptList(optionsList);
setQuestion(splittedQuestion);
setError(clearError ? null : error);
setWarning(clearWarning ? null : maxOptionsWarning);
};
const handleTextareaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const validatedInput = validateInput(e.target.value);
const clearError = validatedInput.length > 0 && type === pollTypes.Response;
if (!customInput) {
setQuestion(validatedInput);
setError(clearError ? null : error);
} else {
setQuestionAndOptionsFn(validatedInput);
}
};
const handleRemoveOption = (index: number) => {
const list = [...optList];
const removed = list[index];
let questionAndOptionsList: string[] = [];
let clearWarning = false;
list.splice(index, 1);
// If customInput then removing text from input field.
if (customInput) {
const QnO = questionAndOptions as string;
questionAndOptionsList = QnO.split('\n');
delete questionAndOptionsList[index + 1];
questionAndOptionsList = questionAndOptionsList.filter((val: string) => val !== undefined);
clearWarning = !!warning && list.length <= MAX_CUSTOM_FIELDS;
}
setOptList(list);
setQuestionAndOptions(questionAndOptionsList.length > 0
? questionAndOptionsList.join('\n') : '');
setWarning(clearWarning ? null : warning);
addNewAlert(`${intl.formatMessage(intlMessages.removePollOpt,
{ 0: removed.val || intl.formatMessage(intlMessages.emptyPollOpt) })}`);
};
const handleAddOption = () => {
setOptList([...optList, { val: '' }]);
};
const handleToggle = () => {
const toggledValue = !secretPoll;
Session.set('secretPoll', toggledValue);
setSecretPoll(toggledValue);
};
const handlePollLetterOptions = () => {
if (optList.length === 0) {
setType(pollTypes.Letter);
setOptList([
{ val: '' },
{ val: '' },
{ val: '' },
{ val: '' },
]);
}
};
const toggleIsMultipleResponse = () => {
setIsMultipleResponse((prev) => !prev);
return !isMultipleResponse;
};
useEffect(() => {
return () => {
Session.set('secretPoll', false);
};
}, []);
useEffect(() => {
if (textareaRef.current) {
textareaRef.current?.focus();
}
}, [textareaRef]);
const pollOptions = () => {
if (!hasCurrentPresentation) return <EmptySlideArea />;
if (hasPoll) return <LiveResultContainer />;
return (
<>
{
ALLOW_CUSTOM_INPUT && (
<Styled.CustomInputRow>
<Styled.CustomInputHeadingCol aria-hidden="true">
<Styled.CustomInputHeading>
{intl.formatMessage(intlMessages.customInputToggleLabel)}
</Styled.CustomInputHeading>
</Styled.CustomInputHeadingCol>
<Styled.CustomInputToggleCol>
<Styled.Toggle>
<Styled.ToggleLabel>
{customInput
? intl.formatMessage(intlMessages.on)
: intl.formatMessage(intlMessages.off)}
</Styled.ToggleLabel>
<Toggle
// @ts-ignore - JS component wrapped by intl
icons={false}
defaultChecked={customInput}
onChange={() => {
setCustomInput(!customInput);
setType(pollTypes.Custom);
}}
ariaLabel={intl.formatMessage(intlMessages.customInputToggleLabel)}
showToggleLabel={false}
data-test="autoOptioningPollBtn"
/>
</Styled.Toggle>
</Styled.CustomInputToggleCol>
</Styled.CustomInputRow>
)
}
{customInput && (
<Styled.PollParagraph>
{intl.formatMessage(intlMessages.customInputInstructionsLabel)}
</Styled.PollParagraph>
)}
<PollQuestionArea
customInput={customInput}
question={question}
questionAndOptions={questionAndOptions}
handleTextareaChange={handleTextareaChange}
error={error}
type={type}
textareaRef={textareaRef}
handlePollLetterOptions={handlePollLetterOptions}
optList={optList}
setIsPasting={setIsPasting}
warning={warning}
/>
<ResponseTypes
customInput={customInput}
type={type}
setOptList={setOptList}
setType={setType}
/>
<ResponseChoices
type={type}
toggleIsMultipleResponse={toggleIsMultipleResponse}
isMultipleResponse={isMultipleResponse}
optList={optList}
handleAddOption={handleAddOption}
secretPoll={secretPoll}
question={question}
setError={setError}
setIsPolling={() => {
setType(null);
setOptList([]);
setQuestion('');
setQuestionAndOptions('');
}}
hasCurrentPresentation={hasCurrentPresentation}
handleToggle={handleToggle}
error={error}
handleInputChange={handleInputChange}
handleRemoveOption={handleRemoveOption}
customInput={customInput}
questionAndOptions={questionAndOptions}
/>
</>
);
};
return (
<div>
<Header
data-test="pollPaneTitle"
leftButtonProps={{
'aria-label': intl.formatMessage(intlMessages.hidePollDesc),
'data-test': 'hidePollDesc',
label: intl.formatMessage(intlMessages.pollPaneTitle),
onClick: () => {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
},
}}
rightButtonProps={{
'aria-label': `${intl.formatMessage(intlMessages.closeLabel)} ${intl.formatMessage(intlMessages.pollPaneTitle)}`,
'data-test': 'closePolling',
icon: 'close',
label: intl.formatMessage(intlMessages.closeLabel),
onClick: () => {
if (hasPoll) stopPoll();
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
Session.set('forcePollOpen', false);
Session.set('pollInitiated', false);
},
}}
customRightButton={null}
/>
{pollOptions()}
<span className="sr-only" id="poll-config-button">{intl.formatMessage(intlMessages.showRespDesc)}</span>
<span className="sr-only" id="add-item-button">{intl.formatMessage(intlMessages.addRespDesc)}</span>
<span className="sr-only" id="start-poll-button">{intl.formatMessage(intlMessages.startPollDesc)}</span>
</div>
);
};
const PollCreationPanelContainer: React.FC = () => {
const sidebarContent = layoutSelectInput((i: Input) => i.sidebarContent);
const layoutContextDispatch = layoutDispatch();
const { sidebarContentPanel } = sidebarContent;
const {
data: currentUser,
loading: currentUserLoading,
} = useCurrentUser((u) => {
return {
presenter: u?.presenter,
};
});
const {
data: currentMeeting,
loading: currentMeetingLoading,
} = useMeeting((m) => {
return {
componentsFlags: m?.componentsFlags,
};
});
const {
data: getHasCurrentPresentationData,
loading: getHasCurrentPresentationLoading,
} = useSubscription<GetHasCurrentPresentationResponse>(getHasCurrentPresentation);
if (currentUserLoading || !currentUser) return null;
if (currentMeetingLoading || !currentMeeting) return null;
if (getHasCurrentPresentationLoading || !getHasCurrentPresentationData) return null;
if (!currentUser.presenter && sidebarContentPanel === PANELS.POLL) {
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: false,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.NONE,
});
}
return (
<PollCreationPanel
layoutContextDispatch={layoutContextDispatch}
hasPoll={currentMeeting.componentsFlags?.hasPoll ?? false}
hasCurrentPresentation={getHasCurrentPresentationData.pres_page_aggregate.aggregate.count > 0}
/>
);
};
export default PollCreationPanelContainer;

View File

@ -0,0 +1,33 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Session } from 'meteor/session';
import Styled from '../styles';
const intlMessages = defineMessages({
noPresentationSelected: {
id: 'app.poll.noPresentationSelected',
description: 'no presentation label',
},
clickHereToSelect: {
id: 'app.poll.clickHereToSelect',
description: 'open uploader modal button label',
},
});
const EmptySlideArea: React.FC = () => {
const intl = useIntl();
return (
<Styled.NoSlidePanelContainer>
<Styled.SectionHeading data-test="noPresentation">
{intl.formatMessage(intlMessages.noPresentationSelected)}
</Styled.SectionHeading>
<Styled.PollButton
label={intl.formatMessage(intlMessages.clickHereToSelect)}
color="primary"
onClick={() => Session.set('showUploadPresentationView', true)}
/>
</Styled.NoSlidePanelContainer>
);
};
export default EmptySlideArea;

View File

@ -0,0 +1,255 @@
import { useMutation, useSubscription } from '@apollo/client';
import React, { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Session } from 'meteor/session';
import {
Bar, BarChart, ResponsiveContainer, XAxis, YAxis,
} from 'recharts';
import Styled from '../styles';
import {
ResponseInfo,
UserInfo,
getCurrentPollData,
getCurrentPollDataResponse,
} from '../queries';
import logger from '/imports/startup/client/logger';
import Settings from '/imports/ui/services/settings';
import { POLL_CANCEL, POLL_PUBLISH_RESULT } from '../mutation';
import { layoutDispatch } from '../../../layout/context';
import { ACTIONS, PANELS } from '../../../layout/enums';
const intlMessages = defineMessages({
usersTitle: {
id: 'app.poll.liveResult.usersTitle',
description: 'heading label for poll users',
},
responsesTitle: {
id: 'app.poll.liveResult.responsesTitle',
description: 'heading label for poll responses',
},
publishLabel: {
id: 'app.poll.publishLabel',
description: 'label for the publish button',
},
cancelPollLabel: {
id: 'app.poll.cancelPollLabel',
description: 'label for cancel poll button',
},
backLabel: {
id: 'app.poll.backLabel',
description: 'label for the return to poll options button',
},
doneLabel: {
id: 'app.createBreakoutRoom.doneLabel',
description: 'label shown when all users have responded',
},
waitingLabel: {
id: 'app.poll.waitingLabel',
description: 'label shown while waiting for responses',
},
secretPollLabel: {
id: 'app.poll.liveResult.secretLabel',
description: 'label shown instead of users in poll responses if poll is secret',
},
activePollInstruction: {
id: 'app.poll.activePollInstruction',
description: 'instructions displayed when a poll is active',
},
});
interface LiveResultProps {
questionText: string;
responses: Array<ResponseInfo>;
usersCount: number;
numberOfAnswerCount: number;
animations: boolean;
pollId: string;
users: Array<UserInfo>;
isSecret: boolean;
}
const CHAT_CONFIG = window.meetingClientSettings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_group_id;
const LiveResult: React.FC<LiveResultProps> = ({
questionText,
responses,
usersCount,
numberOfAnswerCount,
animations,
pollId,
users,
isSecret,
}) => {
const intl = useIntl();
const [pollPublishResult] = useMutation(POLL_PUBLISH_RESULT);
const [stopPoll] = useMutation(POLL_CANCEL);
const layoutContextDispatch = layoutDispatch();
const publishPoll = useCallback((pId: string) => {
pollPublishResult({
variables: {
pollId: pId,
},
});
}, []);
return (
<div>
<Styled.Instructions>
{intl.formatMessage(intlMessages.activePollInstruction)}
</Styled.Instructions>
<Styled.Stats>
{questionText ? <Styled.Title data-test="currentPollQuestion">{questionText}</Styled.Title> : null}
<Styled.Status>
{usersCount !== numberOfAnswerCount
? (
<span>
{`${intl.formatMessage(intlMessages.waitingLabel, {
0: numberOfAnswerCount,
1: usersCount,
})} `}
</span>
)
: <span>{intl.formatMessage(intlMessages.doneLabel)}</span>}
{usersCount !== numberOfAnswerCount
? <Styled.ConnectingAnimation animations={animations} /> : null}
</Styled.Status>
<ResponsiveContainer width="90%" height={250}>
<BarChart
data={responses}
layout="vertical"
>
<XAxis type="number" />
<YAxis width={70} type="category" dataKey="optionDesc" />
<Bar dataKey="optionResponsesCount" fill="#0C57A7" />
</BarChart>
</ResponsiveContainer>
</Styled.Stats>
{numberOfAnswerCount >= 0
? (
<Styled.ButtonsActions>
<Styled.PublishButton
onClick={() => {
Session.set('pollInitiated', false);
publishPoll(pollId);
stopPoll();
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_IS_OPEN,
value: true,
});
layoutContextDispatch({
type: ACTIONS.SET_SIDEBAR_CONTENT_PANEL,
value: PANELS.CHAT,
});
layoutContextDispatch({
type: ACTIONS.SET_ID_CHAT_OPEN,
value: PUBLIC_CHAT_KEY,
});
}}
disabled={numberOfAnswerCount <= 0}
label={intl.formatMessage(intlMessages.publishLabel)}
data-test="publishPollingLabel"
color="primary"
/>
<Styled.CancelButton
onClick={() => {
Session.set('pollInitiated', false);
Session.set('resetPollPanel', true);
stopPoll();
}}
label={intl.formatMessage(intlMessages.cancelPollLabel)}
data-test="cancelPollLabel"
/>
</Styled.ButtonsActions>
) : (
<Styled.LiveResultButton
onClick={() => {
stopPoll();
}}
label={intl.formatMessage(intlMessages.backLabel)}
color="primary"
data-test="restartPoll"
/>
)}
<Styled.Separator />
{
!isSecret
? (
<table>
<tbody>
<tr>
<Styled.THeading>{intl.formatMessage(intlMessages.usersTitle)}</Styled.THeading>
<Styled.THeading>{intl.formatMessage(intlMessages.responsesTitle)}</Styled.THeading>
</tr>
{
users.map((user) => (
<tr key={user.user.userId}>
<Styled.ResultLeft>{user.user.name}</Styled.ResultLeft>
<Styled.ResultRight data-test="userVoteLiveResult">{user.optionDescIds.join()}</Styled.ResultRight>
</tr>
))
}
</tbody>
</table>
)
: (
<div>
{intl.formatMessage(intlMessages.secretPollLabel)}
</div>
)
}
</div>
);
};
const LiveResultContainer: React.FC = () => {
const {
data: currentPollData,
loading: currentPollLoading,
error: currentPollDataError,
} = useSubscription<getCurrentPollDataResponse>(getCurrentPollData);
if (currentPollLoading || !currentPollData) {
return null;
}
if (currentPollDataError) {
logger.error(currentPollDataError);
return (
<div>
{JSON.stringify(currentPollDataError)}
</div>
);
}
if (!currentPollData.poll.length) return null;
// @ts-ignore - JS code
const { animations } = Settings.application;
const currentPoll = currentPollData.poll[0];
const isSecret = currentPoll.secret;
const {
questionText,
responses,
pollId,
users,
} = currentPoll;
const numberOfAnswerCount = currentPoll.responses_aggregate.aggregate.sum.optionResponsesCount;
const numberOfUsersCount = currentPoll.users_aggregate.aggregate.count;
return (
<LiveResult
questionText={questionText}
responses={responses}
isSecret={isSecret}
usersCount={numberOfUsersCount}
numberOfAnswerCount={numberOfAnswerCount ?? 0}
animations={animations}
pollId={pollId}
users={users}
/>
);
};
export default LiveResultContainer;

View File

@ -0,0 +1,96 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import { pollTypes } from '../service';
import Styled from '../styles';
const POLL_SETTINGS = Meteor.settings.public.poll;
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
const MAX_INPUT_CHARS = POLL_SETTINGS.maxTypedAnswerLength;
const MIN_OPTIONS_LENGTH = 2;
const intlMessages = defineMessages({
customPlaceholder: {
id: 'app.poll.customPlaceholder',
description: 'custom poll input field placeholder text',
},
delete: {
id: 'app.poll.optionDelete.label',
description: '',
},
deleteRespDesc: {
id: 'app.poll.deleteRespDesc',
description: '',
},
emptyPollOpt: {
id: 'app.poll.emptyPollOpt',
description: 'screen reader for blank poll option',
},
});
interface PollInputsProps {
optList: Array<{ val: string }>;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>, i: number) => void;
handleRemoveOption: (i: number) => void;
type: string | null;
error: string | null;
}
const PollInputs: React.FC<PollInputsProps> = ({
optList,
handleInputChange,
handleRemoveOption,
type,
error,
}) => {
const intl = useIntl();
let hasVal = false;
return optList.slice(0, MAX_CUSTOM_FIELDS).map((o: { val: string }, i: number) => {
const pollOptionKey = `poll-option-${i}`;
if (o.val && o.val.length > 0) hasVal = true;
return (
<span key={pollOptionKey}>
<Styled.OptionWrapper>
<Styled.PollOptionInput
type="text"
value={o.val}
placeholder={intl.formatMessage(intlMessages.customPlaceholder)}
data-test="pollOptionItem"
onChange={(e) => handleInputChange(e, i)}
maxLength={MAX_INPUT_CHARS}
onPaste={(e) => { e.stopPropagation(); }}
onCut={(e) => { e.stopPropagation(); }}
onCopy={(e) => { e.stopPropagation(); }}
/>
{optList.length > MIN_OPTIONS_LENGTH && (
<Styled.DeletePollOptionButton
label={intl.formatMessage(intlMessages.delete)}
aria-describedby={`option-${i}`}
icon="delete"
data-test="deletePollOption"
hideLabel
circle
color="default"
onClick={() => {
handleRemoveOption(i);
}}
/>
)}
<span className="sr-only" id={`option-${i}`}>
{intl.formatMessage(
intlMessages.deleteRespDesc,
{ 0: o.val || intl.formatMessage(intlMessages.emptyPollOpt) },
)}
</span>
</Styled.OptionWrapper>
{!hasVal && type !== pollTypes.Response && error ? (
<Styled.InputError data-test="errorNoValueInput">{error}</Styled.InputError>
) : (
<Styled.ErrorSpacer>&nbsp;</Styled.ErrorSpacer>
)}
</span>
);
});
};
export default PollInputs;

View File

@ -0,0 +1,104 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import DraggableTextArea from '/imports/ui/components/poll/dragAndDrop/component';
import { pollTypes } from '../service';
import Styled from '../styles';
const POLL_SETTINGS = Meteor.settings.public.poll;
const MAX_INPUT_CHARS = POLL_SETTINGS.maxTypedAnswerLength;
const QUESTION_MAX_INPUT_CHARS = 1200;
const intlMessages = defineMessages({
questionAndOptionsPlaceholder: {
id: 'app.poll.questionAndoptions.label',
description: 'poll input questions and options label',
},
questionLabel: {
id: 'app.poll.question.label',
description: '',
},
optionalQuestionLabel: {
id: 'app.poll.optionalQuestion.label',
description: '',
},
});
interface PollQuestionAreaProps {
customInput: boolean;
optList: Array<{ val: string }>;
warning: string | null;
type: string | null;
error: string | null;
questionAndOptions: string | string[];
handleTextareaChange: (e: React.ChangeEvent<HTMLTextAreaElement>) => void;
setIsPasting: (isPasting: boolean) => void;
handlePollLetterOptions: () => void;
textareaRef: React.RefObject<HTMLTextAreaElement>;
question: string | string[];
}
const PollQuestionArea: React.FC<PollQuestionAreaProps> = ({
customInput,
optList,
warning,
error,
type,
questionAndOptions,
handleTextareaChange,
setIsPasting,
handlePollLetterOptions,
textareaRef,
question,
}) => {
const intl = useIntl();
const hasOptionError = (customInput && optList.length === 0 && error);
const hasWarning = (customInput && warning);
const hasQuestionError = (type === pollTypes.Response
&& questionAndOptions.length === 0 && error);
const questionsAndOptionsPlaceholder = intlMessages.questionAndOptionsPlaceholder;
const questionPlaceholder = (type === pollTypes.Response)
? intlMessages.questionLabel
: intlMessages.optionalQuestionLabel;
return (
<div>
<Styled.PollQuestionArea
hasError={hasQuestionError || hasOptionError}
data-test="pollQuestionArea"
value={customInput ? questionAndOptions : question}
onChange={(e) => handleTextareaChange(e)}
onPaste={(e) => { e.stopPropagation(); setIsPasting(true); }}
onCut={(e) => { e.stopPropagation(); }}
onCopy={(e) => { e.stopPropagation(); }}
onKeyPress={(event) => {
if (event.key === 'Enter' && customInput) {
handlePollLetterOptions();
}
}}
rows="5"
cols="35"
maxLength={QUESTION_MAX_INPUT_CHARS}
aria-label={intl.formatMessage(customInput ? questionsAndOptionsPlaceholder
: questionPlaceholder)}
placeholder={intl.formatMessage(customInput ? questionsAndOptionsPlaceholder
: questionPlaceholder)}
{...{ MAX_INPUT_CHARS }}
as={customInput ? DraggableTextArea : 'textarea'}
ref={textareaRef}
/>
{hasQuestionError || hasOptionError ? (
<Styled.InputError>{error}</Styled.InputError>
) : (
<Styled.ErrorSpacer>&nbsp;</Styled.ErrorSpacer>
)}
{hasWarning ? (
<Styled.Warning>{warning}</Styled.Warning>
) : (
<Styled.ErrorSpacer>&nbsp;</Styled.ErrorSpacer>
)}
</div>
);
};
export default PollQuestionArea;

View File

@ -0,0 +1,162 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import Checkbox from '/imports/ui/components/common/checkbox/component';
import Toggle from '/imports/ui/components/common/switch/component';
import Styled from '../styles';
import { pollTypes, isDefaultPoll } from '../service';
import StartPollButton from './StartPollButton';
import PollInputs from './PollInputs';
const POLL_SETTINGS = Meteor.settings.public.poll;
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
const intlMessages = defineMessages({
enableMultipleResponseLabel: {
id: 'app.poll.enableMultipleResponseLabel',
description: 'label for checkbox to enable multiple choice',
},
addOptionLabel: {
id: 'app.poll.addItem.label',
description: '',
},
secretPollLabel: {
id: 'app.poll.secretPoll.label',
description: '',
},
isSecretPollLabel: {
id: 'app.poll.secretPoll.isSecretLabel',
description: '',
},
on: {
id: 'app.switch.onLabel',
description: 'label for toggle switch on state',
},
off: {
id: 'app.switch.offLabel',
description: 'label for toggle switch off state',
},
});
interface ResponseAreaProps {
type: string | null;
toggleIsMultipleResponse: () => void;
isMultipleResponse: boolean;
optList: Array<{ val: string }>;
handleAddOption: () => void;
secretPoll: boolean;
question: string | string[];
setError: (err: string) => void;
setIsPolling: (isPolling: boolean) => void;
hasCurrentPresentation: boolean;
handleToggle: () => void;
error: string | null;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>, i: number) => void;
handleRemoveOption: (i: number) => void;
}
const ResponseArea: React.FC<ResponseAreaProps> = ({
type,
toggleIsMultipleResponse,
isMultipleResponse,
optList,
handleAddOption,
secretPoll,
question,
setError,
setIsPolling,
hasCurrentPresentation,
handleToggle,
error,
handleInputChange,
handleRemoveOption,
}) => {
const intl = useIntl();
const defaultPoll = isDefaultPoll(type as string);
if (defaultPoll || type === pollTypes.Response) {
return (
<Styled.ResponseArea>
{defaultPoll && (
<div>
<Styled.PollCheckbox data-test="allowMultiple">
<Checkbox
onChange={toggleIsMultipleResponse}
checked={isMultipleResponse}
ariaLabelledBy="multipleResponseCheckboxLabel"
label={intl.formatMessage(intlMessages.enableMultipleResponseLabel)}
/>
</Styled.PollCheckbox>
<div id="multipleResponseCheckboxLabel" hidden>
{intl.formatMessage(intlMessages.enableMultipleResponseLabel)}
</div>
</div>
)}
{defaultPoll && (
<PollInputs
error={error}
optList={optList}
handleInputChange={handleInputChange}
handleRemoveOption={handleRemoveOption}
type={type}
/>
)}
{defaultPoll && (
<Styled.AddItemButton
data-test="addPollItem"
label={intl.formatMessage(intlMessages.addOptionLabel)}
aria-describedby="add-item-button"
color="default"
icon="add"
disabled={optList.length >= MAX_CUSTOM_FIELDS}
onClick={() => handleAddOption()}
/>
)}
<Styled.AnonymousRow>
<Styled.AnonymousHeadingCol aria-hidden="true">
<Styled.AnonymousHeading>
{intl.formatMessage(intlMessages.secretPollLabel)}
</Styled.AnonymousHeading>
</Styled.AnonymousHeadingCol>
<Styled.AnonymousToggleCol>
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<Styled.Toggle>
<Styled.ToggleLabel>
{secretPoll
? intl.formatMessage(intlMessages.on)
: intl.formatMessage(intlMessages.off)}
</Styled.ToggleLabel>
<Toggle
// @ts-ignore - component Wrapped by intl, not reflecting the correct props
icons={false}
defaultChecked={secretPoll}
onChange={() => handleToggle()}
ariaLabel={intl.formatMessage(intlMessages.secretPollLabel)}
showToggleLabel={false}
data-test="anonymousPollBtn"
/>
</Styled.Toggle>
</Styled.AnonymousToggleCol>
</Styled.AnonymousRow>
{secretPoll && (
<Styled.PollParagraph>
{intl.formatMessage(intlMessages.isSecretPollLabel)}
</Styled.PollParagraph>
)}
<StartPollButton
hasCurrentPresentation={hasCurrentPresentation}
question={question}
isMultipleResponse={isMultipleResponse}
optList={optList}
type={type}
secretPoll={secretPoll}
setError={setError}
setIsPolling={setIsPolling}
key="startPollButton"
/>
</Styled.ResponseArea>
);
}
return null;
};
export default ResponseArea;

View File

@ -0,0 +1,103 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Styled from '../styles';
import { pollTypes } from '../service';
import ResponseArea from './ResponseArea';
const intlMessages = defineMessages({
typedResponseDesc: {
id: 'app.poll.typedResponse.desc',
description: '',
},
responseChoices: {
id: 'app.poll.responseChoices.label',
description: '',
},
pollingQuestion: {
id: 'app.polling.pollQuestionTitle',
description: 'polling question header',
},
});
interface ResponseChoicesProps {
type: string | null;
toggleIsMultipleResponse: () => void;
isMultipleResponse: boolean;
optList: Array<{ val: string }>;
handleAddOption: () => void;
secretPoll: boolean;
question: string | string[];
setError: (err: string) => void;
setIsPolling: (isPolling: boolean) => void;
hasCurrentPresentation: boolean;
handleToggle: () => void;
error: string | null;
handleInputChange: (e: React.ChangeEvent<HTMLInputElement>, i: number) => void;
handleRemoveOption: (i: number) => void;
customInput: boolean;
questionAndOptions: string[] | string;
}
const ResponseChoices: React.FC<ResponseChoicesProps> = ({
type,
toggleIsMultipleResponse,
isMultipleResponse,
optList,
handleAddOption,
secretPoll,
question,
setError,
setIsPolling,
hasCurrentPresentation,
handleToggle,
error,
handleInputChange,
handleRemoveOption,
customInput,
questionAndOptions,
}) => {
const intl = useIntl();
if ((!customInput && type) || (questionAndOptions && customInput)) {
return (
<div data-test="responseChoices">
{customInput && questionAndOptions && (
<Styled.Question>
<Styled.SectionHeading>
{intl.formatMessage(intlMessages.pollingQuestion)}
</Styled.SectionHeading>
<Styled.PollParagraph>
<span>{question}</span>
</Styled.PollParagraph>
</Styled.Question>
)}
<Styled.SectionHeading>
{intl.formatMessage(intlMessages.responseChoices)}
</Styled.SectionHeading>
{type === pollTypes.Response && (
<Styled.PollParagraph>
<span>{intl.formatMessage(intlMessages.typedResponseDesc)}</span>
</Styled.PollParagraph>
)}
<ResponseArea
error={error}
type={type}
toggleIsMultipleResponse={toggleIsMultipleResponse}
isMultipleResponse={isMultipleResponse}
optList={optList}
handleAddOption={handleAddOption}
secretPoll={secretPoll}
question={question}
setError={setError}
setIsPolling={setIsPolling}
hasCurrentPresentation={hasCurrentPresentation}
handleToggle={handleToggle}
handleInputChange={handleInputChange}
handleRemoveOption={handleRemoveOption}
/>
</div>
);
}
return null;
};
export default ResponseChoices;

View File

@ -0,0 +1,158 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { pollTypes } from '../service';
import Styled from '../styles';
const intlMessages = defineMessages({
responseTypesLabel: {
id: 'app.poll.responseTypes.label',
description: '',
},
tf: {
id: 'app.poll.tf',
description: 'label for true / false poll',
},
true: {
id: 'app.poll.answer.true',
description: '',
},
false: {
id: 'app.poll.answer.false',
description: '',
},
a4: {
id: 'app.poll.a4',
description: 'label for A / B / C / D poll',
},
a: {
id: 'app.poll.answer.a',
description: '',
},
b: {
id: 'app.poll.answer.b',
description: '',
},
c: {
id: 'app.poll.answer.c',
description: '',
},
d: {
id: 'app.poll.answer.d',
description: '',
},
e: {
id: 'app.poll.answer.e',
description: '',
},
yna: {
id: 'app.poll.yna',
description: '',
},
yes: {
id: 'app.poll.y',
description: '',
},
no: {
id: 'app.poll.n',
description: '',
},
abstention: {
id: 'app.poll.abstention',
description: '',
},
userResponse: {
id: 'app.poll.userResponse.label',
description: '',
},
});
interface ResponseTypesProps {
customInput: boolean;
setType: (type: string | null) => void;
type: string | null;
setOptList: (optList: Array<{ val: string }>) => void;
}
const ResponseTypes: React.FC<ResponseTypesProps> = ({
customInput,
setType,
type,
setOptList,
}) => {
const intl = useIntl();
if (!customInput) {
return (
<div data-test="responseTypes">
<Styled.SectionHeading>
{intl.formatMessage(intlMessages.responseTypesLabel)}
</Styled.SectionHeading>
<Styled.ResponseType>
<Styled.PollConfigButton
selected={type === pollTypes.TrueFalse}
small={false}
label={intl.formatMessage(intlMessages.tf)}
aria-describedby="poll-config-button"
data-test="pollTrueFalse"
color="default"
onClick={() => {
setType(pollTypes.TrueFalse);
setOptList([
{ val: intl.formatMessage(intlMessages.true) },
{ val: intl.formatMessage(intlMessages.false) },
]);
}}
/>
<Styled.PollConfigButton
selected={type === pollTypes.Letter}
small={false}
label={intl.formatMessage(intlMessages.a4)}
aria-describedby="poll-config-button"
data-test="pollLetterAlternatives"
color="default"
onClick={() => {
if (!customInput) {
setType(pollTypes.Letter);
setOptList([
{ val: intl.formatMessage(intlMessages.a) },
{ val: intl.formatMessage(intlMessages.b) },
{ val: intl.formatMessage(intlMessages.c) },
{ val: intl.formatMessage(intlMessages.d) },
]);
}
}}
/>
<Styled.PollConfigButton
selected={type === pollTypes.YesNoAbstention}
small={false}
full
label={intl.formatMessage(intlMessages.yna)}
aria-describedby="poll-config-button"
data-test="pollYesNoAbstentionBtn"
color="default"
onClick={() => {
setType(pollTypes.YesNoAbstention);
setOptList([
{ val: intl.formatMessage(intlMessages.yes) },
{ val: intl.formatMessage(intlMessages.no) },
{ val: intl.formatMessage(intlMessages.abstention) },
]);
}}
/>
<Styled.PollConfigButton
selected={type === pollTypes.Response}
small={false}
full
label={intl.formatMessage(intlMessages.userResponse)}
aria-describedby="poll-config-button"
data-test="userResponseBtn"
color="default"
onClick={() => { setType(pollTypes.Response); }}
/>
</Styled.ResponseType>
</div>
);
}
return null;
};
export default ResponseTypes;

View File

@ -0,0 +1,150 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import { useMutation } from '@apollo/client';
import Styled from '../styles';
import { pollTypes, checkPollType } from '../service';
import { POLL_CREATE } from '../mutation';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
const POLL_SETTINGS = Meteor.settings.public.poll;
const MAX_CUSTOM_FIELDS = POLL_SETTINGS.maxCustom;
const intlMessages = defineMessages({
startPollLabel: {
id: 'app.poll.start.label',
description: '',
},
questionErr: {
id: 'app.poll.questionErr',
description: 'question text area error label',
},
optionErr: {
id: 'app.poll.optionErr',
description: 'poll input error label',
},
yes: {
id: 'app.poll.y',
description: '',
},
no: {
id: 'app.poll.n',
description: '',
},
abstention: {
id: 'app.poll.abstention',
description: '',
},
true: {
id: 'app.poll.answer.true',
description: '',
},
false: {
id: 'app.poll.answer.false',
description: '',
},
});
interface StartPollButtonProps {
optList: Array<{val: string}>;
question: string | string[];
type: string | null;
setError: (err: string) => void;
setIsPolling: (isPolling: boolean) => void;
secretPoll: boolean;
isMultipleResponse: boolean;
hasCurrentPresentation: boolean;
}
const StartPollButton: React.FC<StartPollButtonProps> = ({
optList,
question,
type,
setError,
setIsPolling,
secretPoll,
isMultipleResponse,
hasCurrentPresentation,
}) => {
const intl = useIntl();
const [createPoll] = useMutation(POLL_CREATE);
const startPoll = (
pollType: string | null,
secretPoll: boolean,
question: string | string[],
isMultipleResponse: boolean,
answers: (string | null)[] = [],
) => {
const pollId = hasCurrentPresentation || PUBLIC_CHAT_KEY;
createPoll({
variables: {
pollType,
pollId: `${pollId}/${new Date().getTime()}`,
secretPoll,
question,
isMultipleResponse,
answers,
},
});
};
return (
<Styled.StartPollBtn
data-test="startPoll"
label={intl.formatMessage(intlMessages.startPollLabel)}
color="primary"
onClick={() => {
const optionsList = optList.slice(0, MAX_CUSTOM_FIELDS);
let hasVal = false;
optionsList.forEach((o) => {
if (o.val.trim().length > 0) hasVal = true;
});
let err = null;
if (type === pollTypes.Response && question.length === 0) {
err = intl.formatMessage(intlMessages.questionErr);
}
if (!hasVal && type !== pollTypes.Response) {
err = intl.formatMessage(intlMessages.optionErr);
}
if (err) {
setError(err);
} else {
setIsPolling(true);
const verifiedPollType = checkPollType(
type,
optionsList,
intl.formatMessage(intlMessages.yes),
intl.formatMessage(intlMessages.no),
intl.formatMessage(intlMessages.abstention),
intl.formatMessage(intlMessages.true),
intl.formatMessage(intlMessages.false),
);
const verifiedOptions = optionsList.map((o) => {
if (o.val.trim().length > 0) return o.val;
return null;
});
if (verifiedPollType === pollTypes.Custom) {
startPoll(
verifiedPollType,
secretPoll,
question,
isMultipleResponse,
verifiedOptions?.filter(Boolean),
);
} else {
startPoll(verifiedPollType, secretPoll, question, isMultipleResponse);
}
}
}}
/>
);
};
export default StartPollButton;

View File

@ -0,0 +1,61 @@
import { gql } from '@apollo/client';
export const POLL_PUBLISH_RESULT = gql`
mutation PollPublishResult($pollId: String!) {
pollPublishResult(
pollId: $pollId,
)
}
`;
export const POLL_SUBMIT_TYPED_VOTE = gql`
mutation PollSubmitTypedVote($pollId: String!, $answer: String!) {
pollSubmitUserTypedVote(
pollId: $pollId,
answer: $answer,
)
}
`;
export const POLL_SUBMIT_VOTE = gql`
mutation PollSubmitVote($pollId: String!, $answerIds: [Int]!) {
pollSubmitUserVote(
pollId: $pollId,
answerIds: $answerIds,
)
}
`;
export const POLL_CANCEL = gql`
mutation PollCancel {
pollCancel
}
`;
export const POLL_CREATE = gql`
mutation PollCreate(
$pollType: String!,
$pollId: String!,
$secretPoll: Boolean!,
$question: String!,
$isMultipleResponse: Boolean!,
$answers: [String]!
) {
pollCreate(
pollType: $pollType,
pollId: $pollId,
secretPoll: $secretPoll,
question: $question,
isMultipleResponse: $isMultipleResponse,
answers: $answers,
)
}
`;
export default {
POLL_PUBLISH_RESULT,
POLL_SUBMIT_TYPED_VOTE,
POLL_SUBMIT_VOTE,
POLL_CANCEL,
POLL_CREATE,
};

View File

@ -0,0 +1,103 @@
import { gql } from '@apollo/client';
export interface GetHasCurrentPresentationResponse {
pres_page_aggregate: {
aggregate: {
count: number;
};
};
}
export interface UserInfo {
user: {
name: string;
userId: string;
};
optionDescIds: Array<string>;
}
export interface ResponseInfo {
optionResponsesCount: number;
optionDesc: string;
pollResponsesCount: number;
}
export interface PollInfo {
published: boolean;
pollId: string;
secret: boolean;
questionText: string;
ended: boolean;
multipleResponses: boolean;
users: Array<UserInfo>;
responses: Array<ResponseInfo>;
users_aggregate: {
aggregate: {
count: number;
};
};
responses_aggregate: {
aggregate: {
count: number;
sum: {
optionResponsesCount: number;
}
};
};
}
export interface getCurrentPollDataResponse {
poll: Array<PollInfo>;
}
export const getHasCurrentPresentation = gql`
subscription getHasCurrentPresentation {
pres_page_aggregate(where: {isCurrentPage: {_eq: true}}) {
aggregate {
count
}
}
}
`;
export const getCurrentPollData = gql`
subscription getCurrentPollData {
poll(order_by: {createdAt: desc}, limit: 1,) {
pollId
published
secret
questionText
ended
multipleResponses
users(where: {responded: {_eq: true}}) {
user {
name
userId
}
optionDescIds
}
responses {
optionResponsesCount
optionDesc
pollResponsesCount
}
users_aggregate {
aggregate {
count
}
}
responses_aggregate {
aggregate {
count
sum {
optionResponsesCount
}
}
}
}
}
`;
export default {
getHasCurrentPresentation,
};

View File

@ -0,0 +1,118 @@
export const pollTypes = {
YesNo: 'YN',
YesNoAbstention: 'YNA',
TrueFalse: 'TF',
Letter: 'A-',
A2: 'A-2',
A3: 'A-3',
A4: 'A-4',
A5: 'A-5',
Custom: 'CUSTOM',
Response: 'R-',
};
export const validateInput = (input: string) => {
let i = input;
while (/^\s/.test(i)) i = i.substring(1);
return i;
};
export const getSplittedQuestionAndOptions = (questionAndOptions: string[] | string) => {
const inputList = Array.isArray(questionAndOptions)
? questionAndOptions
: questionAndOptions.split('\n').filter((val: string) => val !== '');
const splittedQuestion = inputList.length > 0 ? inputList[0] : questionAndOptions;
const optList = inputList.slice(1);
const optionsList = optList.map((val) => {
const option = validateInput(val);
return { val: option };
});
return {
splittedQuestion,
optionsList,
};
};
export const removeEmptyLineSpaces = (input: string) => {
const filteredInput = input.split('\n').filter((val) => val.trim() !== '');
return filteredInput;
};
export const isDefaultPoll = (pollType: string) => pollType !== pollTypes.Response;
const matchYesNoPoll = (yesValue: string, noValue: string, contentString: string) => {
const ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`;
const ynOptionsRegex = new RegExp(ynPollString, 'gi');
const ynPoll = contentString.replace(/\n/g, '').match(ynOptionsRegex) || [];
return ynPoll;
};
const matchYesNoAbstentionPoll = (yesValue:string, noValue:string, abstentionValue:string, contentString:string) => {
/* eslint max-len: [off] */
const ynaPollString = `(${yesValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${abstentionValue})|(${yesValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${noValue})|(${abstentionValue}\\s*\\/\\s*${noValue}\\s*\\/\\s*${yesValue})|(${noValue}\\s*\\/\\s*${yesValue}\\s*\\/\\s*${abstentionValue})|(${noValue}\\s*\\/\\s*${abstentionValue}\\s*\\/\\s*${yesValue})`;
const ynaOptionsRegex = new RegExp(ynaPollString, 'gi');
const ynaPoll = contentString.replace(/\n/g, '').match(ynaOptionsRegex) || [];
return ynaPoll;
};
const matchTrueFalsePoll = (trueValue:string, falseValue:string, contentString:string) => {
const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`;
const tgOptionsRegex = new RegExp(tfPollString, 'gi');
const tfPoll = contentString.match(tgOptionsRegex) || [];
return tfPoll;
};
export const checkPollType = (
type: string | null,
optList: { val: string }[],
yesValue: string,
noValue: string,
abstentionValue: string,
trueValue: string,
falseValue: string,
) => {
/* eslint no-underscore-dangle: "off" */
let _type = type;
let pollString = '';
let defaultMatch: RegExpMatchArray | [] | null = null;
let isDefault = null;
switch (_type) {
case pollTypes.Letter:
pollString = optList.map((x) => x.val.toUpperCase()).sort().join('');
defaultMatch = pollString.match(/^(ABCDEF)|(ABCDE)|(ABCD)|(ABC)|(AB)$/gi);
isDefault = defaultMatch && pollString.length === defaultMatch[0].length;
_type = isDefault && Array.isArray(defaultMatch) ? `${_type}${defaultMatch[0].length}` : pollTypes.Custom;
break;
case pollTypes.TrueFalse:
pollString = optList.map((x) => x.val).join('/');
defaultMatch = matchTrueFalsePoll(trueValue, falseValue, pollString);
isDefault = defaultMatch.length > 0 && pollString.length === (defaultMatch[0]?.length);
if (!isDefault) _type = pollTypes.Custom;
break;
case pollTypes.YesNoAbstention:
pollString = optList.map((x) => x.val).join('/');
defaultMatch = matchYesNoAbstentionPoll(yesValue, noValue, abstentionValue, pollString);
isDefault = Array.isArray(defaultMatch) && defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
if (!isDefault) {
// also try to match only yes/no
defaultMatch = matchYesNoPoll(yesValue, noValue, pollString);
isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
_type = isDefault ? pollTypes.YesNo : _type = pollTypes.Custom;
}
break;
default:
break;
}
return _type;
};
export default {
pollTypes,
validateInput,
getSplittedQuestionAndOptions,
removeEmptyLineSpaces,
isDefaultPoll,
};

View File

@ -0,0 +1,634 @@
import styled, { css, keyframes } from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import {
jumboPaddingY,
smPaddingX,
smPaddingY,
lgPaddingX,
borderRadius,
borderSize,
pollInputHeight,
pollSmMargin,
pollMdMargin,
mdPaddingX,
pollStatsElementWidth,
pollResultWidth,
borderSizeLarge,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorText,
colorBlueLight,
colorGray,
colorGrayLight,
colorGrayLighter,
colorGrayLightest,
colorDanger,
colorWarning,
colorHeading,
colorPrimary,
colorGrayDark,
colorWhite,
pollBlue,
pollStatsBorderColor,
} from '/imports/ui/stylesheets/styled-components/palette';
import { fontSizeBase, fontSizeSmall } from '/imports/ui/stylesheets/styled-components/typography';
const ToggleLabel = styled.span`
margin-right: ${smPaddingX};
[dir="rtl"] & {
margin: 0 0 0 ${smPaddingX};
}
`;
const PollOptionInput = styled.input`
margin-right: 1rem;
[dir="rtl"] & {
margin-right: 0;
margin-left: 1rem;
}
&:focus {
outline: none;
border-radius: ${borderSize};
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
}
width: 100%;
color: ${colorText};
-webkit-appearance: none;
padding: calc(${smPaddingY} * 2) ${smPaddingX};
border-radius: ${borderRadius};
font-size: ${fontSizeBase};
border: 1px solid ${colorGrayLighter};
box-shadow: 0 0 0 1px ${colorGrayLighter};
`;
// @ts-ignore - Button is a JS Component
const DeletePollOptionButton = styled(Button)`
font-size: ${fontSizeBase};
flex: none;
width: 40px;
position: relative;
& > i {
font-size: 150%;
}
`;
const ErrorSpacer = styled.div`
position: relative;
height: 1.25rem;
`;
const InputError = styled(ErrorSpacer)`
color: ${colorDanger};
font-size: ${fontSizeSmall};
`;
const Instructions = styled.div`
margin-bottom: ${lgPaddingX};
color: ${colorText};
`;
type PollQuestionAreaProps = {
hasError: boolean;
};
const PollQuestionArea = styled.textarea<PollQuestionAreaProps>`
resize: none;
&:focus {
outline: none;
border-radius: ${borderSize};
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
}
width: 100%;
color: ${colorText};
-webkit-appearance: none;
padding: calc(${smPaddingY} * 2) ${smPaddingX};
border-radius: ${borderRadius};
font-size: ${fontSizeBase};
border: 1px solid ${colorGrayLighter};
box-shadow: 0 0 0 1px ${colorGrayLighter};
${({ hasError }) => hasError && `
border-color: ${colorDanger};
box-shadow: 0 0 0 1px ${colorDanger};
`}
`;
const SectionHeading = styled.h4`
margin-top: 0;
font-weight: 600;
color: ${colorHeading};
`;
const ResponseType = styled.div`
display: flex;
justify-content: space-between;
flex-flow: wrap;
overflow-wrap: break-word;
position: relative;
width: 100%;
margin-bottom: ${lgPaddingX};
& > button {
position: relative;
width: 100%;
}
`;
// @ts-ignore - Button is a JS Component
const PollConfigButton = styled(Button)`
border: solid ${colorGrayLight} 1px;
min-height: ${pollInputHeight};
font-size: ${fontSizeBase};
white-space: pre-wrap;
width: 100%;
margin-bottom: 1rem;
& > span {
&:hover {
opacity: 1;
}
}
${({ selected }) => selected && `
background-color: ${colorGrayLightest};
font-size: ${fontSizeBase};
&:hover,
&:focus,
&:active {
background-color: ${colorGrayLightest} !important;
box-shadow: none !important;
}
`}
${({ small }) => small && `
width: 49% !important;
`}
${({ full }) => full && `
width: 100%;
`}
`;
const PollParagraph = styled.div`
color: ${colorText};
margin-bottom: 0.9rem;
`;
const PollCheckbox = styled.div`
display: inline-block;
margin-right: ${pollSmMargin};
margin-bottom: ${pollMdMargin};
`;
// @ts-ignore - Button is a JS Component
const AddItemButton = styled(Button)`
top: 1px;
position: relative;
display: block;
width: 100%;
text-align: left;
color: ${colorPrimary};
padding-left: 0;
padding-right: 0;
font-size: ${fontSizeBase};
white-space: pre-wrap;
&:hover {
& > span {
opacity: 1;
}
}
`;
const Row = styled.div`
display: flex;
flex-flow: wrap;
flex-grow: 1;
justify-content: space-between;
margin-top: 0.7rem;
margin-bottom: 0.7rem;
`;
const Warning = styled.div`
color: ${colorWarning};
font-size: ${fontSizeSmall};
`;
const CustomInputRow = styled.div`
display: flex;
flex-flow: nowrap;
flex-grow: 1;
justify-content: space-between;
`;
const Col = styled.div`
display: flex;
position: relative;
flex-flow: column;
flex-grow: 1;
&:last-child {
padding-right: 0;
padding-left: 1rem;
[dir="rtl"] & {
padding-right: 0.1rem;
padding-left: 0;
}
}
`;
const Toggle = styled.label`
margin-left: auto;
display: flex;
align-items: center;
`;
// @ts-ignore - Button is a JS Component
const StartPollBtn = styled(Button)`
position: relative;
width: 100%;
min-height: ${pollInputHeight};
margin-top: 1rem;
font-size: ${fontSizeBase};
overflow-wrap: break-word;
white-space: pre-wrap;
&:hover {
& > span {
opacity: 1;
}
}
`;
const NoSlidePanelContainer = styled.div`
color: ${colorGrayDark};
text-align: center;
`;
// @ts-ignore - Button is a JS Component
const PollButton = styled(Button)`
margin-top: ${smPaddingY};
margin-bottom: ${smPaddingY};
// background-color: ${colorWhite};
box-shadow: 0 0 0 1px ${colorPrimary};
color: ${colorWhite};
background-color: ${colorPrimary}
& > span {
color: ${colorGray};
}
& > span:hover {
color: ${colorWhite};
opacity: 1;
}
&:active {
background-color: ${colorWhite};
box-shadow: 0 0 0 1px ${pollBlue};
& > span {
color: ${pollBlue};
}
}
&:focus {
background-color: ${colorWhite};
box-shadow: 0 0 0 1px ${pollBlue};
& > span {
color: ${pollBlue};
}
}
&:nth-child(even) {
margin-right: inherit;
margin-left: ${smPaddingY};
[dir="rtl"] & {
margin-right: ${smPaddingY};
margin-left: inherit;
}
}
&:nth-child(odd) {
margin-right: 1rem;
margin-left: inherit;
[dir="rtl"] & {
margin-right: inherit;
margin-left: ${smPaddingY};
}
}
&:hover {
box-shadow: 0 0 0 1px ${colorWhite};
background-color: ${colorWhite};
color: ${pollBlue};
& > span {
color: ${pollBlue};
opacity: 1;
}
}
`;
const DragAndDropPollContainer = styled.div`
width: 200px !important;
height: 200px !important;
`;
const Question = styled.div`
margin-bottom: ${lgPaddingX};
`;
const OptionWrapper = styled.div`
display: flex;
justify-content: space-between;
`;
const ResponseArea = styled.div`
display: flex;
flex-flow: column wrap;
`;
const CustomInputHeading = styled(SectionHeading)`
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-bottom: ${jumboPaddingY};
`;
const CustomInputHeadingCol = styled(Col)`
overflow: hidden;
`;
const CustomInputToggleCol = styled(Col)`
flex-shrink: 0;
`;
const AnonymousHeading = styled(CustomInputHeading)``;
const AnonymousHeadingCol = styled(CustomInputHeadingCol)``;
const AnonymousToggleCol = styled(CustomInputToggleCol)``;
const AnonymousRow = styled(Row)`
flex-flow: nowrap;
width: 100%;
`;
const ResultLeft = styled.td`
padding: 0 .5rem 0 0;
border-bottom: 1px solid ${colorGrayLightest};
[dir="rtl"] & {
padding: 0 0 0 .5rem;
}
padding-bottom: .25rem;
word-break: break-all;
`;
const ResultRight = styled.td`
padding-bottom: .25rem;
word-break: break-all;
`;
const Main = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
`;
const Left = styled.div`
font-weight: bold;
max-width: ${pollResultWidth};
min-width: ${pollStatsElementWidth};
word-wrap: break-word;
flex: 6;
padding: ${smPaddingY};
margin-top: ${pollSmMargin};
margin-bottom: ${pollSmMargin};
color: ${colorText};
position: relative;
`;
const Center = styled.div`
position: relative;
flex: 3;
border-left: 1px solid ${colorGrayLighter};
border-right : none;
width: 100%;
height: 100%;
[dir="rtl"] & {
border-left: none;
border-right: 1px solid ${colorGrayLighter};
}
padding: ${smPaddingY};
margin-top: ${pollSmMargin};
margin-bottom: ${pollSmMargin};
color: ${colorText};
`;
const Right = styled.div`
text-align: right;
max-width: ${pollStatsElementWidth};
min-width: ${pollStatsElementWidth};
flex: 1;
[dir="rtl"] & {
text-align: left;
}
padding: ${smPaddingY};
margin-top: ${pollSmMargin};
margin-bottom: ${pollSmMargin};
color: ${colorText};
position: relative;
`;
const BarShade = styled.div`
background-color: ${colorGrayLighter};
height: 100%;
min-height: 100%;
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
`;
const BarVal = styled.div`
position: inherit;
`;
const Stats = styled.div`
margin-bottom: ${smPaddingX};
display: flex;
flex-direction: column;
border: 1px solid ${pollStatsBorderColor};
border-radius: ${borderSizeLarge};
padding: ${mdPaddingX};
& > div {
display: flex;
flex-direction: row;
& > div:nth-child(even) {
position: relative;
height: 75%;
width: 50%;
text-align: center;
}
}
`;
const Title = styled.span`
font-weight: bold;
word-break: break-all;
white-space: pre-wrap;
`;
const Status = styled.div`
margin-bottom: .5rem;
`;
const ellipsis = keyframes`
to {
width: 1.25em;
margin-right: 0;
margin-left: 0;
}
`;
interface ConnectingAnimationProps {
animations: boolean;
}
const ConnectingAnimation = styled.span<ConnectingAnimationProps>`
&:after {
overflow: hidden;
display: inline-block;
vertical-align: bottom;
content: "\\2026"; /* ascii code for the ellipsis character */
width: 0;
margin: 0 1.25em 0 0;
[dir="rtl"] & {
margin: 0 0 0 1.25em;
}
${({ animations }) => animations && css`
animation: ${ellipsis} steps(4, end) 900ms infinite;
`}
}
`;
const ButtonsActions = styled.div`
display: flex;
width: 100%;
justify-content: space-between;
`;
// @ts-ignore - Button is a JS Component
const PublishButton = styled(Button)`
width: 48%;
overflow-wrap: break-word;
white-space: pre-wrap;
`;
const CancelButton = styled(PublishButton)``;
// @ts-ignore - Button is a JS Component
const LiveResultButton = styled(Button)`
width: 100%;
margin-top: ${smPaddingY};
margin-bottom: ${smPaddingY};
font-size: ${fontSizeBase};
overflow-wrap: break-word;
white-space: pre-wrap;
`;
const Separator = styled.div`
display: flex;
flex: 1 1 100%;
height: 1px;
min-height: 1px;
background-color: ${colorGrayLightest};
padding: 0;
margin-top: 1rem;
margin-bottom: 1rem;
`;
const THeading = styled.th`
text-align: left;
[dir="rtl"] & {
text-align: right;
}
`;
export default {
ToggleLabel,
PollOptionInput,
DeletePollOptionButton,
ErrorSpacer,
InputError,
Instructions,
PollQuestionArea,
SectionHeading,
ResponseType,
PollConfigButton,
PollParagraph,
PollCheckbox,
AddItemButton,
Row,
Col,
Toggle,
StartPollBtn,
NoSlidePanelContainer,
PollButton,
DragAndDropPollContainer,
Warning,
CustomInputRow,
Question,
OptionWrapper,
ResponseArea,
CustomInputHeading,
CustomInputHeadingCol,
CustomInputToggleCol,
AnonymousHeading,
AnonymousHeadingCol,
AnonymousToggleCol,
AnonymousRow,
ResultLeft,
ResultRight,
Main,
Left,
Center,
Right,
BarShade,
BarVal,
Stats,
Title,
Status,
ConnectingAnimation,
ButtonsActions,
PublishButton,
CancelButton,
LiveResultButton,
Separator,
THeading,
};

View File

@ -1,16 +1,15 @@
import createUseSubscription from './createUseSubscription';
import useCreateUseSubscription from './createUseSubscription';
import MEETING_SUBSCRIPTION from '../graphql/queries/meetingSubscription';
import { Meeting } from '../../Types/meeting';
const useMeetingSubscription = createUseSubscription<Meeting>(MEETING_SUBSCRIPTION);
const useMeetingSubscription = useCreateUseSubscription<Meeting>(MEETING_SUBSCRIPTION, {}, true);
export const useMeeting = (fn: (c: Partial<Meeting>) => Partial<Meeting>) => {
const response = useMeetingSubscription(fn);
const returnObject = {
return {
...response,
data: response.data?.[0],
};
return returnObject;
};
export default useMeeting;

View File

@ -264,9 +264,7 @@ const pollAnswerOptionDesc = 'div[data-test="optionsAnswers"]';
exports.firstPollAnswerDescOption = `${pollAnswerOptionDesc} input:nth-child(1)`;
exports.secondPollAnswerDescOption = `${pollAnswerOptionDesc}>>nth=1`;
exports.submitAnswersMultiple = 'button[data-test="submitAnswersMultiple"]';
exports.numberVotes = 'div[data-test="numberOfVotes"]';
exports.answer1 = 'div[data-test="numberOfVotes"]>>nth=0';
exports.answer2 = 'div[data-test="numberOfVotes"]>>nth=1';
exports.userVoteLiveResult = 'td[data-test="userVoteLiveResult"]';
exports.errorNoValueInput = 'div[data-test="errorNoValueInput"]';
exports.smartSlides1 = 'smartSlidesPresentation.pdf';
exports.responsePollQuestion = 'div[data-test="pollQuestion"]';
@ -278,7 +276,7 @@ exports.closePollingBtn = 'button[data-test="closePolling"]';
exports.yesNoOption = 'button[data-test="yesNoQuickPoll"]';
exports.yesNoAbstentionOption = 'li[role="menuitem"]>>nth=1';
exports.pollAnswerOptionE = 'button[data-test="pollAnswerOption"]>>nth=4';
exports.answerE = 'div[data-test="numberOfVotes"]>>nth=4';
// Presentation
exports.currentSlideImg = '[id="whiteboard-element"] [class="tl-image"]';
exports.uploadPresentationFileName = 'uploadTest.png';

View File

@ -60,7 +60,7 @@ class LearningDashboard extends MultiUsers {
await this.modPage.waitAndClick(e.startPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasText(e.numberVotes, '1');
await this.modPage.hasText(e.userVoteLiveResult, 'True');
await this.modPage.waitAndClick(e.cancelPollBtn);
//ABCD
@ -69,7 +69,7 @@ class LearningDashboard extends MultiUsers {
await this.modPage.waitAndClick(e.pollLetterAlternatives);
await this.modPage.waitAndClick(e.startPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasText(e.numberVotes, '1');
await this.modPage.hasText(e.userVoteLiveResult, 'A');
await this.modPage.waitAndClick(e.cancelPollBtn);
//Yes/No/Abstention
@ -78,7 +78,7 @@ class LearningDashboard extends MultiUsers {
await this.modPage.waitAndClick(e.pollYesNoAbstentionBtn);
await this.modPage.waitAndClick(e.startPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasText(e.numberVotes, '1');
await this.modPage.hasText(e.userVoteLiveResult, 'Yes');
await this.modPage.waitAndClick(e.cancelPollBtn);
//User Response
@ -89,7 +89,7 @@ class LearningDashboard extends MultiUsers {
await this.userPage.waitForSelector(e.pollingContainer);
await this.userPage.type(e.pollAnswerOptionInput, e.answerMessage);
await this.userPage.waitAndClick(e.pollSubmitAnswer);
await this.modPage.hasText(e.numberVotes, '1');
await this.modPage.hasText(e.userVoteLiveResult, e.answerMessage);
await this.modPage.waitAndClick(e.cancelPollBtn);
//Checks

View File

@ -17,7 +17,9 @@ class Polling extends MultiUsers {
async createPoll() {
await util.startPoll(this.modPage);
await this.modPage.hasElement(e.pollMenuButton);
await this.modPage.hasElement(e.publishPollingLabel);
await this.modPage.hasElement(e.cancelPollBtn);
await this.userPage.hasElement(e.pollingContainer);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
}
@ -58,16 +60,13 @@ class Polling extends MultiUsers {
await this.userPage.type(e.pollAnswerOptionInput, e.answerMessage);
await this.userPage.waitAndClick(e.pollSubmitAnswer);
await this.modPage.hasText(e.receivedAnswer, e.answerMessage);
await this.modPage.hasText(e.userVoteLiveResult, e.answerMessage);
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.waitForSelector(e.restartPoll);
await this.modPage.wasRemoved(e.pollingContainer);
await this.modPage.hasElement(e.wbDrawnRectangle, ELEMENT_WAIT_LONGER_TIME);
await this.userPage.hasElement(e.wbDrawnRectangle);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
}
async stopPoll() {
@ -110,20 +109,20 @@ class Polling extends MultiUsers {
await this.checkLastOptionText();
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.waitAndClick(e.actions);
await this.modPage.waitAndClick(e.managePresentations);
await this.modPage.waitAndClick(e.removePresentation);
await this.modPage.waitAndClick(e.confirmManagePresentation);
}
async notAbleStartNewPollWithoutPresentation() {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.waitAndClick(e.actions);
await this.modPage.waitAndClick(e.managePresentations);
await this.modPage.waitAndClick(e.removePresentation);
await this.modPage.waitAndClick(e.confirmManagePresentation);
const allRemovePresentationBtn = await this.modPage.getLocator(e.removePresentation).all();
// reversing the order of clicking is needed to avoid failure as the tooltip shows in front of the below button
const reversedRemovePresentationButtons = allRemovePresentationBtn.reverse();
for (const removeBtn of reversedRemovePresentationButtons) {
await removeBtn.click({ timeout: ELEMENT_WAIT_TIME });
}
await this.modPage.waitAndClick(e.confirmManagePresentation);
await this.modPage.waitAndClick(e.actions);
await this.modPage.waitAndClick(e.polling);
await this.modPage.hasElement(e.noPresentation);
@ -147,7 +146,7 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasText(e.currentPollQuestion, /Test/);
await this.modPage.hasText(e.answer1, '1');
await this.modPage.hasText(e.userVoteLiveResult, '1');
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
@ -175,8 +174,8 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.secondPollAnswerOptionBtn);
await this.userPage.waitAndClickElement(e.submitAnswersMultiple);
await this.modPage.hasText(e.answer1, '1');
await this.modPage.hasText(e.answer2, '1');
await this.modPage.hasText(e.userVoteLiveResult, '1');
await this.modPage.hasText(e.userVoteLiveResult, '2');
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
@ -193,11 +192,10 @@ class Polling extends MultiUsers {
await this.userPage.type(e.pollAnswerOptionInput, 'test');
await this.userPage.waitAndClick(e.pollSubmitAnswer);
await this.userPage.wasRemoved(e.pollingContainer, ELEMENT_WAIT_LONGER_TIME);
await this.modPage.hasText(e.receivedAnswer, 'test');
await this.modPage.hasText(e.userVoteLiveResult, 'test');
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
await this.modPage.wasRemoved(e.pollingContainer);
// Multiple Choices
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
@ -206,23 +204,21 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.firstPollAnswerDescOption);
await this.userPage.waitAndClick(e.secondPollAnswerDescOption);
await this.userPage.waitAndClick(e.submitAnswersMultiple);
await this.modPage.hasText(e.answer1, '1');
await this.modPage.hasText(e.answer2, '1');
await this.modPage.hasText(e.userVoteLiveResult, 'A) 2222');
await this.modPage.hasText(e.userVoteLiveResult, 'B) 3333');
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
await this.modPage.wasRemoved(e.pollingContainer);
// One option answer
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
await skipSlide(this.modPage);
await this.modPage.waitAndClick(e.quickPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionE);
await this.modPage.hasText(e.answerE, '1');
await this.modPage.hasText(e.userVoteLiveResult, 'E) 22222');
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
await this.modPage.wasRemoved(e.pollingContainer);
// Yes/No/Abstention
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
@ -230,24 +226,21 @@ class Polling extends MultiUsers {
await this.modPage.waitAndClick(e.yesNoOption);
await this.modPage.waitAndClick(e.yesNoAbstentionOption)
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasText(e.answer1, '1');
await this.modPage.hasText(e.userVoteLiveResult, 'Yes');
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
await this.modPage.wasRemoved(e.pollingContainer);
// True/False
await sleep(500); // avoid error when the tooltip is in front of the button due to layout shift
await skipSlide(this.modPage);
await this.modPage.waitAndClick(e.quickPoll);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasText(e.answer1, '1');
await this.modPage.hasText(e.userVoteLiveResult, 'True');
await this.modPage.waitAndClick(e.publishPollingLabel);
await this.modPage.hasElementDisabled(e.nextSlide);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
await this.modPage.wasRemoved(e.pollingContainer);
}
async pollResultsOnChat() {
@ -273,24 +266,29 @@ class Polling extends MultiUsers {
await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME);
await util.startPoll(this.modPage);
const wbDrawnRectangleLocator = await this.modPage.getLocator(e.wbDrawnRectangle);
const initialWbDrawnRectangleCount = await wbDrawnRectangleLocator.count();
await this.modPage.hasElementDisabled(e.publishPollingLabel);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await this.modPage.hasElement(e.publishPollingLabel);
await this.modPage.waitAndClick(e.publishPollingLabel);
await expect(wbDrawnRectangleLocator).toHaveCount(initialWbDrawnRectangleCount + 1);
const wbDrawnRectangleLocator = await this.modPage.getLocator(e.wbDrawnRectangle).last();
await expect(wbDrawnRectangleLocator).toBeVisible({ timeout: ELEMENT_WAIT_TIME});
const lastWbDrawnRectangleLocator = await wbDrawnRectangleLocator.last();
await expect(lastWbDrawnRectangleLocator).toBeVisible({ timeout: ELEMENT_WAIT_TIME});
const modWbLocator = this.modPage.getLocator(e.whiteboard);
const wbBox = await modWbLocator.boundingBox();
await wbDrawnRectangleLocator.dblclick();
// poll results should be editable by the presenter
await lastWbDrawnRectangleLocator.dblclick({ timeout: ELEMENT_WAIT_TIME });
await this.modPage.page.mouse.down();
await this.modPage.page.mouse.move(wbBox.x + 0.7 * wbBox.width, wbBox.y + 0.7 * wbBox.height);
await this.modPage.page.mouse.up();
await wbDrawnRectangleLocator.dblclick();
await lastWbDrawnRectangleLocator.dblclick({ timeout: ELEMENT_WAIT_TIME });
await this.modPage.page.keyboard.type('test');
await expect(wbDrawnRectangleLocator).toContainText('test');
await expect(lastWbDrawnRectangleLocator).toContainText('test');
// user turns to presenter to edit the poll results
await this.modPage.waitAndClick(e.userListItem);
@ -300,10 +298,10 @@ class Polling extends MultiUsers {
await this.userPage.waitAndClick(e.resetZoomButton);
const wbDrawnRectangleUserLocator = await this.userPage.getLocator(e.wbDrawnRectangle).last();
await wbDrawnRectangleUserLocator.dblclick();
await wbDrawnRectangleUserLocator.dblclick({ timeout: ELEMENT_WAIT_TIME });
await this.userPage.page.keyboard.type('testUser');
await expect(wbDrawnRectangleUserLocator).toContainText('testUser');
await this.modPage.waitAndClick(e.currentUser);
await this.modPage.waitAndClick(e.takePresenter);
await this.userPage.waitAndClick(e.hidePublicChat);
@ -311,17 +309,12 @@ class Polling extends MultiUsers {
async pollResultsInDifferentPresentation() {
await waitAndClearDefaultPresentationNotification(this.modPage);
await uploadSinglePresentation(this.modPage, e.questionSlideFileName);
await util.startPoll(this.modPage);
await this.userPage.waitAndClick(e.pollAnswerOptionBtn);
await uploadSinglePresentation(this.modPage, e.questionSlideFileName);
await this.modPage.waitAndClick(e.publishPollingLabel);
// Check poll results
await this.modPage.hasElement(e.wbDrawnRectangle);
await this.modPage.waitAndClick(e.closePollingBtn);
await this.modPage.wasRemoved(e.closePollingBtn);
}
async startNewPoll() {

View File

@ -24,7 +24,7 @@ test.describe('Polling', async () => {
await polling.quickPoll();
});
test('Create poll with user response @ci @flaky', async () => {
test('Create poll with user response @ci', async () => {
await polling.pollUserResponse();
});
@ -61,7 +61,8 @@ test.describe('Polling', async () => {
await polling.pollResultsOnWhiteboard();
});
test('Poll results in a different presentation', async () => {
test('Poll results in a different presentation', async ({}, testInfo) => {
test.fixme(!testInfo.config.fullyParallel, 'Currently only works in parallel mode. Poll results not being displayed in the presentation');
await polling.pollResultsInDifferentPresentation();
});
});

View File

@ -24,7 +24,7 @@ async function getCurrentPresentationHeight(locator) {
async function uploadSinglePresentation(test, fileName, uploadTimeout = UPLOAD_PDF_WAIT_TIME) {
const firstSlideSrc = await test.page.evaluate(selector => document.querySelector(selector)
.style
?.style
.backgroundImage
.split('"')[1],
[e.currentSlideImg]);