Merge pull request #19989 from JoVictorNunes/remove-old-polls
cleanup: remove old polls
This commit is contained in:
commit
f2f255d37f
@ -4,7 +4,6 @@ import AbstractCollection from '/imports/ui/services/LocalCollectionSynchronizer
|
||||
import PresentationUploadToken from '/imports/api/presentation-upload-token';
|
||||
import Screenshare from '/imports/api/screenshare';
|
||||
import UserInfos from '/imports/api/users-infos';
|
||||
import Polls, { CurrentPoll } from '/imports/api/polls';
|
||||
import UserSettings from '/imports/api/users-settings';
|
||||
import VideoStreams from '/imports/api/video-streams';
|
||||
import VoiceUsers from '/imports/api/voice-users';
|
||||
@ -21,8 +20,6 @@ import Users from '/imports/api/users';
|
||||
|
||||
// Custom Publishers
|
||||
export const localCollectionRegistry = {
|
||||
localCurrentPollSync: new AbstractCollection(CurrentPoll, CurrentPoll),
|
||||
localPollsSync: new AbstractCollection(Polls, Polls),
|
||||
localPresentationUploadTokenSync: new AbstractCollection(
|
||||
PresentationUploadToken,
|
||||
PresentationUploadToken,
|
||||
|
@ -6,7 +6,6 @@ import { removeExternalVideoStreamer } from '/imports/api/external-videos/server
|
||||
import clearUsers from '/imports/api/users/server/modifiers/clearUsers';
|
||||
import clearUsersSettings from '/imports/api/users-settings/server/modifiers/clearUsersSettings';
|
||||
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 clearPads from '/imports/api/pads/server/modifiers/clearPads';
|
||||
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
|
||||
@ -33,7 +32,6 @@ export default async function meetingHasEnded(meetingId) {
|
||||
clearCaptions(meetingId),
|
||||
clearPads(meetingId),
|
||||
clearBreakouts(meetingId),
|
||||
clearPolls(meetingId),
|
||||
clearUsers(meetingId),
|
||||
clearUsersSettings(meetingId),
|
||||
clearVoiceUsers(meetingId),
|
||||
|
@ -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;
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
@ -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);
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
import './eventHandlers';
|
||||
import './publishers';
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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}`);
|
||||
}
|
||||
}
|
@ -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
@ -5,17 +5,17 @@ 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 { addAlert } from '../../screenreader-alert/service';
|
||||
import { PANELS, ACTIONS } from '../../layout/enums';
|
||||
import { Input } from '../layout/layoutTypes';
|
||||
import { layoutDispatch, layoutSelectInput } from '../layout/context';
|
||||
import { addAlert } from '../screenreader-alert/service';
|
||||
import { PANELS, ACTIONS } from '../layout/enums';
|
||||
import useMeeting from '/imports/ui/core/hooks/useMeeting';
|
||||
import { POLL_CANCEL } from './mutation';
|
||||
import { POLL_CANCEL } from './mutations';
|
||||
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 Styled from './styles';
|
||||
import ResponseChoices from './components/ResponseChoices';
|
||||
import ResponseTypes from './components/ResponseTypes';
|
||||
import PollQuestionArea from './components/PollQuestionArea';
|
@ -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;
|
@ -14,9 +14,9 @@ import {
|
||||
} 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';
|
||||
import { POLL_CANCEL, POLL_PUBLISH_RESULT } from '../mutations';
|
||||
import { layoutDispatch } from '../../layout/context';
|
||||
import { ACTIONS, PANELS } from '../../layout/enums';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
usersTitle: {
|
@ -1,7 +1,7 @@
|
||||
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 DraggableTextArea from './DragAndDrop';
|
||||
import { pollTypes } from '../service';
|
||||
import Styled from '../styles';
|
||||
|
@ -4,7 +4,7 @@ import { Meteor } from 'meteor/meteor';
|
||||
import { useMutation } from '@apollo/client';
|
||||
import Styled from '../styles';
|
||||
import { pollTypes, checkPollType } from '../service';
|
||||
import { POLL_CREATE } from '../mutation';
|
||||
import { POLL_CREATE } from '../mutations';
|
||||
|
||||
const CHAT_CONFIG = Meteor.settings.public.chat;
|
||||
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
|
@ -1,111 +1,3 @@
|
||||
import React, { useContext } from 'react';
|
||||
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);
|
||||
import PollCreationPanelContainer from './component';
|
||||
|
||||
export default PollCreationPanelContainer;
|
||||
|
@ -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;
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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,
|
||||
};
|
@ -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 { escapeHtml } from '/imports/utils/string-utils';
|
||||
|
||||
const POLL_AVATAR_COLOR = '#3B48A9';
|
||||
const MAX_POLL_RESULT_BARS = 10;
|
||||
const MAX_POLL_RESULT_KEY_LENGTH = 30;
|
||||
const POLL_BAR_CHAR = '\u220E';
|
||||
|
||||
// 'YN' = Yes,No
|
||||
// 'YNA' = Yes,No,Abstention
|
||||
// 'TF' = True,False
|
||||
// 'A-2' = A,B
|
||||
// 'A-3' = A,B,C
|
||||
// 'A-4' = A,B,C,D
|
||||
// 'A-5' = A,B,C,D,E
|
||||
const pollTypes = {
|
||||
interface PollResultData {
|
||||
id: string;
|
||||
answers: {
|
||||
id: number;
|
||||
key: string;
|
||||
numVotes: number;
|
||||
}[];
|
||||
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',
|
||||
YesNoAbstention: 'YNA',
|
||||
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) => {
|
||||
if (answer.key.length >= 2) {
|
||||
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
|
||||
// answer's length is greater than 2
|
||||
const newText = answerText.slice(2).trim();
|
||||
@ -102,9 +112,9 @@ const getFormattedAnswerValue = (answerText) => {
|
||||
};
|
||||
|
||||
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:
|
||||
// "A- the answer is" -> Remove "A-" -> "the answer is"
|
||||
const listOfForbiddenSignsToStart = ['.', ':', '-'];
|
||||
@ -118,7 +128,7 @@ const generatePossibleLabels = (alphabetCharacters) => {
|
||||
return possibleLabels;
|
||||
};
|
||||
|
||||
const truncate = (text, length) => {
|
||||
const truncate = (text: string, length: number) => {
|
||||
let resultText = text;
|
||||
if (resultText.length < length) {
|
||||
const diff = length - resultText.length;
|
||||
@ -130,7 +140,7 @@ const truncate = (text, length) => {
|
||||
return resultText;
|
||||
};
|
||||
|
||||
const getPollResultsText = (isDefaultPoll, answers, numRespondents, intl) => {
|
||||
const getPollResultsText = (isDefaultPoll: boolean, answers: PollResultData['answers'], numRespondents: number, intl: Intl) => {
|
||||
let responded = 0;
|
||||
let resultString = '';
|
||||
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 pctFotmatted = `${Number.isNaN(pct) ? 0 : pct}%`;
|
||||
if (isDefaultPoll) {
|
||||
let translatedKey = pollAnswerIds[item.key.toLowerCase()]
|
||||
? intl.formatMessage(pollAnswerIds[item.key.toLowerCase()])
|
||||
let translatedKey = pollAnswerIds[item.key.toLowerCase() as keyof typeof pollAnswerIds]
|
||||
? intl.formatMessage(pollAnswerIds[item.key.toLowerCase() as keyof typeof pollAnswerIds])
|
||||
: item.key;
|
||||
translatedKey = truncate(translatedKey, longestKeyLength);
|
||||
resultString += `${translatedKey}: ${item.numVotes || 0} ${pctBars}${POLL_BAR_CHAR} ${pctFotmatted}\n`;
|
||||
} else {
|
||||
if (isPollAnswerMatchFormat) {
|
||||
resultString += `${pollAnswerMatchLabeledFormat[index][0]}`;
|
||||
resultString += `${pollAnswerMatchLabeledFormat[index]?.[0]}`;
|
||||
const formattedAnswerValue = getFormattedAnswerValue(item.key);
|
||||
optionsString += `${pollAnswerMatchLabeledFormat[index][0]}: ${formattedAnswerValue}\n`;
|
||||
optionsString += `${pollAnswerMatchLabeledFormat[index]?.[0]}: ${formattedAnswerValue}\n`;
|
||||
} else {
|
||||
let { key } = item;
|
||||
key = truncate(key, longestKeyLength);
|
||||
@ -182,13 +192,10 @@ const getPollResultsText = (isDefaultPoll, answers, numRespondents, intl) => {
|
||||
return { resultString, optionsString };
|
||||
};
|
||||
|
||||
const isDefaultPoll = (pollType) => pollType !== pollTypes.Custom
|
||||
&& pollType !== pollTypes.Response;
|
||||
const getPollResultString = (pollResultData: PollResultData, intl: Intl) => {
|
||||
const formatBoldBlack = (s: string) => s.bold().fontcolor('black');
|
||||
|
||||
const getPollResultString = (pollResultData, intl) => {
|
||||
const formatBoldBlack = (s) => s.bold().fontcolor('black');
|
||||
|
||||
const sanitize = (value) => escapeHtml(value);
|
||||
const sanitize = (value: string) => escapeHtml(value);
|
||||
|
||||
const { answers, numRespondents, questionType } = pollResultData;
|
||||
const isDefault = isDefaultPoll(questionType);
|
||||
@ -215,39 +222,72 @@ const getPollResultString = (pollResultData, intl) => {
|
||||
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 ynOptionsRegex = new RegExp(ynPollString, 'gi');
|
||||
const ynPoll = contentString.replace(/\n/g, '').match(ynOptionsRegex) || [];
|
||||
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 ynaOptionsRegex = new RegExp(ynaPollString, 'gi');
|
||||
const ynaPoll = contentString.replace(/\n/g, '').match(ynaOptionsRegex) || [];
|
||||
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 tgOptionsRegex = new RegExp(tfPollString, 'gi');
|
||||
const tfPoll = contentString.match(tgOptionsRegex) || [];
|
||||
return tfPoll;
|
||||
};
|
||||
|
||||
const checkPollType = (
|
||||
type,
|
||||
optList,
|
||||
yesValue,
|
||||
noValue,
|
||||
abstentionValue,
|
||||
trueValue,
|
||||
falseValue,
|
||||
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 = null;
|
||||
let defaultMatch: RegExpMatchArray | [] | null = null;
|
||||
let isDefault = null;
|
||||
|
||||
switch (_type) {
|
||||
@ -255,22 +295,22 @@ const checkPollType = (
|
||||
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 ? `${_type}${defaultMatch[0].length}` : pollTypes.Custom;
|
||||
_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;
|
||||
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 = defaultMatch.length > 0 && pollString.length === defaultMatch[0].length;
|
||||
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;
|
||||
isDefault = defaultMatch.length > 0 && pollString.length === defaultMatch[0]?.length;
|
||||
_type = isDefault ? pollTypes.YesNo : _type = pollTypes.Custom;
|
||||
}
|
||||
break;
|
||||
@ -280,57 +320,18 @@ const checkPollType = (
|
||||
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 {
|
||||
pollTypes,
|
||||
currentPoll: () => CurrentPoll.findOne({ meetingId: Auth.meetingID }),
|
||||
validateInput,
|
||||
getSplittedQuestionAndOptions,
|
||||
removeEmptyLineSpaces,
|
||||
isDefaultPoll,
|
||||
pollAnswerIds,
|
||||
POLL_AVATAR_COLOR,
|
||||
isDefaultPoll,
|
||||
getPollResultString,
|
||||
matchYesNoPoll,
|
||||
matchYesNoAbstentionPoll,
|
||||
matchTrueFalsePoll,
|
||||
checkPollType,
|
||||
validateInput,
|
||||
removeEmptyLineSpaces,
|
||||
getSplittedQuestionAndOptions,
|
||||
POLL_BAR_CHAR,
|
||||
};
|
@ -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,
|
||||
};
|
@ -580,6 +580,16 @@ const THeading = styled.th`
|
||||
}
|
||||
`;
|
||||
|
||||
const DndTextArea = styled.textarea<{ active: boolean }>`
|
||||
${({ active }) => active && `
|
||||
background: ${colorGrayLighter};
|
||||
`}
|
||||
|
||||
${({ active }) => !active && `
|
||||
background: ${colorWhite};
|
||||
`}
|
||||
`;
|
||||
|
||||
export default {
|
||||
ToggleLabel,
|
||||
PollOptionInput,
|
||||
@ -631,4 +641,5 @@ export default {
|
||||
LiveResultButton,
|
||||
Separator,
|
||||
THeading,
|
||||
DndTextArea,
|
||||
};
|
@ -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,
|
||||
};
|
@ -1,75 +1,3 @@
|
||||
import React from 'react';
|
||||
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);
|
||||
import PollingGraphqlContainer from './component';
|
||||
|
||||
export default PollingGraphqlContainer;
|
||||
|
@ -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 };
|
@ -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,
|
||||
};
|
@ -14,7 +14,6 @@ const TYPING_INDICATOR_ENABLED = CHAT_CONFIG.typingIndicator.enabled;
|
||||
const SUBSCRIPTIONS = [
|
||||
// 'users',
|
||||
// 'meetings',
|
||||
'polls',
|
||||
'captions',
|
||||
// 'voiceUsers',
|
||||
'screenshare',
|
||||
|
@ -3,7 +3,6 @@ import '/imports/startup/server';
|
||||
// 2x
|
||||
import '/imports/api/meetings/server';
|
||||
import '/imports/api/users/server';
|
||||
import '/imports/api/polls/server';
|
||||
import '/imports/api/captions/server';
|
||||
import '/imports/api/presentation-upload-token/server';
|
||||
import '/imports/api/breakouts/server';
|
||||
|
Loading…
Reference in New Issue
Block a user