Skip to content
Commits on Source (24)
......@@ -39,7 +39,7 @@ stages:
- !reference [.rules-map, on-dev]
- !reference [.rules-map, not-on-semantic-release-commit]
- !reference [.rules-map, on-branch]
image: hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.7.3
image: hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.8.1
cache:
key:
files:
......@@ -117,14 +117,14 @@ build-docker:
# `release` stage: `new release`, testing prerelease`, `merge-to-dev`, `tag *`
###############################################################################
# Avoid regression and update `version` of app/package*.json in `$DEV_BRANCH`
merge-to-dev: {extends: '.git:merge-to', variables: {GIT_MERGE_TARGET: $DEV_BRANCH}}
merge-to-dev: { extends: '.git:merge-to', variables: { GIT_MERGE_TARGET: $DEV_BRANCH } }
# Create the release versions on `$STABLE_BRANCH`
new release: {extends: '.semantic-release:stable'}
new release: { extends: '.semantic-release:stable' }
# Create the prereleases versions on `$TESTING_BRANCH`
# update `.releaserc.js` variable `betaBranch`
testing prerelease: {extends: '.semantic-release:testing'}
testing prerelease: { extends: '.semantic-release:testing' }
## tag contribution branches with a more stable name than `git-${CI_COMMIT_SHORT_SHA}`
tag contrib branch:
......
......@@ -6,14 +6,14 @@
meteor-base@1.5.1 # Packages every Meteor app needs to have
mobile-experience@1.1.0 # Packages for a great mobile UX
mongo@1.15.0 # The database Meteor supports right now
reactive-var@1.0.11 # Reactive variable for tracker
tracker@1.2.0 # Meteor's client-side reactive programming library
mongo@1.16.1 # The database Meteor supports right now
reactive-var@1.0.12 # Reactive variable for tracker
tracker@1.2.1 # Meteor's client-side reactive programming library
fourseven:scss
standard-minifier-js@2.8.0 # JS minifier run for production mode
standard-minifier-js@2.8.1 # JS minifier run for production mode
es5-shim@4.8.0 # ECMAScript 5 compatibility for older browsers
ecmascript@0.16.2 # Enable ECMAScript2015+ syntax in app code
ecmascript@0.16.3 # Enable ECMAScript2015+ syntax in app code
typescript@4.5.4 # Enable TypeScript syntax in .ts and .tsx modules
shell-server@0.5.0 # Server-side component of the `meteor shell` command
......@@ -21,19 +21,19 @@ zodern:melte
rdb:svelte-meteor-data
static-html@1.3.2
accounts-password@2.3.1
accounts-base@2.2.3
hot-module-replacement@0.5.1
accounts-base@2.2.5
hot-module-replacement@0.5.2
aldeed:collection2
aldeed:schema-index
mdg:validated-method
#server-render
mexar:mdt
ddp-rate-limiter@1.1.0
ddp-rate-limiter@1.1.1
tmeasday:publish-counts
email@2.2.1
email@2.2.2
# testing
random@1.2.0
random@1.2.1
meteortesting:mocha
dburles:factory
johanbrook:publication-collector
......
accounts-base@2.2.3
accounts-base@2.2.5
accounts-oauth@1.4.1
accounts-password@2.3.1
aldeed:collection2@3.5.0
aldeed:schema-index@3.0.0
allow-deny@1.1.1
autoupdate@1.8.0
babel-compiler@7.9.0
babel-compiler@7.9.2
babel-runtime@1.5.1
base64@1.0.12
binary-heap@1.0.11
......@@ -14,29 +14,29 @@ boilerplate-generator@1.7.1
caching-compiler@1.2.2
caching-html-compiler@1.2.1
callback-hook@1.4.0
check@1.3.1
check@1.3.2
dburles:factory@1.1.0
ddp@1.4.0
ddp-client@2.5.0
ddp@1.4.1
ddp-client@2.6.1
ddp-common@1.4.0
ddp-rate-limiter@1.1.0
ddp-server@2.5.0
diff-sequence@1.1.1
ddp-rate-limiter@1.1.1
ddp-server@2.6.0
diff-sequence@1.1.2
dynamic-import@0.7.2
ecmascript@0.16.2
ecmascript@0.16.3
ecmascript-runtime@0.8.0
ecmascript-runtime-client@0.12.1
ecmascript-runtime-server@0.11.0
ejson@1.1.2
email@2.2.1
ejson@1.1.3
email@2.2.2
eoleteam:accounts-keycloak@2.1.0
eoleteam:keycloak-oauth@2.2.0
es5-shim@4.8.0
fetch@0.1.1
fetch@0.1.2
fourseven:scss@4.15.0
geojson-utils@1.0.10
geojson-utils@1.0.11
hot-code-push@1.0.4
hot-module-replacement@0.5.1
hot-module-replacement@0.5.2
html-tools@1.1.3
htmljs@1.1.1
http@2.0.0
......@@ -47,54 +47,54 @@ launch-screen@1.3.0
localstorage@1.2.0
logging@1.3.1
mdg:validated-method@1.2.0
meteor@1.10.0
meteor@1.10.2
meteor-base@1.5.1
meteortesting:browser-tests@1.3.5
meteortesting:mocha@2.0.3
meteortesting:mocha-core@8.1.2
mexar:mdt@0.2.2
minifier-js@2.7.4
minimongo@1.8.0
minifier-js@2.7.5
minimongo@1.9.0
mobile-experience@1.1.0
mobile-status-bar@1.1.0
modern-browsers@0.1.8
modules@0.18.0
modules-runtime@0.13.0
modules-runtime-hot@0.14.0
mongo@1.15.0
modern-browsers@0.1.9
modules@0.19.0
modules-runtime@0.13.1
modules-runtime-hot@0.14.1
mongo@1.16.1
mongo-decimal@0.1.3
mongo-dev-server@1.1.0
mongo-id@1.0.8
npm-mongo@4.3.1
npm-mongo@4.11.0
oauth@2.1.2
oauth2@1.3.1
ordered-dict@1.1.0
promise@0.12.0
promise@0.12.1
raix:eventemitter@1.0.0
random@1.2.0
random@1.2.1
rate-limit@1.0.9
rdb:svelte-meteor-data@0.3.1
react-fast-refresh@0.2.3
reactive-var@1.0.11
reactive-var@1.0.12
reload@1.3.1
retry@1.1.0
routepolicy@1.1.1
service-configuration@1.3.0
service-configuration@1.3.1
sha@1.0.9
shell-server@0.5.0
socket-stream-client@0.5.0
spacebars-compiler@1.3.1
standard-minifier-js@2.8.0
standard-minifier-js@2.8.1
static-html@1.3.2
svelte:compiler@3.46.4
templating-tools@1.2.2
tmeasday:check-npm-versions@1.0.2
tmeasday:publish-counts@0.8.0
tracker@1.2.0
tracker@1.2.1
typescript@4.5.4
underscore@1.0.10
underscore@1.0.11
url@1.3.2
webapp@1.13.1
webapp-hashing@1.1.0
zodern:melte@1.6.0
zodern:melte-compiler@1.3.0
webapp@1.13.2
webapp-hashing@1.1.1
zodern:melte@1.6.1
zodern:melte-compiler@1.3.1
# The tag here should match the Meteor version of your app, per .meteor/release
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.7.3
FROM hub.eole.education/proxyhub/geoffreybooth/meteor-base:2.8.1
# Copy app package.json and package-lock.json into container
#COPY ./app/package*.json $APP_SOURCE_FOLDER/
......@@ -14,7 +14,7 @@ RUN bash $SCRIPTS_FOLDER/build-meteor-bundle.sh
# Rather than Node 8 latest (Alpine), you can also use the specific version of Node expected by your Meteor release, per https://docs.meteor.com/changelog.html
FROM hub.eole.education/proxyhub/library/node:14.19.3-alpine
FROM hub.eole.education/proxyhub/library/node:14.21.1-alpine
ENV APP_BUNDLE_FOLDER /opt/bundle
ENV SCRIPTS_FOLDER /docker
......@@ -37,7 +37,7 @@ RUN bash $SCRIPTS_FOLDER/build-meteor-npm-dependencies.sh --build-from-source
# Start another Docker stage, so that the final image doesn’t contain the layer with the build dependencies
# See previous FROM line; this must match
FROM hub.eole.education/proxyhub/library/node:14.19.3-alpine
FROM hub.eole.education/proxyhub/library/node:14.21.1-alpine
ENV APP_BUNDLE_FOLDER /opt/bundle
ENV SCRIPTS_FOLDER /docker
......
# Changelog
## [1.6.0](https://gitlab.mim-libre.fr/alphabet/sondage/compare/release/1.5.0...release/1.6.0) (2023-01-30)
### Features
* **groups:** display structure groups correctly ([4cdce24](https://gitlab.mim-libre.fr/alphabet/sondage/commit/4cdce244b93153121bd69cd0c19611c2070e6e3f))
* **meetings:** add meeting answer cancel and edit page ([62ba40b](https://gitlab.mim-libre.fr/alphabet/sondage/commit/62ba40b5926b72470d3ceb4a20c69744c5561c2c))
* **meetings:** allow poll owner to edit meeting slot for an answer ([cab6c99](https://gitlab.mim-libre.fr/alphabet/sondage/commit/cab6c99af32ccb1b62b61e6cc2d1b13e092bf84a))
* **meetings:** check user input on meeting answer edit ([20281e8](https://gitlab.mim-libre.fr/alphabet/sondage/commit/20281e828bccb41398e3baa5de5c52ee7020c489))
* **meteor:** update meteor to 2.8.1 ([f1b5cca](https://gitlab.mim-libre.fr/alphabet/sondage/commit/f1b5ccae41c392cfbfe39036ea25e4d57bd86d04))
* **packages:** update meteor to 2.8.0 and others packages ([3771692](https://gitlab.mim-libre.fr/alphabet/sondage/commit/3771692c4576357b1f6686c262e457c39dea99f3))
* **poll creation:** don't allow duplicate time slots ([dd841b1](https://gitlab.mim-libre.fr/alphabet/sondage/commit/dd841b143a48158edf3d98a871397c1556d02ff3))
* **poll:** add divider in poll answer ([df5de28](https://gitlab.mim-libre.fr/alphabet/sondage/commit/df5de28b808973390a06f4caeac8624e0680b21e))
* **polls:** add ten minutes step ([d6e1e50](https://gitlab.mim-libre.fr/alphabet/sondage/commit/d6e1e5017f0582fae0ca7bc957de4b35a3495597))
### Bug Fixes
* **edition:** don't add erroneous values at group selection ([d445769](https://gitlab.mim-libre.fr/alphabet/sondage/commit/d445769dbb96321c778f048fd8fac9b1755730c5))
* **navigation:** add condition on previous button in step 4 ([08907f0](https://gitlab.mim-libre.fr/alphabet/sondage/commit/08907f0f2926e4551e4dd6f7c6ab9dceeaeb7c0e))
### Tests
* **meeting:** add tests for poll_answers edit, cancel, get ([16b95c5](https://gitlab.mim-libre.fr/alphabet/sondage/commit/16b95c5f8f709a040b40fb9b55978e258f20ad3c))
## [1.5.0](https://gitlab.mim-libre.fr/alphabet/sondage/compare/release/1.4.0...release/1.5.0) (2022-11-22)
......
......@@ -12,6 +12,26 @@ export const meetingTemplate = ({ sender, date }) => `
<br/>
`;
export const meetingCancelTemplate = ({ date, content }) => `
<h4>Votre rendez vous le ${date} a été annulé</h4>
<br/>
<div>
${content}
</div>
`;
export const meetingEditTemplate = ({ date, email, name }) => `
<h4>Votre rendez vous a été édité</h4>
<br/>
<div>
Date: ${date}
<br/>
Votre nom: ${name}
<br/>
Votre adresse email: ${email}
</div>
`;
export const eventTemplate = ({ sender, title, date }) => `
<h4>Votre évenement a été confirmé</h4>
<br/>
......
......@@ -7,7 +7,13 @@ import PollsAnswers from '../../polls_answers/polls_answers';
import Polls from '../../polls/polls';
import Groups from '../../groups/groups';
import { DURATIONS_TIME, POLLS_TYPES, ROUTES } from '../../../utils/enums';
import { meetingTemplate, eventTemplate, adminMeetingTemplate } from './email_template';
import {
meetingTemplate,
eventTemplate,
adminMeetingTemplate,
meetingCancelTemplate,
meetingEditTemplate,
} from './email_template';
import { EventsAgenda } from '../events';
export const sendEmail = new ValidatedMethod({
......@@ -68,6 +74,33 @@ export function sendEmailToCreator(poll, answer, userId) {
html,
});
}
export function sendCancelEmail(poll, answer, content) {
const template = meetingCancelTemplate;
const html = template({ date: moment(answer.meetingSlot).format('LLL'), content });
Email.send({
to: answer.email,
from: Meteor.settings.smtp.fromEmail,
subject: `Sondage - annulation de votre rendez-vous pour ${poll.title}`,
inReplyTo: Meteor.settings.smtp.toEmail,
html,
});
}
export function sendEditEmail(poll, email, name, meetingSlot) {
const template = meetingEditTemplate;
const html = template({ date: moment(meetingSlot).format('LLL'), email, name });
Email.send({
to: email,
from: Meteor.settings.smtp.fromEmail,
subject: `Sondage - Edition de votre rendez-vous pour ${poll.title}`,
inReplyTo: Meteor.settings.smtp.toEmail,
html,
});
}
export const createEventAgendaMeeting = new ValidatedMethod({
name: 'events.createMeeting',
validate: new SimpleSchema({
......
......@@ -14,7 +14,7 @@ Meteor.publish('groups.memberOf', function () {
{
limit: 100,
sort: { name: -1 },
fields: { _id: 1, name: 1 },
fields: { _id: 1, name: 1, type: 1 },
},
);
});
......@@ -7,7 +7,13 @@ import { POLLS_TYPES } from '../../../utils/enums';
import PollsAnswers from '../polls_answers';
import Groups from '../../groups/groups';
import Polls from '../../polls/polls';
import { createEventAgendaMeeting, sendEmail, sendEmailToCreator } from '../../events/server/methods';
import {
createEventAgendaMeeting,
sendEmail,
sendCancelEmail,
sendEditEmail,
sendEmailToCreator,
} from '../../events/server/methods';
export const createPollAnswers = new ValidatedMethod({
name: 'polls_answers.create',
......@@ -100,6 +106,73 @@ export const validateMeetingPollAnswer = new ValidatedMethod({
},
});
export const cancelMeetingPollAnswer = new ValidatedMethod({
name: 'polls_answers.meeting.cancel',
validate: new SimpleSchema({
answerId: String,
emailNotice: Boolean,
emailContent: String,
}).validator(),
run({ answerId, emailNotice, emailContent }) {
const answer = PollsAnswers.findOne({ _id: answerId });
if (!answer) {
throw new Meteor.Error('api.polls_answers.methods.cancel.notFound', 'api.errors.answerNotFound');
}
const poll = Polls.findOne({ _id: answer.pollId });
if (poll.userId !== this.userId || poll.type !== POLLS_TYPES.MEETING) {
throw new Meteor.Error('api.polls_answers.methods.cancel.notAllowed', 'api.errors.notAllowed');
}
if (emailNotice) sendCancelEmail(poll, answer, emailContent);
return PollsAnswers.remove({ _id: answerId });
},
});
export const editMeetingPollAnswer = new ValidatedMethod({
name: 'polls_answers.meeting.edit',
validate: new SimpleSchema({
answerId: String,
emailNotice: Boolean,
email: {
type: String,
regEx: SimpleSchema.RegEx.Email,
},
name: String,
meetingSlot: Date,
}).validator(),
run({ answerId, emailNotice, email, name, meetingSlot }) {
const answer = PollsAnswers.findOne({ _id: answerId });
if (!answer) {
throw new Meteor.Error('api.polls_answers.methods.edit.notFound', 'api.errors.answerNotFound');
}
const poll = Polls.findOne({ _id: answer.pollId });
if (poll.userId !== this.userId || poll.type !== POLLS_TYPES.MEETING) {
throw new Meteor.Error('api.polls_answers.methods.edit.notAllowed', 'api.errors.notAllowed');
}
if (emailNotice) sendEditEmail(poll, email, name, meetingSlot);
return PollsAnswers.update({ _id: answerId }, { $set: { email, name, meetingSlot } });
},
});
export const getPollAnswer = new ValidatedMethod({
name: 'polls_answers.get',
validate: new SimpleSchema({
answerId: String,
}).validator({ clean: true }),
run({ answerId }) {
const answer = PollsAnswers.findOne(answerId);
if (!answer) {
throw new Meteor.Error('api.polls_answers.methods.get.notFound', 'api.errors.answerNotFound');
}
const poll = Polls.findOne(answer.pollId);
if (poll.userId !== this.userId) {
throw new Meteor.Error('api.polls_answers.methods.get.notAllowed', 'api.errors.notAllowed');
}
return answer;
},
});
const methodsKeys = ['polls_answers.create', 'polls_answers.meeting.validate'];
DDPRateLimiter.addRule(
{
......
......@@ -6,8 +6,15 @@ import { Random } from 'meteor/random';
import faker from 'faker';
import { Factory } from 'meteor/dburles:factory';
import { PublicationCollector } from 'meteor/johanbrook:publication-collector';
import { POLLS_TYPES } from '../../../utils/enums';
import { createPollAnswers, validateMeetingPollAnswer } from './methods';
import {
createPollAnswers,
validateMeetingPollAnswer,
cancelMeetingPollAnswer,
editMeetingPollAnswer,
getPollAnswer,
} from './methods';
import './publications';
import PollsAnswers from '../polls_answers';
import Polls from '../../polls/polls';
......@@ -268,5 +275,152 @@ describe('polls_answers', function () {
assert.isTrue(resultPollAnswer.confirmed);
});
});
describe('cancelMeetingPollanswer', function () {
it('should throw error if user is not pollOwner', function () {
const poll = Factory.create('poll', {
userId: ownerPollUser._id,
active: true,
public: true,
type: POLLS_TYPES.MEETING,
});
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
confirmed: true,
pollId: poll._id,
});
assert.throws(
() => {
cancelMeetingPollAnswer._execute(
{ userId: anotherUser._id },
{ answerId: pollAnswer._id, emailNotice: false, emailContent: '' },
);
},
Meteor.Error,
/api.polls_answers.methods.cancel.notAllowed/,
);
});
it('should delete pollAnswer', function () {
const poll = Factory.create('poll', { userId: ownerPollUser._id, type: POLLS_TYPES.MEETING });
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
confirmed: true,
pollId: poll._id,
});
cancelMeetingPollAnswer._execute(
{ userId: ownerPollUser._id },
{ answerId: pollAnswer._id, emailNotice: false, emailContent: '' },
);
const resultPollAnswer = PollsAnswers.findOne({ _id: pollAnswer._id });
assert.equal(resultPollAnswer, null);
});
});
describe('editMeetingPollanswer', function () {
it('should throw error if user is not pollOwner', function () {
const poll = Factory.create('poll', {
userId: ownerPollUser._id,
active: true,
public: true,
type: POLLS_TYPES.MEETING,
});
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: 'toto',
confirmed: true,
pollId: poll._id,
});
assert.throws(
() => {
editMeetingPollAnswer._execute(
{ userId: anotherUser._id },
{
answerId: pollAnswer._id,
emailNotice: false,
email: 'newmail@test.fr',
name: 'titi',
meetingSlot: new Date(),
},
);
},
Meteor.Error,
/api.polls_answers.methods.edit.notAllowed/,
);
});
it('should modify pollAnswer', function () {
const poll = Factory.create('poll', { userId: ownerPollUser._id, type: POLLS_TYPES.MEETING });
const newSlot = new Date();
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
name: 'toto',
confirmed: true,
pollId: poll._id,
});
editMeetingPollAnswer._execute(
{ userId: ownerPollUser._id },
{
answerId: pollAnswer._id,
emailNotice: false,
email: 'newmail@test.fr',
name: 'titi',
meetingSlot: newSlot,
},
);
const resultPollAnswer = PollsAnswers.findOne({ _id: pollAnswer._id });
assert.equal(resultPollAnswer.name, 'titi');
assert.equal(resultPollAnswer.email, 'newmail@test.fr');
assert.equal(resultPollAnswer.meetingSlot.toString(), newSlot.toString());
});
});
describe('getPollanswer', function () {
it('should throw error if user is not pollOwner', function () {
const poll = Factory.create('poll', { userId: ownerPollUser._id, active: true, public: true });
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
confirmed: true,
pollId: poll._id,
});
assert.throws(
() => {
getPollAnswer._execute({ userId: anotherUser._id }, { answerId: pollAnswer._id });
},
Meteor.Error,
/api.polls_answers.methods.get.notAllowed/,
);
});
it('should throw error if poll answer does not exist', function () {
const poll = Factory.create('poll', { userId: ownerPollUser._id, active: true, public: true });
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
confirmed: true,
pollId: poll._id,
});
assert.throws(
() => {
getPollAnswer._execute({ userId: ownerPollUser._id }, { answerId: `xxx${pollAnswer._id}` });
},
Meteor.Error,
/api.polls_answers.methods.get.notFound/,
);
});
it('should send poll answer to poll owner', function () {
const poll = Factory.create('poll', { userId: ownerPollUser._id });
const pollAnswer = Factory.create('poll_answer', {
userId: ownerPollUser._id,
email: ownerPollUser.emails[0].address,
confirmed: true,
pollId: poll._id,
});
const result = getPollAnswer._execute({ userId: ownerPollUser._id }, { answerId: pollAnswer._id });
assert.equal(result._id, pollAnswer._id);
assert.equal(result.pollId, poll._id);
assert.equal(result.email, ownerPollUser.emails[0].address);
assert.equal(result.userId, ownerPollUser._id);
});
});
});
});
......@@ -17,6 +17,8 @@
import { currentUser, loggingIn } from '/imports/utils/functions/stores';
import PollStepsRoutes from './routes/PollEdition/PollStepsRoutes.svelte';
import Poll from './routes/Poll/Poll.svelte';
import MeetingAnswerCancel from './routes/Poll/MeetingAnswerCancel.svelte';
import MeetingAnswerEdit from './routes/Poll/MeetingAnswerEdit.svelte';
import Transition from './components/common/Transition.svelte';
import Logout from './routes/Logout.svelte';
import Maintenance from './routes/Maintenance.svelte';
......@@ -62,6 +64,12 @@
<Route path={ROUTES.ADMIN} let:meta>
<Home {meta} />
</Route>
<Route path={ROUTES.CANCEL_ANSWER} let:meta>
<MeetingAnswerCancel {meta} />
</Route>
<Route path={ROUTES.EDIT_ANSWER} let:meta>
<MeetingAnswerEdit {meta} />
</Route>
<Route path="/" redirect={ROUTES.POLLS} />
{/if}
<Route path={ROUTES.POLL}>
......
......@@ -13,6 +13,8 @@
export let poll = {};
export let toggleChoice = () => null;
export let currentAnswer = '';
export let editMode = false;
let answers;
let options;
let events;
......@@ -28,7 +30,7 @@
}
const selectSlot = ({ event }) => {
if (Meteor.userId() === poll.userId) {
if (Meteor.userId() === poll.userId && !editMode) {
return;
}
if (!$answers.find((a) => moment(a.meetingSlot).isSame(event.start))) {
......@@ -97,7 +99,7 @@
locale: $locale,
timeZone: 'local',
// date handling
slotDuration: poll.duration === '00:15' ? '00:15:00' : '00:30:00',
slotDuration: poll.duration === '00:10' ? '00:10:00' : poll.duration === '00:15' ? '00:15:00' : '00:30:00',
businessHours: {
daysOfWeek: [1, 2, 3, 4, 5, 6, 7],
startTime: '05:00',
......
<script>
import { _ } from 'svelte-i18n';
import moment from 'moment';
import { Meteor } from 'meteor/meteor';
import { toast } from '@zerodevx/svelte-toast';
import { router } from 'tinro';
import { ROUTES, toasts } from '/imports/utils/enums';
import Checkbox from '../../components/common/Checkbox.svelte';
import BigLink from '/imports/ui/components/common/BigLink.svelte';
import PackageJSON from '../../../../package.json';
import { onMount } from 'svelte';
let version = PackageJSON.version;
export let meta;
let emailNotice = true;
let emailMsg = '';
let answer = {};
const answerId = meta.params._id;
const cancelMeeting = () => {
Meteor.call('polls_answers.meeting.cancel', { answerId: answer._id, emailNotice, emailContent: emailMsg }, (e) => {
if (e) {
console.log(e);
toast.push($_(e.reason), toasts.error);
} else {
router.goto(ROUTES.ANSWER_POLL_RM(answer.pollId));
}
});
};
const grabData = () => {
if (meta.params._id) {
Meteor.call('polls_answers.get', { answerId }, (e, r) => {
if (r) {
answer = r;
} else {
toast.push($_(e.reason), toasts.error);
router.goto(ROUTES.ADMIN);
}
});
} else {
toast.push($_('components.MeetingAnswerCancel.answer_not_found'), toasts.error);
router.goto(ROUTES.ADMIN);
}
};
onMount(grabData);
</script>
<svelte:head>
<title>{$_('title')} {version} | {$_('links.meeting_cancel')}</title>
</svelte:head>
<section class="box-transparent">
<div class="container">
<h1 class="title is-3">
{$_('components.MeetingAnswerCancel.title')} : {moment(answer.meetingSlot).format('LLL')}
</h1>
<div class="box">
<div class="field">
<div class="control">
<Checkbox bind:checked={emailNotice} label={$_('components.MeetingAnswerCancel.sendEmail')} />
</div>
</div>
<div class="field">
<label class="label">{$_('components.MeetingAnswerCancel.emailMsg')}</label>
<div class="control">
<textarea disabled={!emailNotice} class="textarea" maxlength="2048" rows="5" bind:value={emailMsg} />
</div>
</div>
<div class="columns is-multiline">
<div class="column is-half-desktop is-full-mobile">
<BigLink
link={ROUTES.ANSWER_POLL_RM(answer.pollId)}
text={$_('components.MeetingAnswerCancel.goBack')}
color="is-secondary"
/>
</div>
<div class="column is-half-desktop is-full-mobile is-right">
<BigLink action={cancelMeeting} text={$_('components.MeetingAnswerCancel.delete')} color="is-danger" />
</div>
</div>
</div>
</div>
</section>
<style>
.is-right {
display: flex;
justify-content: flex-end;
}
</style>
<script>
import { _ } from 'svelte-i18n';
import moment from 'moment';
import { Meteor } from 'meteor/meteor';
import { toast } from '@zerodevx/svelte-toast';
import { router } from 'tinro';
import { ROUTES, toasts } from '/imports/utils/enums';
import CalendarPoll from './CalendarPoll.svelte';
import Checkbox from '../../components/common/Checkbox.svelte';
import BigLink from '/imports/ui/components/common/BigLink.svelte';
import isValideMail from '/imports/utils/functions/email';
import PackageJSON from '../../../../package.json';
import { onMount } from 'svelte';
let version = PackageJSON.version;
export let meta;
let emailNotice = true;
let answer = {};
let poll = {};
let email = '';
let name = '';
let meetingSlot = '';
let initialDate = '';
let loading = true;
let pollLoaded = false;
let answerLoaded = false;
const answerId = meta.params._id;
const pollId = meta.params.pollId;
const editMeeting = () => {
Meteor.call('polls_answers.meeting.edit', { answerId: answer._id, emailNotice, email, name, meetingSlot }, (e) => {
if (e) {
console.log(e);
toast.push($_(e.reason), toasts.error);
} else {
router.goto(ROUTES.ANSWER_POLL_RM(answer.pollId));
}
});
};
const toggleChoice = (indexOrDate) => {
meetingSlot = indexOrDate || initialDate;
answer.meetingSlot = indexOrDate;
};
const grabData = () => {
if (meta.params._id) {
Meteor.call('polls_answers.get', { answerId }, (e, r) => {
answerLoaded = true;
if (pollLoaded) loading = false;
if (r) {
answer = r;
email = r.email;
name = r.name;
meetingSlot = r.meetingSlot;
initialDate = r.meetingSlot;
} else {
toast.push($_(e.reason), toasts.error);
router.goto(ROUTES.ADMIN);
}
});
} else {
toast.push($_('components.MeetingAnswerEdit.answer_not_found'), toasts.error);
router.goto(ROUTES.ADMIN);
}
if (meta.params.pollId) {
Meteor.call('polls.getSinglePollToAnswer', { pollId }, (e, r) => {
pollLoaded = true;
if (answerLoaded) loading = false;
if (r) {
poll = r.poll;
} else {
toast.push($_(e.reason), toasts.error);
router.goto(ROUTES.ADMIN);
}
});
} else {
toast.push($_('pages.new_poll.poll_not_found'), toasts.error);
router.goto(ROUTES.ADMIN);
}
};
onMount(grabData);
</script>
<svelte:head>
<title>{$_('title')} {version} | {$_('links.meeting_edit')}</title>
</svelte:head>
<section class="box-transparent">
<div class="container">
<h1 class="title is-3">
{$_('components.MeetingAnswerEdit.title')} : {moment(initialDate).format('LLL')}
</h1>
<div class="box">
<div class="field">
<div class="control">
<Checkbox bind:checked={emailNotice} label={$_('components.MeetingAnswerCancel.sendEmail')} />
</div>
</div>
<div class="field">
<label class="label">{$_('components.MeetingAnswerEdit.email')}</label>
<div class="control">
<input
disabled={answer.userId !== null}
class="input"
type="text"
autofocus
maxlength="256"
bind:value={email}
/>
</div>
</div>
<div class="field">
<label class="label">{$_('components.MeetingAnswerEdit.name')}</label>
<div class="control">
<input disabled={answer.userId !== null} class="input" type="text" maxlength="256" bind:value={name} />
</div>
</div>
<label class="label">{$_('components.MeetingAnswerEdit.slot')}</label>
<div class="column is-full">
{#if pollLoaded}
<CalendarPoll {answer} {poll} {loading} {toggleChoice} currentAnswer={answer} editMode={true} />
{/if}
</div>
<div class="columns is-multiline">
<div class="column is-half-desktop is-full-mobile">
<BigLink
link={ROUTES.ANSWER_POLL_RM(answer.pollId)}
text={$_('components.MeetingAnswerCancel.goBack')}
color="is-secondary"
/>
</div>
<div class="column is-half-desktop is-full-mobile is-right">
<BigLink
disabled={!isValideMail(email) || !name || loading}
action={editMeeting}
text={$_('components.MeetingAnswerEdit.submit')}
/>
</div>
</div>
</div>
</div>
</section>
<style>
.is-right {
display: flex;
justify-content: flex-end;
}
</style>
......@@ -8,6 +8,7 @@
import { ROUTES, toasts } from '/imports/utils/enums';
import { currentUser, loggingIn, accountsConfigured } from '/imports/utils/functions/stores';
import isValideMail from '/imports/utils/functions/email';
// components
import BigLink from '/imports/ui/components/common/BigLink.svelte';
import Divider from '/imports/ui/components/common/Divider.svelte';
......@@ -15,12 +16,15 @@
import CalendarPoll from './CalendarPoll.svelte';
import PollDateTable from './PollDateTable.svelte';
import { POLLS_TYPES } from '../../../utils/enums';
import getGroupName from '/imports/utils/functions/groups'
import MeetingAnswersList from './MeetingAnswersList.svelte';
import PackageJSON from '../../../../package.json';
import Modal from '../../components/common/Modal.svelte';
let version = PackageJSON.version;
$: mobile = () => window.innerWidth < 600;
export let meta;
let selectedGroups;
let poll = {};
......@@ -124,15 +128,6 @@
}
});
};
function isValideMail(mail) {
var regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
if (mail.match(regex)) {
return true;
}
return false;
}
</script>
<svelte:head>
......@@ -193,7 +188,7 @@
<div class="tags">
{#each selectedGroups as group}
<span class="tag is-medium is-primary">
{group.name}
{getGroupName(group)}
</span>
{/each}
</div>
......@@ -204,7 +199,7 @@
<div class="column is-full">
<Divider />
</div>
<div class="column is-half">
<div class="column is-two-fifths">
<label class="label">{$_('pages.answer.details')}</label>
<div class="field">
<div class="control">
......@@ -225,7 +220,14 @@
</div>
</div>
</div>
<div class="column is-half">
{#if window.innerWidth < 600}
<div class="column is-full">
<Divider />
</div>
{:else}
<div id="Vdivider" class="column is-one-fifths" />
{/if}
<div class="column is-two-fifths">
{#if !Meteor.userId()}
<label class="label">{$_('pages.answer.login_with_lb')}</label>
<div class="control">
......@@ -283,4 +285,10 @@
display: flex;
justify-content: flex-end;
}
#Vdivider {
border-left: 1px solid var(--lightgrey);
margin-left: 10%;
margin-top: 1vw;
margin-bottom: 1vw;
}
</style>
......@@ -3,7 +3,7 @@
import moment from 'moment';
import tippy from 'sveltejs-tippy';
import { toast } from '@zerodevx/svelte-toast';
import { toasts } from '../../../utils/enums';
import { toasts, ROUTES } from '../../../utils/enums';
export let answer;
let loading = false;
......@@ -50,6 +50,22 @@
{/if}
</span>
</button>
<a class="button is-primary" href={ROUTES.EDIT_ANSWER_RM(answer.pollId, answer._id)}>
<span class="icon">
<i class="fas fa-pen" />
</span>
<span>
{$_('components.SinglePollAnswerLine.edit')}
</span>
</a>
<a class="button is-danger" href={ROUTES.CANCEL_ANSWER_RM(answer._id)}>
<span class="icon">
<i class="fas fa-trash" />
</span>
<span>
{$_('components.SinglePollAnswerLine.cancel')}
</span>
</a>
</div>
</th>
<td>
......
......@@ -84,7 +84,8 @@
locale: $locale,
timeZone: 'local',
// date handling
slotDuration: $newPollStore.duration === '00:15' ? '00:15:00' : '00:30:00',
slotDuration:
$newPollStore.duration === '00:10' ? '00:10:00' : $newPollStore.duration === '00:15' ? '00:15:00' : '00:30:00',
selectable: true,
select: selectTimeSlot,
selectOverlap: false,
......
......@@ -19,6 +19,7 @@
import StepBar from '../../components/common/StepBar.svelte';
import Radios from '../../components/common/Radios.svelte';
import { POLLS_TYPES } from '/imports/utils/enums';
import getGroupName from '/imports/utils/functions/groups'
import PackageJSON from '../../../../package.json';
let version = PackageJSON.version;
......@@ -61,7 +62,9 @@
);
const handleSelect = (event) => {
$newPollStore.groups = [...$newPollStore.groups, event.target.value];
if (event.target.value !== "null" && !$newPollStore.groups.includes(event.target.value)) {
$newPollStore.groups = [...$newPollStore.groups, event.target.value];
}
};
const handleRemoveGroup = (groupId) => {
......@@ -136,7 +139,7 @@
</option>
{#each $groups as group}
<option value={group._id}>
{group.name}
{getGroupName(group)}
</option>
{/each}
</select>
......@@ -147,7 +150,7 @@
<div class="tags">
{#each $selectedGroups as group}
<span class="tag is-medium is-primary">
{group.name}
{getGroupName(group)}
<button on:click={() => handleRemoveGroup(group._id)} class="delete is-small" />
</span>
{/each}
......
......@@ -15,6 +15,7 @@
import { router } from 'tinro';
import StepBar from '../../components/common/StepBar.svelte';
import { POLLS_TYPES } from '../../../utils/enums';
import getGroupName from '/imports/utils/functions/groups';
import PackageJSON from '../../../../package.json';
let version = PackageJSON.version;
......@@ -22,6 +23,7 @@
let selectedGroups;
let titleOk = false;
let dateOk = false;
let slotsOk = $newPollStore.allDay ? true : false;
$: selectedGroups = useTracker(() =>
Groups.find({ _id: { $in: $newPollStore.groups } }, { sort: { name: -1 } }).fetch(),
......@@ -37,11 +39,21 @@
? true
: $newPollStore.dates.filter((date) => date.slots.length === 0).length === 0
: $newPollStore.meetingSlots.length > 0;
// check that there are no duplicates in time slots
if (!$newPollStore.allDay) {
let datesOk = true;
$newPollStore.dates.forEach((day) => {
let uniqSlots = new Set(day.slots);
if (uniqSlots.size != day.slots.length) datesOk = false;
});
if (datesOk) slotsOk = true;
}
const validatePollEdition = () => {
if (!titleOk) toast.push($_('pages.new_poll_4.missingTitle'), toasts.error);
if (!dateOk) toast.push($_('pages.new_poll_4.missingDate'), toasts.error);
if (titleOk && dateOk) {
if (!slotsOk) toast.push($_('pages.new_poll_4.duplicateSlots'), toasts.error);
if (titleOk && dateOk && slotsOk) {
Meteor.call(
meta.params._id ? 'polls.update' : 'polls.create',
{
......@@ -115,7 +127,7 @@
<div class="tags">
{#each $selectedGroups as group}
<span class="tag is-medium is-primary">
{group.name}
{getGroupName(group)}
</span>
{/each}
</div>
......@@ -203,7 +215,9 @@
</div>
<div class="column is-half-desktop is-full-mobile">
<BigLink
link={meta.params._id ? ROUTES.EDIT_POLL_RM(meta.params._id, 3) : ROUTES.NEW_POLL_RM(3)}
link={meta.params._id
? ROUTES.EDIT_POLL_RM(meta.params._id, $newPollStore.type === POLLS_TYPES.MEETING ? 2 : 3)
: ROUTES.NEW_POLL_RM($newPollStore.type === POLLS_TYPES.MEETING ? 2 : 3)}
text={$_('pages.new_poll.previous')}
color="is-secondary"
/>
......