Merge pull request #19989 from JoVictorNunes/remove-old-polls

cleanup: remove old polls
This commit is contained in:
Ramón Souza 2024-04-12 10:21:52 -03:00 committed by GitHub
commit f2f255d37f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 218 additions and 3732 deletions

View File

@ -4,7 +4,6 @@ import AbstractCollection from '/imports/ui/services/LocalCollectionSynchronizer
import PresentationUploadToken from '/imports/api/presentation-upload-token'; import PresentationUploadToken from '/imports/api/presentation-upload-token';
import Screenshare from '/imports/api/screenshare'; import Screenshare from '/imports/api/screenshare';
import UserInfos from '/imports/api/users-infos'; import UserInfos from '/imports/api/users-infos';
import Polls, { CurrentPoll } from '/imports/api/polls';
import UserSettings from '/imports/api/users-settings'; import UserSettings from '/imports/api/users-settings';
import VideoStreams from '/imports/api/video-streams'; import VideoStreams from '/imports/api/video-streams';
import VoiceUsers from '/imports/api/voice-users'; import VoiceUsers from '/imports/api/voice-users';
@ -21,8 +20,6 @@ import Users from '/imports/api/users';
// Custom Publishers // Custom Publishers
export const localCollectionRegistry = { export const localCollectionRegistry = {
localCurrentPollSync: new AbstractCollection(CurrentPoll, CurrentPoll),
localPollsSync: new AbstractCollection(Polls, Polls),
localPresentationUploadTokenSync: new AbstractCollection( localPresentationUploadTokenSync: new AbstractCollection(
PresentationUploadToken, PresentationUploadToken,
PresentationUploadToken, PresentationUploadToken,

View File

@ -6,7 +6,6 @@ import { removeExternalVideoStreamer } from '/imports/api/external-videos/server
import clearUsers from '/imports/api/users/server/modifiers/clearUsers'; import clearUsers from '/imports/api/users/server/modifiers/clearUsers';
import clearUsersSettings from '/imports/api/users-settings/server/modifiers/clearUsersSettings'; import clearUsersSettings from '/imports/api/users-settings/server/modifiers/clearUsersSettings';
import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts'; import clearBreakouts from '/imports/api/breakouts/server/modifiers/clearBreakouts';
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions'; import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions';
import clearPads from '/imports/api/pads/server/modifiers/clearPads'; import clearPads from '/imports/api/pads/server/modifiers/clearPads';
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers'; import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
@ -33,7 +32,6 @@ export default async function meetingHasEnded(meetingId) {
clearCaptions(meetingId), clearCaptions(meetingId),
clearPads(meetingId), clearPads(meetingId),
clearBreakouts(meetingId), clearBreakouts(meetingId),
clearPolls(meetingId),
clearUsers(meetingId), clearUsers(meetingId),
clearUsersSettings(meetingId), clearUsersSettings(meetingId),
clearVoiceUsers(meetingId), clearVoiceUsers(meetingId),

View File

@ -1,17 +0,0 @@
import { Meteor } from 'meteor/meteor';
const collectionOptions = Meteor.isClient ? {
connection: null,
} : {};
const Polls = new Mongo.Collection('polls', collectionOptions);
export const CurrentPoll = new Mongo.Collection('current-poll', { connection: null });
if (Meteor.isServer) {
// We can have just one active poll per meeting
// makes no sense to index it by anything other than meetingId
Polls.createIndexAsync({ meetingId: 1 });
}
export default Polls;

View File

@ -1,103 +0,0 @@
import { Meteor } from 'meteor/meteor';
import Polls from '/imports/api/polls';
import { expect } from 'chai';
// Modifiers
import clearPolls from './server/modifiers/clearPolls';
import removePoll from './server/modifiers/removePoll';
import addPoll from './server/modifiers/addPoll';
import updateVotes from './server/modifiers/updateVotes';
// Handlers
import pollStarted from './server/handlers/pollStarted';
import pollStopped from './server/handlers/pollStopped';
// mock test data
const _id = 'sJt6JaJMsTgy64TZG';
const meetingId = '183f0bf3a0982a127bdb8161e0c44eb696b3e75c-1623285094106';
const requester = 'w_iotqesmfrtqj';
const pollType = 'TF';
const id = 'd2d9a672040fbde2a47a10bf6c37b6a4b5ae187f-1623285094108/1/1623285145173';
const answers = [{ id: 0, key: 'True' }, { id: 1, key: 'False' }];
const users = [];
const questionText = '';
const pollObj = {
_id,
meetingId,
requester,
pollType,
answers,
id,
users,
questionText,
};
Polls.insert(pollObj);
const poll = Polls.findOne(pollObj);
if (Meteor.isServer) {
describe('Polls Collection', () => {
describe('Modifiers :', () => {
it('Validate (#_id, #meetingId, #requester, #pollType, #users, #answers)', () => {
expect(poll?._id).to.be.a('string').equal(_id);
expect(poll?.meetingId).to.be.a('string').equal(meetingId);
expect(poll?.requester).to.be.a('string').equal(requester);
expect(poll?.pollType).to.be.a('string').equal(pollType);
expect(poll?.users).to.be.an('array');
expect(poll?.answers).to.be.an('array');
});
it('addPoll(): Should have added a poll', () => {
addPoll(meetingId, requester, { id, answers }, pollType, questionText);
expect(Polls.findOne({ id, meetingId })?.id).to.be.a('string').equal(pollObj.id);
});
it('updateVotes(): Should update vote for a poll', () => {
answers[0].numVotes = 1;
answers[1].numVotes = 0;
updateVotes({
id,
answers,
numResponders: 1,
numRespondents: 1,
}, meetingId);
expect(Polls.findOne({ meetingId, id })?.answers[0]?.numVotes).to.be.a('number').equal(1);
expect(Polls.findOne({ meetingId, id })?.answers[1]?.numVotes).to.be.a('number').equal(0);
});
it('removePoll(): Should have removed specified poll', () => {
removePoll(meetingId, id);
expect(Polls.findOne({ meetingId, id })).to.be.an('undefined');
});
it('clearPolls(): Should have cleared all polls', () => {
Polls.insert(pollObj);
clearPolls();
expect(Polls.findOne({ meetingId })).to.be.an('undefined');
});
});
describe('Handlers :', () => {
it('pollStarted(): should add a poll and reset publishedPoll flag', () => {
delete answers[0].numVotes;
delete answers[1].numVotes;
pollStarted({
body: {
userId: requester,
poll: { id, answers },
pollType,
question: '',
},
}, meetingId);
expect(Polls.findOne({ meetingId, id })?.id).to.be.a('string').equal(id);
});
it('pollStopped(): Should have removed poll', () => {
pollStopped({ body: { poll: { pollId: id } } }, meetingId);
expect(Polls.findOne({ meetingId, id })?.id).to.be.an('undefined');
});
});
});
}

View File

@ -1,14 +0,0 @@
import RedisPubSub from '/imports/startup/server/redis';
import handlePollStarted from './handlers/pollStarted';
import handlePollStopped from './handlers/pollStopped';
import handlePollPublished from './handlers/pollPublished';
import handleUserVoted from './handlers/userVoted';
import handleUserResponded from './handlers/userResponded';
import handleUserTypedResponse from './handlers/userTypedResponse';
RedisPubSub.on('PollShowResultEvtMsg', handlePollPublished);
RedisPubSub.on('PollStartedEvtMsg', handlePollStarted);
RedisPubSub.on('PollStoppedEvtMsg', handlePollStopped);
RedisPubSub.on('PollUpdatedEvtMsg', handleUserVoted);
RedisPubSub.on('UserRespondedToPollRespMsg', handleUserResponded);
RedisPubSub.on('UserRespondedToTypedPollRespMsg', handleUserTypedResponse);

View File

@ -1,11 +0,0 @@
import { check } from 'meteor/check';
import setPublishedPoll from '../../../meetings/server/modifiers/setPublishedPoll';
export default function pollPublished({ body }, meetingId) {
const { pollId } = body;
check(meetingId, String);
check(pollId, String);
setPublishedPoll(meetingId, true);
}

View File

@ -1,22 +0,0 @@
import { check } from 'meteor/check';
import addPoll from '../modifiers/addPoll';
import setPublishedPoll from '../../../meetings/server/modifiers/setPublishedPoll';
export default async function pollStarted({ body }, meetingId) {
const {
userId, poll, pollType, secretPoll, question,
} = body;
check(meetingId, String);
check(userId, String);
check(poll, Object);
check(pollType, String);
check(secretPoll, Boolean);
check(question, String);
setPublishedPoll(meetingId, false);
const result = await addPoll(meetingId, userId, poll, pollType, secretPoll, question);
return result;
}

View File

@ -1,23 +0,0 @@
import { check } from 'meteor/check';
import removePoll from '../modifiers/removePoll';
import clearPolls from '../modifiers/clearPolls';
export default async function pollStopped({ body }, meetingId) {
const { poll } = body;
check(meetingId, String);
if (poll) {
const { pollId } = poll;
check(pollId, String);
const result = await removePoll(meetingId, pollId);
return result;
}
const result = await clearPolls(meetingId);
return result;
}

View File

@ -1,34 +0,0 @@
import { check } from 'meteor/check';
import Polls from '/imports/api/polls';
import Logger from '/imports/startup/server/logger';
export default async function userResponded({ body }) {
const { pollId, userId, answerIds } = body;
check(pollId, String);
check(userId, String);
check(answerIds, Array);
const selector = {
id: pollId,
};
const modifier = {
$pull: {
users: userId,
},
$push: {
responses: { userId, answerIds },
},
};
try {
const numberAffected = await Polls.updateAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Updating Poll response (userId: ${userId}, response: ${JSON.stringify(answerIds)}, pollId: ${pollId})`);
}
} catch (err) {
Logger.error(`Updating Poll responses: ${err}`);
}
}

View File

@ -1,34 +0,0 @@
import { check } from 'meteor/check';
import Polls from '/imports/api/polls';
import RedisPubSub from '/imports/startup/server/redis';
export default async function userTypedResponse({ header, body }) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'RespondToPollReqMsg';
const { pollId, userId, answer } = body;
const { meetingId } = header;
check(pollId, String);
check(meetingId, String);
check(userId, String);
check(answer, String);
const poll = await Polls.findOneAsync({ meetingId, id: pollId });
let answerId = 0;
poll.answers.forEach((a) => {
const { id, key } = a;
if (key === answer) answerId = id;
});
const payload = {
requesterId: userId,
pollId,
questionId: 0,
answerIds: [answerId],
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, userId, payload);
}

View File

@ -1,26 +0,0 @@
import { check } from 'meteor/check';
import updateVotes from '../modifiers/updateVotes';
export default async function userVoted({ body }, meetingId) {
const { poll } = body;
check(meetingId, String);
check(poll, {
id: String,
questionType: String,
questionText: String,
answers: [
{
id: Number,
key: String,
numVotes: Number,
},
],
numRespondents: Number,
numResponders: Number,
});
const result = await updateVotes(poll, meetingId);
return result;
}

View File

@ -1,2 +0,0 @@
import './eventHandlers';
import './publishers';

View File

@ -1,56 +0,0 @@
import Users from '/imports/api/users';
import Polls from '/imports/api/polls';
import Logger from '/imports/startup/server/logger';
import flat from 'flat';
import { check } from 'meteor/check';
export default async function addPoll(meetingId, requesterId, poll, pollType, secretPoll, question = '') {
check(requesterId, String);
check(meetingId, String);
check(poll, {
id: String,
answers: [
{
id: Number,
key: String,
},
],
isMultipleResponse: Boolean,
});
const userSelector = {
meetingId,
userId: { $ne: requesterId },
clientType: { $ne: 'dial-in-user' },
};
const users = await Users.find(userSelector, { fields: { userId: 1 } })
.fetchAsync();
const userIds = users.map(user => user.userId);
const selector = {
meetingId,
requester: requesterId,
id: poll.id,
};
const modifier = Object.assign(
{ meetingId },
{ requester: requesterId },
{ users: userIds },
{ question, pollType, secretPoll },
flat(poll, { safe: true }),
);
try {
const { insertedId } = await Polls.upsertAsync(selector, modifier);
if (insertedId) {
Logger.info(`Added Poll id=${poll.id}`);
} else {
Logger.info(`Upserted Poll id=${poll.id}`);
}
} catch (err) {
Logger.error(`Adding Poll to collection: ${poll.id}`);
}
}

View File

@ -1,26 +0,0 @@
import Polls from '/imports/api/polls';
import Logger from '/imports/startup/server/logger';
export default async function clearPolls(meetingId) {
if (meetingId) {
try {
const numberAffected = await Polls.removeAsync({ meetingId });
if (numberAffected) {
Logger.info(`Cleared Polls (${meetingId})`);
}
} catch (err) {
Logger.info(`Error on clearing Polls (${meetingId}). ${err}`);
}
} else {
try {
const numberAffected = await Polls.removeAsync({});
if (numberAffected) {
Logger.info('Cleared Polls (all)');
}
} catch (err) {
Logger.info(`Error on clearing Polls (all). ${err}`);
}
}
}

View File

@ -1,23 +0,0 @@
import Polls from '/imports/api/polls';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
export default async function removePoll(meetingId, id) {
check(meetingId, String);
check(id, String);
const selector = {
meetingId,
id,
};
try {
const numberAffected = await Polls.removeAsync(selector);
if (numberAffected) {
Logger.info(`Removed Poll id=${id}`);
}
} catch (err) {
Logger.error(`Removing Poll from collection: ${err}`);
}
}

View File

@ -1,41 +0,0 @@
import Polls from '/imports/api/polls';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import flat from 'flat';
export default async function updateVotes(poll, meetingId) {
check(meetingId, String);
check(poll, Object);
const {
id,
answers,
numResponders,
numRespondents,
} = poll;
check(id, String);
check(answers, Array);
check(numResponders, Number);
check(numRespondents, Number);
const selector = {
meetingId,
id,
};
const modifier = {
$set: flat(poll, { safe: true }),
};
try {
const numberAffected = await Polls.updateAsync(selector, modifier);
if (numberAffected) {
Logger.info(`Updating Polls collection vote (meetingId: ${meetingId}, pollId: ${id}!)`);
}
} catch (err) {
Logger.error(`Updating Polls collection vote: ${err}`);
}
}

View File

@ -1,146 +0,0 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
import Polls from '/imports/api/polls';
import AuthTokenValidation, {
ValidationStates,
} from '/imports/api/auth-token-validation';
import { DDPServer } from 'meteor/ddp-server';
import { publicationSafeGuard } from '/imports/api/common/server/helpers';
Meteor.server.setPublicationStrategy('polls', DDPServer.publicationStrategies.NO_MERGE);
async function currentPoll(secretPoll) {
check(secretPoll, Boolean);
const tokenValidation = await AuthTokenValidation.findOneAsync({
connectionId: this.connection.id,
});
if (
!tokenValidation
|| tokenValidation.validationStatus !== ValidationStates.VALIDATED
) {
Logger.warn(
`Publishing Polls was requested by unauth connection ${this.connection.id}`,
);
return Polls.find({ meetingId: '' });
}
const { meetingId, userId } = tokenValidation;
const User = await Users.findOneAsync({ userId, meetingId },
{ fields: { role: 1, presenter: 1 } });
if (!!User && User.presenter) {
Logger.debug('Publishing Polls', { meetingId, userId });
const selector = {
meetingId,
requester: userId,
};
const options = { fields: {} };
const hasPoll = await Polls.findOneAsync(selector);
if ((hasPoll && hasPoll.secretPoll) || secretPoll) {
options.fields.responses = 0;
selector.secretPoll = true;
} else {
selector.secretPoll = false;
}
Mongo.Collection._publishCursor(Polls.find(selector, options), this, 'current-poll');
return this.ready();
}
Logger.warn(
'Publishing current-poll was requested by non-presenter connection',
{ meetingId, userId, connectionId: this.connection.id },
);
Mongo.Collection._publishCursor(Polls.find({ meetingId: '' }), this, 'current-poll');
return this.ready();
}
function publishCurrentPoll(...args) {
const boundPolls = currentPoll.bind(this);
return boundPolls(...args);
}
Meteor.publish('current-poll', publishCurrentPoll);
async function polls() {
const tokenValidation = await AuthTokenValidation.findOneAsync({
connectionId: this.connection.id,
});
if (
!tokenValidation
|| tokenValidation.validationStatus !== ValidationStates.VALIDATED
) {
Logger.warn(
`Publishing Polls was requested by unauth connection ${this.connection.id}`,
);
return Polls.find({ meetingId: '' });
}
const options = {
fields: {
'answers.numVotes': 0,
responses: 0,
},
};
const noKeyOptions = {
fields: {
'answers.numVotes': 0,
'answers.key': 0,
responses: 0,
},
};
const { meetingId, userId } = tokenValidation;
const User = await Users.findOneAsync({ userId, meetingId },
{ fields: { role: 1, presenter: 1 } });
Logger.debug('Publishing polls', { meetingId, userId });
const selector = {
meetingId,
users: userId,
};
if (User) {
const poll = await Polls.findOneAsync(selector, noKeyOptions);
if (User.presenter || poll?.pollType !== 'R-') {
// Monitor this publication and stop it when user is not a presenter anymore
// or poll type has changed
const comparisonFunc = async () => {
const user = await Users.findOneAsync({ userId, meetingId },
{ fields: { role: 1, userId: 1 } });
const currentPoll = await Polls.findOneAsync(selector, noKeyOptions);
const condition = user.presenter || currentPoll?.pollType !== 'R-';
if (!condition) {
Logger.info(`conditions aren't filled anymore in publication ${this._name}:
user.presenter || currentPoll?.pollType !== 'R-' :${condition}, user.presenter: ${user.presenter} pollType: ${currentPoll?.pollType}`);
}
return condition;
};
publicationSafeGuard(comparisonFunc, this);
return Polls.find(selector, options);
}
}
return Polls.find(selector, noKeyOptions);
}
function publish(...args) {
const boundPolls = polls.bind(this);
return boundPolls(...args);
}
Meteor.publish('polls', publish);

File diff suppressed because it is too large Load Diff

View File

@ -5,17 +5,17 @@ import { Meteor } from 'meteor/meteor';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser'; import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import Header from '/imports/ui/components/common/control-header/component'; import Header from '/imports/ui/components/common/control-header/component';
import { useMutation, useSubscription } from '@apollo/client'; import { useMutation, useSubscription } from '@apollo/client';
import { Input } from '../../layout/layoutTypes'; import { Input } from '../layout/layoutTypes';
import { layoutDispatch, layoutSelectInput } from '../../layout/context'; import { layoutDispatch, layoutSelectInput } from '../layout/context';
import { addAlert } from '../../screenreader-alert/service'; import { addAlert } from '../screenreader-alert/service';
import { PANELS, ACTIONS } from '../../layout/enums'; import { PANELS, ACTIONS } from '../layout/enums';
import useMeeting from '/imports/ui/core/hooks/useMeeting'; import useMeeting from '/imports/ui/core/hooks/useMeeting';
import { POLL_CANCEL } from './mutation'; import { POLL_CANCEL } from './mutations';
import { GetHasCurrentPresentationResponse, getHasCurrentPresentation } from './queries'; import { GetHasCurrentPresentationResponse, getHasCurrentPresentation } from './queries';
import EmptySlideArea from './components/EmptySlideArea'; import EmptySlideArea from './components/EmptySlideArea';
import { getSplittedQuestionAndOptions, pollTypes, validateInput } from './service'; import { getSplittedQuestionAndOptions, pollTypes, validateInput } from './service';
import Toggle from '/imports/ui/components/common/switch/component'; import Toggle from '/imports/ui/components/common/switch/component';
import Styled from '../styles'; import Styled from './styles';
import ResponseChoices from './components/ResponseChoices'; import ResponseChoices from './components/ResponseChoices';
import ResponseTypes from './components/ResponseTypes'; import ResponseTypes from './components/ResponseTypes';
import PollQuestionArea from './components/PollQuestionArea'; import PollQuestionArea from './components/PollQuestionArea';

View File

@ -0,0 +1,107 @@
import React, { useEffect, useRef, useState } from 'react';
import Styled from '../styles';
interface DragAndDropPros {
MAX_INPUT_CHARS: number;
handlePollValuesText: (value: string) => void;
[key: string]: unknown;
}
const DragAndDrop: React.FC<DragAndDropPros> = (props) => {
const { MAX_INPUT_CHARS, handlePollValuesText } = props;
const [drag, setDrag] = useState(false);
const [pollValueText, setPollText] = useState('');
const dropRef = useRef<HTMLTextAreaElement | null>(null);
const dragCounter = useRef(0);
useEffect(() => {
const handleDrag = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDragIn = (e: DragEvent) => {
handleDrag(e);
dragCounter.current += 1;
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
setDrag(true);
}
};
const handleDragOut = (e: DragEvent) => {
handleDrag(e);
dragCounter.current -= 1;
if (dragCounter.current > 0) return;
setDrag(false);
};
const handleDrop = (e: DragEvent) => {
handleDrag(e);
setDrag(false);
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
setPollValuesFromFile(e.dataTransfer.files[0]);
dragCounter.current = 0;
}
};
const div = dropRef.current;
if (!div) return undefined;
div.addEventListener('dragenter', handleDragIn);
div.addEventListener('dragleave', handleDragOut);
div.addEventListener('dragover', handleDrag);
div.addEventListener('drop', handleDrop);
return () => {
div.removeEventListener('dragenter', handleDragIn);
div.removeEventListener('dragleave', handleDragOut);
div.removeEventListener('dragover', handleDrag);
div.removeEventListener('drop', handleDrop);
};
}, []);
const setPollValues = () => {
if (pollValueText) {
handlePollValuesText(pollValueText);
}
};
const setPollValuesFromFile = (file: File) => {
const reader = new FileReader();
reader.onload = async (e) => {
const result = e.target?.result;
if (!result) return;
const text = typeof result === 'string' ? result : String(result);
setPollValueText(text);
setPollValues();
};
reader.readAsText(file);
};
const setPollValueText = (pollText: string) => {
const arr = pollText.split('\n');
const text = arr.map((line) => (line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line)).join('\n');
setPollText(text);
};
const getCleanProps = () => {
const cleanProps = { ...props };
const propsToDelete = ['MAX_INPUT_CHARS', 'handlePollValuesText'] as const;
propsToDelete.forEach((prop) => {
delete cleanProps[prop as keyof typeof cleanProps];
});
return props as Omit<DragAndDropPros, typeof propsToDelete[number]>;
};
return (
<Styled.DndTextArea
ref={dropRef}
active={drag}
// eslint-disable-next-line react/jsx-props-no-spreading
{...getCleanProps()}
/>
);
};
export default DragAndDrop;

View File

@ -14,9 +14,9 @@ import {
} from '../queries'; } from '../queries';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import Settings from '/imports/ui/services/settings'; import Settings from '/imports/ui/services/settings';
import { POLL_CANCEL, POLL_PUBLISH_RESULT } from '../mutation'; import { POLL_CANCEL, POLL_PUBLISH_RESULT } from '../mutations';
import { layoutDispatch } from '../../../layout/context'; import { layoutDispatch } from '../../layout/context';
import { ACTIONS, PANELS } from '../../../layout/enums'; import { ACTIONS, PANELS } from '../../layout/enums';
const intlMessages = defineMessages({ const intlMessages = defineMessages({
usersTitle: { usersTitle: {

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { defineMessages, useIntl } from 'react-intl'; import { defineMessages, useIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import DraggableTextArea from '/imports/ui/components/poll/dragAndDrop/component'; import DraggableTextArea from './DragAndDrop';
import { pollTypes } from '../service'; import { pollTypes } from '../service';
import Styled from '../styles'; import Styled from '../styles';

View File

@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
import { useMutation } from '@apollo/client'; import { useMutation } from '@apollo/client';
import Styled from '../styles'; import Styled from '../styles';
import { pollTypes, checkPollType } from '../service'; import { pollTypes, checkPollType } from '../service';
import { POLL_CREATE } from '../mutation'; import { POLL_CREATE } from '../mutations';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id; const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;

View File

@ -1,111 +1,3 @@
import React, { useContext } from 'react'; import PollCreationPanelContainer from './component';
import { withTracker } from 'meteor/react-meteor-data';
import Poll from '/imports/ui/components/poll/component';
import { Session } from 'meteor/session';
import { useMutation } from '@apollo/client';
import Service from './service';
import Auth from '/imports/ui/services/auth';
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;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_group_id;
const PollContainer = (props) => {
const layoutContextDispatch = layoutDispatch();
const handleChatFormsOpen = () => {
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,
});
};
const sidebarContent = layoutSelectInput((i) => i.sidebarContent);
const { sidebarContentPanel } = sidebarContent;
const users = [];
const usernames = {};
Object.values(users[Auth.meetingID]).forEach((user) => {
usernames[user.userId] = { userId: user.userId, name: user.name };
});
const [pollPublishResult] = useMutation(POLL_PUBLISH_RESULT);
const [stopPoll] = useMutation(POLL_CANCEL);
const [createPoll] = useMutation(POLL_CREATE);
const { currentSlideId } = props;
const startPoll = (pollType, secretPoll, question, isMultipleResponse, answers = []) => {
const pollId = currentSlideId || PUBLIC_CHAT_KEY;
createPoll({
variables: {
pollType,
pollId: `${pollId}/${new Date().getTime()}`,
secretPoll,
question,
isMultipleResponse,
answers,
},
});
};
const publishPoll = (pollId) => {
pollPublishResult({
variables: {
pollId,
},
});
};
return (
<Poll
{...{
layoutContextDispatch,
sidebarContentPanel,
publishPoll,
stopPoll,
startPoll,
handleChatFormsOpen,
...props,
}}
usernames={usernames}
/>
);
};
withTracker(({ amIPresenter, currentSlideId }) => {
const isPollSecret = Session.get('secretPoll') || false;
Meteor.subscribe('current-poll', isPollSecret, amIPresenter);
const { pollTypes } = Service;
return {
isPollSecret,
currentSlideId,
pollTypes,
currentPoll: Service.currentPoll(),
isDefaultPoll: Service.isDefaultPoll,
checkPollType: Service.checkPollType,
resetPollPanel: Session.get('resetPollPanel') || false,
pollAnswerIds: Service.pollAnswerIds,
isMeteorConnected: Meteor.status().connected,
validateInput: Service.validateInput,
removeEmptyLineSpaces: Service.removeEmptyLineSpaces,
getSplittedQuestionAndOptions: Service.getSplittedQuestionAndOptions,
};
})(PollContainer);
export default PollCreationPanelContainer; export default PollCreationPanelContainer;

View File

@ -1,122 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Styled from './styles';
// src: https://medium.com/@650egor/simple-drag-and-drop-file-upload-in-react-2cb409d88929
class DragAndDrop extends Component {
static handleDrag(e) {
e.preventDefault();
e.stopPropagation();
}
constructor(props) {
super(props);
this.state = {
drag: false,
pollValueText: '',
};
this.dropRef = React.createRef();
}
componentDidMount() {
this.dragCounter = 0;
const div = this.dropRef.current;
div.addEventListener('dragenter', e => this.handleDragIn(e));
div.addEventListener('dragleave', e => this.handleDragOut(e));
div.addEventListener('dragover', e => DragAndDrop.handleDrag(e));
div.addEventListener('drop', e => this.handleDrop(e));
}
componentWillUnmount() {
const div = this.dropRef.current;
div.removeEventListener('dragenter', e => this.handleDragIn(e));
div.removeEventListener('dragleave', e => this.handleDragOut(e));
div.removeEventListener('dragover', e => DragAndDrop.handleDrag(e));
div.removeEventListener('drop', e => this.handleDrop(e));
}
setPollValues() {
const { pollValueText } = this.state;
const { handlePollValuesText } = this.props;
if (pollValueText) {
handlePollValuesText(pollValueText);
}
}
setPollValuesFromFile(file) {
const reader = new FileReader();
reader.onload = async (e) => {
const text = e.target.result;
this.setPollValueText(text);
this.setPollValues();
};
reader.readAsText(file);
}
setPollValueText(pollText) {
const { MAX_INPUT_CHARS } = this.props;
const arr = pollText.split('\n');
const text = arr.map(line => (line.length > MAX_INPUT_CHARS ? line.substring(0, MAX_INPUT_CHARS) : line)).join('\n');
this.setState({ pollValueText: text });
}
handleTextInput(e) {
this.setPollValueText(e.target.value);
}
handleDragIn(e) {
DragAndDrop.handleDrag(e);
this.dragCounter += 1;
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
this.setState({ drag: true });
}
}
handleDragOut(e) {
DragAndDrop.handleDrag(e);
this.dragCounter -= 1;
if (this.dragCounter > 0) return;
this.setState({ drag: false });
}
handleDrop(e) {
DragAndDrop.handleDrag(e);
this.setState({ drag: false });
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
this.setPollValuesFromFile(e.dataTransfer.files[0]);
this.dragCounter = 0;
}
}
getCleanProps() {
const props = Object.assign({}, this.props);
const propsToDelete = ['MAX_INPUT_CHARS', 'handlePollValuesText'];
propsToDelete.forEach((prop) => {
delete props[prop];
});
return props;
}
render() {
const { drag } = this.state;
return (
<Styled.DndTextArea
ref={this.dropRef}
active={drag}
{...this.getCleanProps()}
/>
);
}
}
DragAndDrop.propTypes = {
MAX_INPUT_CHARS: PropTypes.number.isRequired,
handlePollValuesText: PropTypes.func.isRequired,
};
export default DragAndDrop;

View File

@ -1,19 +0,0 @@
import styled from 'styled-components';
import {
colorGrayLighter,
colorWhite,
} from '/imports/ui/stylesheets/styled-components/palette';
const DndTextArea = styled.textarea`
${({ active }) => active && `
background: ${colorGrayLighter};
`}
${({ active }) => !active && `
background: ${colorWhite};
`}
`;
export default {
DndTextArea,
};

View File

@ -1,294 +0,0 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl } from 'react-intl';
import caseInsensitiveReducer from '/imports/utils/caseInsensitiveReducer';
import { Session } from 'meteor/session';
import Styled from './styles';
import Service from './service';
import Settings from '/imports/ui/services/settings';
import { uniqueId } from '/imports/utils/string-utils';
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',
},
noAnswersLabel: {
id: 'app.poll.noAnswerWarning',
description: 'label shown when no answers have been received',
},
});
const getResponseString = (obj) => {
const { children } = obj.props;
if (typeof children !== 'string') {
return getResponseString(children[1]);
}
return children;
};
class LiveResult extends PureComponent {
static getDerivedStateFromProps(nextProps) {
const {
currentPoll, intl, pollAnswerIds, usernames, isDefaultPoll,
} = nextProps;
if (!currentPoll) return null;
const {
answers, responses, users, numResponders, pollType
} = currentPoll;
const defaultPoll = isDefaultPoll(pollType);
const currentPollQuestion = (currentPoll.question) ? currentPoll.question : '';
let userAnswers = responses
? [...users, ...responses.map(u => u.userId)]
: [...users];
userAnswers = userAnswers.map(id => usernames[id])
.map((user) => {
let answer = '';
if (responses) {
const response = responses.find(r => r.userId === user.userId);
if (response) {
const formattedAnswers = [];
response.answerIds.forEach((answerId) => {
const formattedMessageIndex = answers[answerId]?.key?.toLowerCase();
const formattedAnswer = defaultPoll && pollAnswerIds[formattedMessageIndex]
? intl.formatMessage(pollAnswerIds[formattedMessageIndex])
: answers[answerId].key;
formattedAnswers.push(formattedAnswer);
});
answer = formattedAnswers.join(', ');
}
}
return {
name: user.name,
answer,
};
})
.sort(Service.sortUsers)
.reduce((acc, user) => {
return ([
...acc,
(
<tr key={uniqueId('stats-')}>
<Styled.ResultLeft>{user.name}</Styled.ResultLeft>
<Styled.ResultRight data-test="receivedAnswer">
{user.answer}
</Styled.ResultRight>
</tr>
),
]);
}, []);
const pollStats = [];
answers.reduce(caseInsensitiveReducer, []).map((obj) => {
const formattedMessageIndex = obj?.key?.toLowerCase();
const pct = Math.round(obj.numVotes / numResponders * 100);
const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;
const calculatedWidth = {
width: pctFotmatted,
};
return pollStats.push(
<Styled.Main key={uniqueId('stats-')}>
<Styled.Left>
{
defaultPoll && pollAnswerIds[formattedMessageIndex]
? intl.formatMessage(pollAnswerIds[formattedMessageIndex])
: obj.key
}
</Styled.Left>
<Styled.Center>
<Styled.BarShade style={calculatedWidth} />
<Styled.BarVal data-test="numberOfVotes">{obj.numVotes || 0}</Styled.BarVal>
</Styled.Center>
<Styled.Right>
{pctFotmatted}
</Styled.Right>
</Styled.Main>,
);
});
return {
userAnswers,
pollStats,
currentPollQuestion,
};
}
constructor(props) {
super(props);
this.state = {
userAnswers: null,
pollStats: null,
currentPollQuestion: null,
};
}
render() {
const {
isMeteorConnected,
intl,
stopPoll,
handleBackClick,
currentPoll,
publishPoll,
handleChatFormsOpen,
} = this.props;
const { userAnswers, pollStats, currentPollQuestion } = this.state;
const { animations } = Settings.application;
let waiting;
let userCount = 0;
let respondedCount = 0;
if (userAnswers) {
userCount = userAnswers.length;
userAnswers.map((user) => {
const response = getResponseString(user);
if (response === '') return user;
respondedCount += 1;
return user;
});
waiting = respondedCount !== userAnswers.length && currentPoll;
}
return (
<div>
<Styled.Stats>
{currentPollQuestion ? <Styled.Title data-test="currentPollQuestion">{currentPollQuestion}</Styled.Title> : null}
<Styled.Status>
{waiting
? (
<span>
{`${intl.formatMessage(intlMessages.waitingLabel, {
0: respondedCount,
1: userCount,
})} `}
</span>
)
: <span>{intl.formatMessage(intlMessages.doneLabel)}</span>}
{waiting
? <Styled.ConnectingAnimation animations={animations}/> : null}
</Styled.Status>
{pollStats}
</Styled.Stats>
{currentPoll && currentPoll.answers.length >= 0
? (
<Styled.ButtonsActions>
<Styled.PublishButton
disabled={!isMeteorConnected || !currentPoll.numResponders}
onClick={() => {
Session.set('pollInitiated', false);
publishPoll(currentPoll?.id);
stopPoll();
handleChatFormsOpen();
}}
label={intl.formatMessage(intlMessages.publishLabel)}
data-test="publishPollingLabel"
color="primary"
/>
<Styled.CancelButton
disabled={!isMeteorConnected}
onClick={() => {
Session.set('pollInitiated', false);
Session.set('resetPollPanel', true);
stopPoll();
}}
label={intl.formatMessage(intlMessages.cancelPollLabel)}
data-test="cancelPollLabel"
/>
</Styled.ButtonsActions>
) : (
<Styled.LiveResultButton
disabled={!isMeteorConnected}
onClick={() => {
handleBackClick();
}}
label={intl.formatMessage(intlMessages.backLabel)}
color="primary"
data-test="restartPoll"
/>
)}
{currentPoll && !currentPoll.numResponders
&& intl.formatMessage(intlMessages.noAnswersLabel)}
<Styled.Separator />
{ currentPoll && !currentPoll.secretPoll
? (
<table>
<tbody>
<tr>
<Styled.THeading>{intl.formatMessage(intlMessages.usersTitle)}</Styled.THeading>
<Styled.THeading>{intl.formatMessage(intlMessages.responsesTitle)}</Styled.THeading>
</tr>
{userAnswers}
</tbody>
</table>
) : (
currentPoll ? (<div>{intl.formatMessage(intlMessages.secretPollLabel)}</div>) : null
)}
</div>
);
}
}
export default injectIntl(LiveResult);
LiveResult.defaultProps = { currentPoll: null };
LiveResult.propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
currentPoll: PropTypes.oneOfType([
PropTypes.arrayOf(Object),
PropTypes.shape({
answers: PropTypes.arrayOf(PropTypes.object),
users: PropTypes.arrayOf(PropTypes.string),
}),
]),
handleChatFormsOpen: PropTypes.func.isRequired,
stopPoll: PropTypes.func.isRequired,
handleBackClick: PropTypes.func.isRequired,
};

View File

@ -1,35 +0,0 @@
const sortUsers = (a, b) => {
const sortByResponse = (a, b) => {
const DEFAULT_CHAR = '-';
const _a = a.answer.toLowerCase();
const _b = b.answer.toLowerCase();
const isDefault = (_a === DEFAULT_CHAR || _b === DEFAULT_CHAR);
if (_a < _b || isDefault) {
return -1;
} if (_a > _b) {
return 1;
}
return 0;
};
const sortByName = (a, b) => {
const _a = a.name.toLowerCase();
const _b = b.name.toLowerCase();
if (_a < _b) {
return -1;
} if (_a > _b) {
return 1;
}
return 0;
};
let sort = sortByResponse(a, b);
if (sort === 0) sort = sortByName(a, b);
return sort;
};
export default {
sortUsers,
};

View File

@ -1,229 +0,0 @@
import styled, { css, keyframes } from 'styled-components';
import {
colorGrayLightest,
colorText,
colorGrayLighter,
pollStatsBorderColor,
} from '/imports/ui/stylesheets/styled-components/palette';
import {
smPaddingX,
smPaddingY,
mdPaddingX,
pollStatsElementWidth,
pollSmMargin,
pollResultWidth,
borderSizeLarge,
} from '/imports/ui/stylesheets/styled-components/general';
import { fontSizeBase } from '/imports/ui/stylesheets/styled-components/typography';
import Button from '/imports/ui/components/common/button/component';
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;
}
`
const ConnectingAnimation = styled.span`
&: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;
`;
const PublishButton = styled(Button)`
width: 48%;
margin-bottom: ${smPaddingY};
overflow-wrap: break-word;
white-space: pre-wrap;
`;
const CancelButton = styled(PublishButton)``;
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 {
ResultLeft,
ResultRight,
Main,
Left,
Center,
Right,
BarShade,
BarVal,
Stats,
Title,
Status,
ConnectingAnimation,
ButtonsActions,
PublishButton,
CancelButton,
LiveResultButton,
Separator,
THeading,
};

View File

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

View File

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

View File

@ -1,21 +1,31 @@
import Auth from '/imports/ui/services/auth';
import { CurrentPoll } from '/imports/api/polls';
import { escapeHtml } from '/imports/utils/string-utils';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
import { escapeHtml } from '/imports/utils/string-utils';
const POLL_AVATAR_COLOR = '#3B48A9'; const POLL_AVATAR_COLOR = '#3B48A9';
const MAX_POLL_RESULT_BARS = 10; const MAX_POLL_RESULT_BARS = 10;
const MAX_POLL_RESULT_KEY_LENGTH = 30; const MAX_POLL_RESULT_KEY_LENGTH = 30;
const POLL_BAR_CHAR = '\u220E'; const POLL_BAR_CHAR = '\u220E';
// 'YN' = Yes,No interface PollResultData {
// 'YNA' = Yes,No,Abstention id: string;
// 'TF' = True,False answers: {
// 'A-2' = A,B id: number;
// 'A-3' = A,B,C key: string;
// 'A-4' = A,B,C,D numVotes: number;
// 'A-5' = A,B,C,D,E }[];
const pollTypes = { numRespondents: number;
numResponders: number;
questionText: string;
questionType: string;
type: string;
whiteboardId: string;
}
interface Intl {
formatMessage: (descriptor: { id: string; description: string }) => string;
}
export const pollTypes = {
YesNo: 'YN', YesNo: 'YN',
YesNoAbstention: 'YNA', YesNoAbstention: 'YNA',
TrueFalse: 'TF', TrueFalse: 'TF',
@ -82,7 +92,7 @@ const intlMessages = defineMessages({
}, },
}); });
const getUsedLabels = (listOfAnswers, possibleLabels) => listOfAnswers.map( const getUsedLabels = (listOfAnswers: PollResultData['answers'], possibleLabels: string[]) => listOfAnswers.map(
(answer) => { (answer) => {
if (answer.key.length >= 2) { if (answer.key.length >= 2) {
const formattedLabel = answer.key.slice(0, 2).toUpperCase(); const formattedLabel = answer.key.slice(0, 2).toUpperCase();
@ -94,7 +104,7 @@ const getUsedLabels = (listOfAnswers, possibleLabels) => listOfAnswers.map(
}, },
); );
const getFormattedAnswerValue = (answerText) => { const getFormattedAnswerValue = (answerText: string) => {
// In generatePossibleLabels there is a check to see if the // In generatePossibleLabels there is a check to see if the
// answer's length is greater than 2 // answer's length is greater than 2
const newText = answerText.slice(2).trim(); const newText = answerText.slice(2).trim();
@ -102,9 +112,9 @@ const getFormattedAnswerValue = (answerText) => {
}; };
const generateAlphabetList = () => Array.from(Array(26)) const generateAlphabetList = () => Array.from(Array(26))
.map((e, i) => i + 65).map((x) => String.fromCharCode(x)); .map((_, i) => i + 65).map((x) => String.fromCharCode(x));
const generatePossibleLabels = (alphabetCharacters) => { const generatePossibleLabels = (alphabetCharacters: string[]) => {
// Remove the Letter from the beginning and the following sign, if any, like so: // Remove the Letter from the beginning and the following sign, if any, like so:
// "A- the answer is" -> Remove "A-" -> "the answer is" // "A- the answer is" -> Remove "A-" -> "the answer is"
const listOfForbiddenSignsToStart = ['.', ':', '-']; const listOfForbiddenSignsToStart = ['.', ':', '-'];
@ -118,7 +128,7 @@ const generatePossibleLabels = (alphabetCharacters) => {
return possibleLabels; return possibleLabels;
}; };
const truncate = (text, length) => { const truncate = (text: string, length: number) => {
let resultText = text; let resultText = text;
if (resultText.length < length) { if (resultText.length < length) {
const diff = length - resultText.length; const diff = length - resultText.length;
@ -130,7 +140,7 @@ const truncate = (text, length) => {
return resultText; return resultText;
}; };
const getPollResultsText = (isDefaultPoll, answers, numRespondents, intl) => { const getPollResultsText = (isDefaultPoll: boolean, answers: PollResultData['answers'], numRespondents: number, intl: Intl) => {
let responded = 0; let responded = 0;
let resultString = ''; let resultString = '';
let optionsString = ''; let optionsString = '';
@ -160,16 +170,16 @@ const getPollResultsText = (isDefaultPoll, answers, numRespondents, intl) => {
const pctBars = POLL_BAR_CHAR.repeat((pct * MAX_POLL_RESULT_BARS) / 100); const pctBars = POLL_BAR_CHAR.repeat((pct * MAX_POLL_RESULT_BARS) / 100);
const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`; const pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;
if (isDefaultPoll) { if (isDefaultPoll) {
let translatedKey = pollAnswerIds[item.key.toLowerCase()] let translatedKey = pollAnswerIds[item.key.toLowerCase() as keyof typeof pollAnswerIds]
? intl.formatMessage(pollAnswerIds[item.key.toLowerCase()]) ? intl.formatMessage(pollAnswerIds[item.key.toLowerCase() as keyof typeof pollAnswerIds])
: item.key; : item.key;
translatedKey = truncate(translatedKey, longestKeyLength); translatedKey = truncate(translatedKey, longestKeyLength);
resultString += `${translatedKey}: ${item.numVotes || 0} ${pctBars}${POLL_BAR_CHAR} ${pctFotmatted}\n`; resultString += `${translatedKey}: ${item.numVotes || 0} ${pctBars}${POLL_BAR_CHAR} ${pctFotmatted}\n`;
} else { } else {
if (isPollAnswerMatchFormat) { if (isPollAnswerMatchFormat) {
resultString += `${pollAnswerMatchLabeledFormat[index][0]}`; resultString += `${pollAnswerMatchLabeledFormat[index]?.[0]}`;
const formattedAnswerValue = getFormattedAnswerValue(item.key); const formattedAnswerValue = getFormattedAnswerValue(item.key);
optionsString += `${pollAnswerMatchLabeledFormat[index][0]}: ${formattedAnswerValue}\n`; optionsString += `${pollAnswerMatchLabeledFormat[index]?.[0]}: ${formattedAnswerValue}\n`;
} else { } else {
let { key } = item; let { key } = item;
key = truncate(key, longestKeyLength); key = truncate(key, longestKeyLength);
@ -182,13 +192,10 @@ const getPollResultsText = (isDefaultPoll, answers, numRespondents, intl) => {
return { resultString, optionsString }; return { resultString, optionsString };
}; };
const isDefaultPoll = (pollType) => pollType !== pollTypes.Custom const getPollResultString = (pollResultData: PollResultData, intl: Intl) => {
&& pollType !== pollTypes.Response; const formatBoldBlack = (s: string) => s.bold().fontcolor('black');
const getPollResultString = (pollResultData, intl) => { const sanitize = (value: string) => escapeHtml(value);
const formatBoldBlack = (s) => s.bold().fontcolor('black');
const sanitize = (value) => escapeHtml(value);
const { answers, numRespondents, questionType } = pollResultData; const { answers, numRespondents, questionType } = pollResultData;
const isDefault = isDefaultPoll(questionType); const isDefault = isDefaultPoll(questionType);
@ -215,39 +222,72 @@ const getPollResultString = (pollResultData, intl) => {
return pollText; return pollText;
}; };
const matchYesNoPoll = (yesValue, noValue, contentString) => { 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 ynPollString = `(${yesValue}\\s*\\/\\s*${noValue})|(${noValue}\\s*\\/\\s*${yesValue})`;
const ynOptionsRegex = new RegExp(ynPollString, 'gi'); const ynOptionsRegex = new RegExp(ynPollString, 'gi');
const ynPoll = contentString.replace(/\n/g, '').match(ynOptionsRegex) || []; const ynPoll = contentString.replace(/\n/g, '').match(ynOptionsRegex) || [];
return ynPoll; return ynPoll;
}; };
const matchYesNoAbstentionPoll = (yesValue, noValue, abstentionValue, contentString) => { 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 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 ynaOptionsRegex = new RegExp(ynaPollString, 'gi');
const ynaPoll = contentString.replace(/\n/g, '').match(ynaOptionsRegex) || []; const ynaPoll = contentString.replace(/\n/g, '').match(ynaOptionsRegex) || [];
return ynaPoll; return ynaPoll;
}; };
const matchTrueFalsePoll = (trueValue, falseValue, contentString) => { const matchTrueFalsePoll = (trueValue:string, falseValue:string, contentString:string) => {
const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`; const tfPollString = `(${trueValue}\\s*\\/\\s*${falseValue})|(${falseValue}\\s*\\/\\s*${trueValue})`;
const tgOptionsRegex = new RegExp(tfPollString, 'gi'); const tgOptionsRegex = new RegExp(tfPollString, 'gi');
const tfPoll = contentString.match(tgOptionsRegex) || []; const tfPoll = contentString.match(tgOptionsRegex) || [];
return tfPoll; return tfPoll;
}; };
const checkPollType = ( export const checkPollType = (
type, type: string | null,
optList, optList: { val: string }[],
yesValue, yesValue: string,
noValue, noValue: string,
abstentionValue, abstentionValue: string,
trueValue, trueValue: string,
falseValue, falseValue: string,
) => { ) => {
/* eslint no-underscore-dangle: "off" */
let _type = type; let _type = type;
let pollString = ''; let pollString = '';
let defaultMatch = null; let defaultMatch: RegExpMatchArray | [] | null = null;
let isDefault = null; let isDefault = null;
switch (_type) { switch (_type) {
@ -255,22 +295,22 @@ const checkPollType = (
pollString = optList.map((x) => x.val.toUpperCase()).sort().join(''); pollString = optList.map((x) => x.val.toUpperCase()).sort().join('');
defaultMatch = pollString.match(/^(ABCDEF)|(ABCDE)|(ABCD)|(ABC)|(AB)$/gi); defaultMatch = pollString.match(/^(ABCDEF)|(ABCDE)|(ABCD)|(ABC)|(AB)$/gi);
isDefault = defaultMatch && pollString.length === defaultMatch[0].length; isDefault = defaultMatch && pollString.length === defaultMatch[0].length;
_type = isDefault ? `${_type}${defaultMatch[0].length}` : pollTypes.Custom; _type = isDefault && Array.isArray(defaultMatch) ? `${_type}${defaultMatch[0].length}` : pollTypes.Custom;
break; break;
case pollTypes.TrueFalse: case pollTypes.TrueFalse:
pollString = optList.map((x) => x.val).join('/'); pollString = optList.map((x) => x.val).join('/');
defaultMatch = matchTrueFalsePoll(trueValue, falseValue, pollString); defaultMatch = matchTrueFalsePoll(trueValue, falseValue, pollString);
isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0].length; isDefault = defaultMatch.length > 0 && pollString.length === (defaultMatch[0]?.length);
if (!isDefault) _type = pollTypes.Custom; if (!isDefault) _type = pollTypes.Custom;
break; break;
case pollTypes.YesNoAbstention: case pollTypes.YesNoAbstention:
pollString = optList.map((x) => x.val).join('/'); pollString = optList.map((x) => x.val).join('/');
defaultMatch = matchYesNoAbstentionPoll(yesValue, noValue, abstentionValue, pollString); defaultMatch = matchYesNoAbstentionPoll(yesValue, noValue, abstentionValue, pollString);
isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0].length; isDefault = Array.isArray(defaultMatch) && defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
if (!isDefault) { if (!isDefault) {
// also try to match only yes/no // also try to match only yes/no
defaultMatch = matchYesNoPoll(yesValue, noValue, pollString); defaultMatch = matchYesNoPoll(yesValue, noValue, pollString);
isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0].length; isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
_type = isDefault ? pollTypes.YesNo : _type = pollTypes.Custom; _type = isDefault ? pollTypes.YesNo : _type = pollTypes.Custom;
} }
break; break;
@ -280,57 +320,18 @@ const checkPollType = (
return _type; return _type;
}; };
/**
*
* @param {String} input
*/
const validateInput = (input) => {
let _input = input;
while (/^\s/.test(_input)) _input = _input.substring(1);
return _input;
};
/**
*
* @param {String} input
*/
const removeEmptyLineSpaces = (input) => {
const filteredInput = input.split('\n').filter((val) => val.trim() !== '');
return filteredInput;
};
/**
*
* @param {String|Array} questionAndOptions
*/
const getSplittedQuestionAndOptions = (questionAndOptions) => {
const inputList = Array.isArray(questionAndOptions)
? questionAndOptions
: questionAndOptions.split('\n').filter((val) => val !== '');
const splittedQuestion = inputList.length > 0 ? inputList[0] : questionAndOptions;
const optionsList = inputList.slice(1);
optionsList.forEach((val, i) => { optionsList[i] = { val }; });
return {
splittedQuestion,
optionsList,
};
};
export default { export default {
pollTypes, pollTypes,
currentPoll: () => CurrentPoll.findOne({ meetingId: Auth.meetingID }), validateInput,
getSplittedQuestionAndOptions,
removeEmptyLineSpaces,
isDefaultPoll,
pollAnswerIds, pollAnswerIds,
POLL_AVATAR_COLOR, POLL_AVATAR_COLOR,
isDefaultPoll,
getPollResultString, getPollResultString,
matchYesNoPoll, matchYesNoPoll,
matchYesNoAbstentionPoll, matchYesNoAbstentionPoll,
matchTrueFalsePoll, matchTrueFalsePoll,
checkPollType, checkPollType,
validateInput,
removeEmptyLineSpaces,
getSplittedQuestionAndOptions,
POLL_BAR_CHAR, POLL_BAR_CHAR,
}; };

View File

@ -1,340 +0,0 @@
import styled from 'styled-components';
import Button from '/imports/ui/components/common/button/component';
import {
jumboPaddingY,
smPaddingX,
smPaddingY,
lgPaddingX,
borderRadius,
borderSize,
pollInputHeight,
pollSmMargin,
pollMdMargin,
} from '/imports/ui/stylesheets/styled-components/general';
import {
colorText,
colorBlueLight,
colorGrayLight,
colorGrayLighter,
colorGrayLightest,
colorDanger,
colorWarning,
colorHeading,
colorPrimary,
colorGrayDark,
} 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};
`;
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};
`;
const PollQuestionArea = styled.textarea`
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%;
}
`;
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};
`;
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;
`;
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;
`;
const PollButton = styled(Button)``;
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%;
`;
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,
};

View File

@ -580,6 +580,16 @@ const THeading = styled.th`
} }
`; `;
const DndTextArea = styled.textarea<{ active: boolean }>`
${({ active }) => active && `
background: ${colorGrayLighter};
`}
${({ active }) => !active && `
background: ${colorWhite};
`}
`;
export default { export default {
ToggleLabel, ToggleLabel,
PollOptionInput, PollOptionInput,
@ -631,4 +641,5 @@ export default {
LiveResultButton, LiveResultButton,
Separator, Separator,
THeading, THeading,
DndTextArea,
}; };

View File

@ -1,343 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import { defineMessages, injectIntl } from 'react-intl';
import { Meteor } from 'meteor/meteor';
import Styled from './styles';
import AudioService from '/imports/ui/components/audio/service';
import Checkbox from '/imports/ui/components/common/checkbox/component';
const MAX_INPUT_CHARS = window.meetingClientSettings.public.poll.maxTypedAnswerLength;
const intlMessages = defineMessages({
pollingTitleLabel: {
id: 'app.polling.pollingTitle',
},
pollAnswerLabel: {
id: 'app.polling.pollAnswerLabel',
},
pollAnswerDesc: {
id: 'app.polling.pollAnswerDesc',
},
pollQuestionTitle: {
id: 'app.polling.pollQuestionTitle',
},
responseIsSecret: {
id: 'app.polling.responseSecret',
},
responseNotSecret: {
id: 'app.polling.responseNotSecret',
},
submitLabel: {
id: 'app.polling.submitLabel',
},
submitAriaLabel: {
id: 'app.polling.submitAriaLabel',
},
responsePlaceholder: {
id: 'app.polling.responsePlaceholder',
},
});
const validateInput = (i) => {
let _input = i;
if (/^\s/.test(_input)) _input = '';
return _input;
};
class Polling extends Component {
constructor(props) {
super(props);
this.state = {
typedAns: '',
checkedAnswers: [],
};
this.pollingContainer = null;
this.play = this.play.bind(this);
this.handleUpdateResponseInput = this.handleUpdateResponseInput.bind(this);
this.renderButtonAnswers = this.renderButtonAnswers.bind(this);
this.handleCheckboxChange = this.handleCheckboxChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.renderCheckboxAnswers = this.renderCheckboxAnswers.bind(this);
this.handleMessageKeyDown = this.handleMessageKeyDown.bind(this);
}
componentDidMount() {
this.play();
this.pollingContainer && this.pollingContainer?.focus();
}
play() {
AudioService.playAlertSound(`${window.meetingClientSettings.public.app.cdn
+ window.meetingClientSettings.public.app.basename
+ window.meetingClientSettings.public.app.instanceId}`
+ '/resources/sounds/Poll.mp3');
}
handleUpdateResponseInput(e) {
this.responseInput.value = validateInput(e.target.value);
this.setState({ typedAns: this.responseInput.value });
}
handleSubmit(pollId) {
const { handleVote } = this.props;
const { checkedAnswers } = this.state;
handleVote(pollId, checkedAnswers);
}
handleCheckboxChange(pollId, answerId) {
const { checkedAnswers } = this.state;
if (checkedAnswers.includes(answerId)) {
checkedAnswers.splice(checkedAnswers.indexOf(answerId), 1);
} else {
checkedAnswers.push(answerId);
}
checkedAnswers.sort();
this.setState({ checkedAnswers });
}
handleMessageKeyDown(e) {
const {
poll,
handleTypedVote,
} = this.props;
const {
typedAns,
} = this.state;
if (e.keyCode === 13 && typedAns.length > 0) {
handleTypedVote(poll.pollId, typedAns);
}
}
renderButtonAnswers() {
const {
isMeteorConnected,
intl,
poll,
handleVote,
handleTypedVote,
pollAnswerIds,
pollTypes,
isDefaultPoll,
} = this.props;
const {
typedAns,
} = this.state;
if (!poll) return null;
const { stackOptions, answers, question, pollType } = poll;
const defaultPoll = isDefaultPoll(pollType);
return (
<div>
{
poll.pollType !== pollTypes.Response && (
<span>
{
question.length === 0 && (
<Styled.PollingTitle>
{intl.formatMessage(intlMessages.pollingTitleLabel)}
</Styled.PollingTitle>
)
}
<Styled.PollingAnswers removeColumns={answers.length === 1} stacked={stackOptions}>
{answers.map((pollAnswer) => {
const formattedMessageIndex = pollAnswer?.key?.toLowerCase();
let label = pollAnswer.key;
if ((defaultPoll || pollType.includes('CUSTOM')) && pollAnswerIds[formattedMessageIndex]) {
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
}
return (
<Styled.PollButtonWrapper key={pollAnswer.id} >
<Styled.PollingButton
disabled={!isMeteorConnected}
color="primary"
size="md"
label={label}
key={pollAnswer.key}
onClick={() => handleVote(poll.pollId, [pollAnswer.id])}
aria-labelledby={`pollAnswerLabel${pollAnswer.key}`}
aria-describedby={`pollAnswerDesc${pollAnswer.key}`}
data-test="pollAnswerOption"
/>
<Styled.Hidden id={`pollAnswerLabel${pollAnswer.key}`}>
{intl.formatMessage(intlMessages.pollAnswerLabel, { 0: label })}
</Styled.Hidden>
<Styled.Hidden id={`pollAnswerDesc${pollAnswer.key}`}>
{intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })}
</Styled.Hidden>
</Styled.PollButtonWrapper>
);
})}
</Styled.PollingAnswers>
</span>
)
}
{
poll.pollType === pollTypes.Response
&& (
<Styled.TypedResponseWrapper>
<Styled.TypedResponseInput
data-test="pollAnswerOption"
onChange={(e) => {
this.handleUpdateResponseInput(e);
}}
onKeyDown={(e) => {
this.handleMessageKeyDown(e);
}}
type="text"
placeholder={intl.formatMessage(intlMessages.responsePlaceholder)}
maxLength={MAX_INPUT_CHARS}
ref={(r) => { this.responseInput = r; }}
onPaste={(e) => { e.stopPropagation(); }}
onCut={(e) => { e.stopPropagation(); }}
onCopy={(e) => { e.stopPropagation(); }}
/>
<Styled.SubmitVoteButton
data-test="submitAnswer"
disabled={typedAns.length === 0}
color="primary"
size="sm"
label={intl.formatMessage(intlMessages.submitLabel)}
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
onClick={() => {
handleTypedVote(poll.pollId, typedAns);
}}
/>
</Styled.TypedResponseWrapper>
)
}
<Styled.PollingSecret>
{intl.formatMessage(poll.secretPoll ? intlMessages.responseIsSecret : intlMessages.responseNotSecret)}
</Styled.PollingSecret>
</div>
);
}
renderCheckboxAnswers() {
const {
isMeteorConnected,
intl,
poll,
pollAnswerIds,
} = this.props;
const { checkedAnswers } = this.state;
const { question } = poll;
return (
<div>
{question.length === 0
&& (
<Styled.PollingTitle>
{intl.formatMessage(intlMessages.pollingTitleLabel)}
</Styled.PollingTitle>
)}
<Styled.MultipleResponseAnswersTable>
{poll.answers.map((pollAnswer) => {
const formattedMessageIndex = pollAnswer?.key?.toLowerCase();
let label = pollAnswer?.key;
if (pollAnswerIds[formattedMessageIndex]) {
label = intl.formatMessage(pollAnswerIds[formattedMessageIndex]);
}
return (
<Styled.CheckboxContainer
key={pollAnswer.id}
>
<td>
<Styled.PollingCheckbox data-test="optionsAnswers">
<Checkbox
disabled={!isMeteorConnected}
id={`answerInput${pollAnswer.key}`}
onChange={() => this.handleCheckboxChange(poll.pollId, pollAnswer.id)}
checked={checkedAnswers.includes(pollAnswer.id)}
ariaLabelledBy={`pollAnswerLabel${pollAnswer.key}`}
ariaDescribedBy={`pollAnswerDesc${pollAnswer.key}`}
/>
</Styled.PollingCheckbox>
</td>
<Styled.MultipleResponseAnswersTableAnswerText>
<label id={`pollAnswerLabel${pollAnswer.key}`}>
{label}
</label>
<Styled.Hidden id={`pollAnswerDesc${pollAnswer.key}`} >
{intl.formatMessage(intlMessages.pollAnswerDesc, { 0: label })}
</Styled.Hidden>
</Styled.MultipleResponseAnswersTableAnswerText>
</Styled.CheckboxContainer>
);
})}
</Styled.MultipleResponseAnswersTable>
<div>
<Styled.SubmitVoteButton
disabled={!isMeteorConnected || checkedAnswers.length === 0}
color="primary"
size="sm"
label={intl.formatMessage(intlMessages.submitLabel)}
aria-label={intl.formatMessage(intlMessages.submitAriaLabel)}
onClick={() => this.handleSubmit(poll.pollId)}
data-test="submitAnswersMultiple"
/>
</div>
</div>
);
}
render() {
const {
intl,
poll,
} = this.props;
if (!poll) return null;
const { stackOptions, question } = poll;
return (
<Styled.Overlay>
<Styled.PollingContainer
autoWidth={stackOptions}
data-test="pollingContainer"
role="complementary"
ref={el => this.pollingContainer = el}
tabIndex={-1}
>
{
question.length > 0 && (
<Styled.QHeader>
<Styled.QTitle>
{intl.formatMessage(intlMessages.pollQuestionTitle)}
</Styled.QTitle>
<Styled.QText data-test="pollQuestion">{question}</Styled.QText>
</Styled.QHeader>
)
}
{poll.isMultipleResponse ? this.renderCheckboxAnswers() : this.renderButtonAnswers()}
</Styled.PollingContainer>
</Styled.Overlay>
);
}
}
export default injectIntl(injectWbResizeEvent(Polling));
Polling.propTypes = {
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
}).isRequired,
handleVote: PropTypes.func.isRequired,
handleTypedVote: PropTypes.func.isRequired,
poll: PropTypes.shape({
pollId: PropTypes.string.isRequired,
answers: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number.isRequired,
key: PropTypes.string,
}).isRequired).isRequired,
}).isRequired,
};

View File

@ -1,75 +1,3 @@
import React from 'react'; import PollingGraphqlContainer from './component';
import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data';
import { useMutation } from '@apollo/client';
import PollingService from './service';
import PollService from '/imports/ui/components/poll/service';
import PollingComponent from './component';
import { isPollingEnabled } from '/imports/ui/services/features';
import useCurrentUser from '/imports/ui/core/hooks/useCurrentUser';
import { POLL_SUBMIT_TYPED_VOTE, POLL_SUBMIT_VOTE } from '/imports/ui/components/poll/mutations';
import PollingGraphqlContainer from './polling-graphql/component';
const propTypes = {
pollExists: PropTypes.bool.isRequired,
};
const PollingContainer = ({ pollExists, ...props }) => {
const { data: currentUserData } = useCurrentUser((user) => ({
presenter: user.presenter,
}));
const showPolling = pollExists && !currentUserData?.presenter && isPollingEnabled();
const [pollSubmitUserTypedVote] = useMutation(POLL_SUBMIT_TYPED_VOTE);
const [pollSubmitUserVote] = useMutation(POLL_SUBMIT_VOTE);
const handleTypedVote = (pollId, answer) => {
pollSubmitUserTypedVote({
variables: {
pollId,
answer,
},
});
};
const handleVote = (pollId, answerIds) => {
pollSubmitUserVote({
variables: {
pollId,
answerIds,
},
});
};
if (showPolling) {
return (
<PollingComponent handleTypedVote={handleTypedVote} handleVote={handleVote} {...props} />
);
}
return null;
};
PollingContainer.propTypes = propTypes;
withTracker(() => {
const {
pollExists, poll,
} = PollingService.mapPolls();
const { pollTypes } = PollService;
if (poll && poll?.pollType) {
const isResponse = poll.pollType === pollTypes.Response;
Meteor.subscribe('polls', isResponse);
}
return ({
pollExists,
poll,
pollAnswerIds: PollService.pollAnswerIds,
pollTypes,
isDefaultPoll: PollService.isDefaultPoll,
isMeteorConnected: Meteor.status().connected,
});
})(PollingContainer);
export default PollingGraphqlContainer; export default PollingGraphqlContainer;

View File

@ -1,39 +0,0 @@
import Polls from '/imports/api/polls';
const MAX_CHAR_LENGTH = 5;
const mapPolls = () => {
const poll = Polls.findOne({});
if (!poll) {
return { pollExists: false };
}
const { answers } = poll;
let stackOptions = false;
answers.map((obj) => {
if (stackOptions) return obj;
if (obj.key && obj.key.length > MAX_CHAR_LENGTH) {
stackOptions = true;
}
return obj;
});
const amIRequester = poll.requester !== 'userId';
return {
poll: {
answers: poll.answers,
pollId: poll.id,
isMultipleResponse: poll.isMultipleResponse,
pollType: poll.pollType,
stackOptions,
question: poll.question,
secretPoll: poll.secretPoll,
},
pollExists: true,
amIRequester,
};
};
export default { mapPolls };

View File

@ -1,239 +0,0 @@
import styled from 'styled-components';
import {
mdPaddingY,
smPaddingY,
jumboPaddingY,
smPaddingX,
borderRadius,
pollWidth,
pollSmMargin,
overlayIndex,
overlayOpacity,
pollIndex,
lgPaddingY,
pollBottomOffset,
jumboPaddingX,
pollColAmount,
borderSize,
} from '/imports/ui/stylesheets/styled-components/general';
import {
fontSizeSmall,
fontSizeBase,
fontSizeLarge,
} from '/imports/ui/stylesheets/styled-components/typography';
import {
colorText,
colorBlueLight,
colorGrayLighter,
colorOffWhite,
colorGrayDark,
colorWhite,
colorPrimary,
} from '/imports/ui/stylesheets/styled-components/palette';
import { hasPhoneDimentions } from '/imports/ui/stylesheets/styled-components/breakpoints';
import Button from '/imports/ui/components/common/button/component';
const PollingTitle = styled.div`
white-space: nowrap;
padding-bottom: ${mdPaddingY};
padding-top: ${mdPaddingY};
font-size: ${fontSizeSmall};
`;
const PollButtonWrapper = styled.div`
text-align: center;
padding: ${smPaddingY};
width: 100%;
`;
const PollingButton = styled(Button)`
width: 100%;
max-width: 9em;
@media ${hasPhoneDimentions} {
max-width: none;
}
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;
const Hidden = styled.div`
display: none;
`;
const TypedResponseWrapper = styled.div`
margin: ${jumboPaddingY} .5rem .5rem .5rem;
display: flex;
flex-flow: column;
`;
const TypedResponseInput = styled.input`
&:focus {
outline: none;
border-radius: ${borderSize};
box-shadow: 0 0 0 ${borderSize} ${colorBlueLight}, inset 0 0 0 1px ${colorPrimary};
}
color: ${colorText};
-webkit-appearance: none;
padding: calc(${smPaddingY} * 2.5) calc(${smPaddingX} * 1.25);
border-radius: ${borderRadius};
font-size: ${fontSizeBase};
border: 1px solid ${colorGrayLighter};
box-shadow: 0 0 0 1px ${colorGrayLighter};
margin-bottom: 1rem;
`;
const SubmitVoteButton = styled(Button)`
font-size: ${fontSizeBase};
`;
const PollingSecret = styled.div`
font-size: ${fontSizeSmall};
max-width: ${pollWidth};
`;
const MultipleResponseAnswersTable = styled.table`
margin-left: auto;
margin-right: auto;
`;
const PollingCheckbox = styled.div`
display: inline-block;
margin-right: ${pollSmMargin};
`;
const CheckboxContainer = styled.tr`
margin-bottom: ${pollSmMargin};
`;
const MultipleResponseAnswersTableAnswerText = styled.td`
text-align: left;
`;
const Overlay = styled.div`
position: absolute;
height: 100vh;
width: 100vw;
z-index: ${overlayIndex};
pointer-events: none;
@media ${hasPhoneDimentions} {
pointer-events: auto;
background-color: rgba(0, 0, 0, ${overlayOpacity});
}
`;
const QHeader = styled.span`
text-align: left;
position: relative;
left: ${smPaddingY};
`;
const QTitle = styled.div`
font-size: ${fontSizeSmall};
`;
const QText = styled.div`
color: ${colorText};
word-break: break-word;
white-space: pre-wrap;
font-size: ${fontSizeLarge};
max-width: ${pollWidth};
padding-right: ${smPaddingX};
`;
const PollingContainer = styled.aside`
pointer-events:auto;
min-width: ${pollWidth};
position: absolute;
z-index: ${pollIndex};
border: 1px solid ${colorOffWhite};
border-radius: ${borderRadius};
box-shadow: ${colorGrayDark} 0px 0px ${lgPaddingY};
align-items: center;
text-align: center;
font-weight: 600;
padding: ${mdPaddingY};
background-color: ${colorWhite};
bottom: ${pollBottomOffset};
right: ${jumboPaddingX};
&:focus {
border: 1px solid ${colorPrimary};
}
[dir="rtl"] & {
left: ${jumboPaddingX};
right: auto;
}
@media ${hasPhoneDimentions} {
bottom: auto;
right: auto;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-height: 95%;
overflow-y: auto;
[dir="rtl"] & {
left: 50%;
}
}
${({ autoWidth }) => autoWidth && `
width: auto;
`}
`;
const PollingAnswers = styled.div`
display: grid;
grid-template-columns: repeat(${pollColAmount}, 1fr);
@media ${hasPhoneDimentions} {
grid-template-columns: repeat(1, 1fr);
& div button {
grid-column: 1;
}
}
z-index: 1;
${({ removeColumns }) => removeColumns && `
grid-template-columns: auto;
`}
${({ stacked }) => stacked && `
grid-template-columns: repeat(1, 1fr);
& div button {
max-width: none !important;
}
`}
`;
export default {
PollingTitle,
PollButtonWrapper,
PollingButton,
Hidden,
TypedResponseWrapper,
TypedResponseInput,
SubmitVoteButton,
PollingSecret,
MultipleResponseAnswersTable,
PollingCheckbox,
CheckboxContainer,
MultipleResponseAnswersTableAnswerText,
Overlay,
QHeader,
QTitle,
QText,
PollingContainer,
PollingAnswers,
};

View File

@ -14,7 +14,6 @@ const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
const SUBSCRIPTIONS = [ const SUBSCRIPTIONS = [
// 'users', // 'users',
// 'meetings', // 'meetings',
'polls',
'captions', 'captions',
// 'voiceUsers', // 'voiceUsers',
'screenshare', 'screenshare',

View File

@ -3,7 +3,6 @@ import '/imports/startup/server';
// 2x // 2x
import '/imports/api/meetings/server'; import '/imports/api/meetings/server';
import '/imports/api/users/server'; import '/imports/api/users/server';
import '/imports/api/polls/server';
import '/imports/api/captions/server'; import '/imports/api/captions/server';
import '/imports/api/presentation-upload-token/server'; import '/imports/api/presentation-upload-token/server';
import '/imports/api/breakouts/server'; import '/imports/api/breakouts/server';