Merge remote-tracking branch 'upstream/v3.0.x-release' into migrate-breakout-invitation
This commit is contained in:
commit
901d2a7fbb
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
}()
|
||||
|
@ -61,3 +61,4 @@ select_permissions:
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-MeetingId
|
||||
allow_aggregations: true
|
||||
|
@ -42,3 +42,4 @@ select_permissions:
|
||||
filter:
|
||||
meetingId:
|
||||
_eq: X-Hasura-PresenterInMeeting
|
||||
allow_aggregations: true
|
||||
|
@ -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)}
|
||||
|
@ -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(() => {
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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> </Styled.ErrorSpacer>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default PollInputs;
|
@ -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> </Styled.ErrorSpacer>
|
||||
)}
|
||||
{hasWarning ? (
|
||||
<Styled.Warning>{warning}</Styled.Warning>
|
||||
) : (
|
||||
<Styled.ErrorSpacer> </Styled.ErrorSpacer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PollQuestionArea;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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;
|
||||
|
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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]);
|
||||
|
Loading…
Reference in New Issue
Block a user