Better handle datetime

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-10-14 19:29:18 +02:00
parent cc701f8994
commit d93561742a
No known key found for this signature in database
GPG Key ID: A061B9DDE0CA0773
17 changed files with 168 additions and 48 deletions

View File

@ -19,11 +19,18 @@
expanded
:first-day-of-week="parseInt($t('firstDayOfWeek'), 10)"
:min-date="minDate"
v-model="date"
v-model="dateWithoutTime"
:placeholder="$t('Click to select')"
icon="calendar"
/>
<b-input expanded type="time" required v-model="time" />
<b-timepicker
placeholder="Type or select a time..."
icon="clock"
v-model="dateWithTime"
expanded
size="is-small"
inline>
</b-timepicker>
</b-field>
</template>
<script lang="ts">
@ -53,40 +60,26 @@ export default class DateTimePicker extends Vue {
*/
@Prop({ required: false, type: Date, default: null }) minDate!: Date;
date: Date = this.value;
time: string = '00:00';
dateWithoutTime: Date = this.value;
dateWithTime: Date = this.dateWithoutTime;
localeShortWeekDayNamesProxy = localeShortWeekDayNames();
localeMonthNamesProxy = localeMonthNames();
mounted() {
this.convertTime();
@Watch('value')
updateValue() {
this.dateWithoutTime = this.value;
this.dateWithTime = this.dateWithoutTime;
}
convertTime() {
let minutes = this.date.getHours() * 60 + this.date.getMinutes();
minutes = Math.ceil(minutes / this.step) * this.step;
this.time = [Math.floor(minutes / 60), minutes % 60].map((v) => { return v < 10 ? `0${v}` : v; }).join(':');
}
@Watch('time')
updateTime(time: string) {
const [hours, minutes] = time.split(':', 2);
this.date.setHours(parseInt(hours, 10));
this.date.setMinutes(parseInt(minutes, 10));
@Watch('dateWithoutTime')
updateDateWithoutTimeWatcher() {
this.updateDateTime();
}
@Watch('date')
updateDate() {
this.updateTime(this.time);
}
@Watch('value')
updateValue() {
this.date = this.value;
this.convertTime();
@Watch('dateWithTime')
updateDateWithTimeWatcher() {
this.updateDateTime();
}
updateDateTime() {
@ -95,7 +88,17 @@ export default class DateTimePicker extends Vue {
*
* @type {Date}
*/
this.$emit('input', this.date);
this.dateWithoutTime.setHours(this.dateWithTime.getHours());
this.dateWithoutTime.setMinutes(this.dateWithTime.getMinutes());
this.$emit('input', this.dateWithoutTime);
}
}
</script>
<style lang="scss" scoped>
.timepicker {
/deep/ .dropdown-content {
padding: 0;
}
}
</style>

View File

@ -18,14 +18,31 @@
</docs>
<template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span>
<span v-else-if="isSameDay()">
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
<span v-else-if="isSameDay() && showStartTime && showEndTime">
{{ $t('On {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }}
</span>
<span v-else-if="endsOn">
<span v-else-if="isSameDay() && !showStartTime && showEndTime">
{{ $t('On {date} ending at {endTime}', {date: formatDate(beginsOn), endTime: formatTime(endsOn)}) }}
</span>
<span v-else-if="isSameDay() && showStartTime && !showEndTime">
{{ $t('On {date} starting at {startTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn)}) }}
</span>
<span v-else-if="isSameDay()">
{{ $t('On {date}', {date: formatDate(beginsOn)}) }}
</span>
<span v-else-if="endsOn && showStartTime && showEndTime">
{{ $t('From the {startDate} at {startTime} to the {endDate} at {endTime}',
{startDate: formatDate(beginsOn), startTime: formatTime(beginsOn), endDate: formatDate(endsOn), endTime: formatTime(endsOn)}) }}
</span>
<span v-else-if="endsOn && showStartTime">
{{ $t('From the {startDate} at {startTime} to the {endDate}',
{startDate: formatDate(beginsOn), startTime: formatTime(beginsOn), endDate: formatDate(endsOn)}) }}
</span>
<span v-else-if="endsOn">
{{ $t('From the {startDate} to the {endDate}',
{startDate: formatDate(beginsOn), endDate: formatDate(endsOn)}) }}
</span>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@ -34,6 +51,8 @@ import { Component, Prop, Vue } from 'vue-property-decorator';
export default class EventFullDate extends Vue {
@Prop({ required: true }) beginsOn!: string;
@Prop({ required: false }) endsOn!: string;
@Prop({ required: false, default: true }) showStartTime!: boolean;
@Prop({ required: false, default: true }) showEndTime!: boolean;
formatDate(value) {
if (!this.$options.filters) return;

View File

@ -10,8 +10,13 @@ function formatTimeString(value: string): string {
return parseDateTime(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' });
}
function formatDateTimeString(value: string): string {
return parseDateTime(value).toLocaleTimeString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
function formatDateTimeString(value: string, showTime: boolean = true): string {
const options = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
if (showTime) {
options.hour = 'numeric';
options.minute = 'numeric';
}
return parseDateTime(value).toLocaleTimeString(undefined, options);
}

View File

@ -38,6 +38,8 @@ const optionsQuery = `
maximumAttendeeCapacity,
remainingAttendeeCapacity,
showRemainingAttendeeCapacity,
showStartTime,
showEndTime,
offers {
price,
priceCurrency,

View File

@ -59,6 +59,8 @@
"Create": "Create",
"Creator": "Creator",
"Current identity has been changed to {identityName} in order to manage this event.": "Current identity has been changed to {identityName} in order to manage this event.",
"Date and time settings": "Date and time settings",
"Date parameters": "Date parameters",
"Delete event": "Delete event",
"Delete this identity": "Delete this identity",
"Delete your identity": "Delete your identity",
@ -88,6 +90,7 @@
"Event edition": "Event edition",
"Event list": "Event list",
"Event not found.": "Event not found.",
"Event page settings": "Event page settings",
"Event to be confirmed": "Event to be confirmed",
"Event {eventTitle} deleted": "Event {eventTitle} deleted",
"Event {eventTitle} reported": "Event {eventTitle} reported",
@ -103,6 +106,8 @@
"Forgot your password ?": "Forgot your password ?",
"From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?": "From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?",
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "From the {startDate} at {startTime} to the {endDate} at {endTime}",
"From the {startDate} at {startTime} to the {endDate}": "From the {startDate} at {startTime} to the {endDate}",
"From the {startDate} to the {endDate}": "From the {startDate} to the {endDate}",
"Gather ⋅ Organize ⋅ Mobilize": "Gather ⋅ Organize ⋅ Mobilize",
"General information": "General information",
"Going as {name}": "Going as {name}",
@ -150,14 +155,19 @@
"Name": "Name",
"New password": "New password",
"No address defined": "No address defined",
"No end date": "No end date",
"No events found": "No events found",
"No group found": "No group found",
"No groups found": "No groups found",
"No participants yet": "No participants yet",
"No results for \"{queryText}\"": "No results for \"{queryText}\"",
"Number of places": "Number of places",
"OK": "OK",
"Old password": "Old password",
"On {date} ending at {endTime}": "On {date} ending at {endTime}",
"On {date} from {startTime} to {endTime}": "On {date} from {startTime} to {endTime}",
"On {date} starting at {startTime}": "On {date} starting at {startTime}",
"On {date}": "On {date}",
"One person is going": "No one is going | One person is going | {approved} persons are going",
"Only accessible through link and search (private)": "Only accessible through link and search (private)",
"Opened reports": "Opened reports",
@ -222,6 +232,8 @@
"Share this event": "Share this event",
"Show map": "Show map",
"Show remaining number of places": "Show remaining number of places",
"Show the time when the event begins": "Show the time when the event begins",
"Show the time when the event ends": "Show the time when the event ends",
"Sign up": "Sign up",
"Software to the people": "Software to the people",
"Starts on…": "Starts on…",

View File

@ -59,6 +59,8 @@
"Create": "Créer",
"Creator": "Créateur",
"Current identity has been changed to {identityName} in order to manage this event.": "L'identité actuelle a été changée à {identityName} pour pouvoir gérer cet événement.",
"Date and time settings": "Paramètres de date et d'heure",
"Date parameters": "Paramètres de date",
"Delete event": "Supprimer un événement",
"Delete this identity": "Supprimer cette identité",
"Delete your identity": "Supprimer votre identité",
@ -88,6 +90,7 @@
"Event edition": "Édition d'événement",
"Event list": "Liste d'événements",
"Event not found.": "Événement non trouvé.",
"Event page settings": "Paramètres de la page de l'événement",
"Event to be confirmed": "Événement à confirmer",
"Event {eventTitle} deleted": "Événement {eventTitle} supprimé",
"Event {eventTitle} reported": "Événement {eventTitle} signalé",
@ -103,6 +106,8 @@
"Forgot your password ?": "Mot de passe oublié ?",
"From a birthday party with friends and family to a march for climate change, right now, our gatherings are <b>trapped inside the tech giants platforms</b>. How can we organize, how can we click “Attend,” without <b>providing private data</b> to Facebook or <b>locking ourselves up</b> inside MeetUp?": "De lanniversaire entre ami·e·s à une marche pour le climat, aujourdhui, les bonnes raisons de se rassembler sont <b>captées par les géants du web</b>. Comment sorganiser, comment cliquer sur «je participe» sans <b>livrer des données intimes</b> à Facebook ou<b> senfermer</b> dans MeetUp?",
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
"From the {startDate} at {startTime} to the {endDate}": "Du {startDate} à {startTime} jusqu'au {endDate}",
"From the {startDate} to the {endDate}": "Du {startDate} au {endDate}",
"Gather ⋅ Organize ⋅ Mobilize": "Rassembler ⋅ Organiser ⋅ Mobiliser",
"General information": "Information générales",
"Going as {name}": "En tant que {name}",
@ -150,14 +155,19 @@
"Name": "Nom",
"New password": "Nouveau mot de passe",
"No address defined": "Aucune adresse définie",
"No end date": "Pas de date de fin",
"No events found": "Aucun événement trouvé",
"No group found": "Aucun groupe trouvé",
"No groups found": "Aucun groupe trouvé",
"No participants yet": "Aucun⋅e participant⋅e pour le moment",
"No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
"Number of places": "Nombre de places",
"OK": "OK",
"Old password": "Ancien mot de passe",
"On {date} ending at {endTime}": "Le {date}, se terminant à {endTime}",
"On {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
"On {date} starting at {startTime}": "Le {date} à partir de {startTime}",
"On {date}": "Le {date}",
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
"Opened reports": "Signalements ouverts",
@ -222,6 +232,8 @@
"Share this event": "Partager l'événement",
"Show map": "Afficher la carte",
"Show remaining number of places": "Afficher le nombre de places restantes",
"Show the time when the event begins": "Afficher l'heure de début de l'événement",
"Show the time when the event ends": "Afficher l'heure de fin de l'événement",
"Sign up": "S'enregistrer",
"Software to the people": "Software to the people",
"Starts on…": "Débute le…",

View File

@ -147,6 +147,8 @@ export interface IEventOptions {
program: string;
commentModeration: CommentModeration;
showParticipationPrice: boolean;
showStartTime: boolean;
showEndTime: boolean;
}
export class EventOptions implements IEventOptions {
@ -159,6 +161,8 @@ export class EventOptions implements IEventOptions {
program = '';
commentModeration = CommentModeration.ALLOW_ALL;
showParticipationPrice = false;
showStartTime = true;
showEndTime = true;
}
export class EventModel implements IEvent {

View File

@ -21,8 +21,10 @@
<tag-input v-model="event.tags" :data="tags" path="title" />
<date-time-picker v-model="event.beginsOn" :label="$t('Starts on…')" :step="15"/>
<date-time-picker :min-date="minDateForEndsOn" v-model="event.endsOn" :label="$t('Ends on…')" :step="15" />
<date-time-picker v-model="event.beginsOn" :label="$t('Starts on…')" />
<date-time-picker :min-date="minDateForEndsOn" v-model="event.endsOn" :label="$t('Ends on…')" />
<!-- <b-switch v-model="endsOnNull">{{ $t('No end date') }}</b-switch>-->
<b-button type="is-text" @click="dateSettingsIsOpen = true">{{ $t('Date parameters')}}</b-button>
<address-auto-complete v-model="event.physicalAddress" />
@ -166,6 +168,31 @@
</form>
</div>
</div>
<b-modal :active.sync="dateSettingsIsOpen" has-modal-card trap-focus>
<form action="">
<div class="modal-card" style="width: auto">
<header class="modal-card-head">
<p class="modal-card-title">{{ $t('Date and time settings') }}</p>
</header>
<section class="modal-card-body">
<b-field :label="$t('Event page settings')">
<b-switch v-model="event.options.showStartTime">
{{ $t('Show the time when the event begins') }}
</b-switch>
</b-field>
<b-field>
<b-switch v-model="event.options.showEndTime">
{{ $t('Show the time when the event ends') }}
</b-switch>
</b-field>
</section>
<footer class="modal-card-foot">
<button class="button" type="button" @click="dateSettingsIsOpen = false">{{ $t('OK') }}</button>
</footer>
</div>
</form>
</b-modal>
<span ref="bottomObserver"></span>
<nav role="navigation" aria-label="main navigation" class="navbar" :class="{'is-fixed-bottom': showFixedNavbar }">
<div class="container">
@ -302,6 +329,8 @@ export default class EditEvent extends Vue {
CommentModeration = CommentModeration;
showFixedNavbar: boolean = true;
observer!: IntersectionObserver;
dateSettingsIsOpen: boolean = false;
endsOnNull: boolean = false;
// categories: string[] = Object.keys(Category);
@ -505,6 +534,10 @@ export default class EditEvent extends Vue {
delete this.event.physicalAddress['__typename'];
}
if (this.endsOnNull) {
res.endsOn = null;
}
const pictureObj = buildFileVariable(this.pictureFile, 'picture');
res = Object.assign({}, res, pictureObj);
@ -527,6 +560,9 @@ export default class EditEvent extends Vue {
},
});
if (result.data.event.endsOn === null) {
this.endsOnNull = true;
}
return new EventModel(result.data.event);
}
@ -588,17 +624,17 @@ export default class EditEvent extends Vue {
get beginsOn() { return this.event.beginsOn; }
@Watch('beginsOn')
@Watch('beginsOn', { deep: true })
onBeginsOnChanged(beginsOn) {
if (!this.event.endsOn) return;
const dateBeginsOn = new Date(beginsOn);
const dateEndsOn = new Date(this.event.endsOn);
if (dateEndsOn < dateBeginsOn) {
this.event.endsOn = dateBeginsOn;
this.event.endsOn.setUTCHours(dateEndsOn.getUTCHours());
this.event.endsOn.setHours(dateEndsOn.getHours());
}
if (dateEndsOn === dateBeginsOn) {
this.event.endsOn.setUTCHours(dateEndsOn.getUTCHours() + 1);
this.event.endsOn.setHours(dateEndsOn.getHours() + 1);
}
}

View File

@ -64,7 +64,7 @@ import {ParticipantRole} from "@/types/event.model";
<div class="date-and-add-to-calendar">
<div class="date-and-privacy" v-if="event.beginsOn">
<b-icon icon="calendar-clock" />
<event-full-date :beginsOn="event.beginsOn" :endsOn="event.endsOn" />
<event-full-date :beginsOn="event.beginsOn" :show-start-time="event.options.showStartTime" :show-end-time="event.options.showEndTime" :endsOn="event.endsOn" />
</div>
<a class="add-to-calendar" @click="downloadIcsEvent()" v-if="!event.draft">
<b-icon icon="calendar-plus" />

View File

@ -22,7 +22,9 @@ defmodule Mobilizon.Events.EventOptions do
comment_moderation: CommentModeration.t(),
show_participation_price: boolean,
offers: [EventOffer.t()],
participation_condition: [EventParticipationCondition.t()]
participation_condition: [EventParticipationCondition.t()],
show_start_time: boolean,
show_end_time: boolean
}
@attrs [
@ -32,7 +34,9 @@ defmodule Mobilizon.Events.EventOptions do
:attendees,
:program,
:comment_moderation,
:show_participation_price
:show_participation_price,
:show_start_time,
:show_end_time
]
@primary_key false
@ -45,6 +49,8 @@ defmodule Mobilizon.Events.EventOptions do
field(:program, :string)
field(:comment_moderation, CommentModeration)
field(:show_participation_price, :boolean)
field(:show_start_time, :boolean)
field(:show_end_time, :boolean)
embeds_many(:offers, EventOffer)
embeds_many(:participation_condition, EventParticipationCondition)

View File

@ -181,6 +181,9 @@ defmodule MobilizonWeb.Schema.EventType do
field(:show_participation_price, :boolean,
description: "Whether or not to show the participation price"
)
field(:show_start_time, :boolean, description: "Show event start time")
field(:show_end_time, :boolean, description: "Show event end time")
end
input_object :event_options_input do
@ -214,6 +217,9 @@ defmodule MobilizonWeb.Schema.EventType do
field(:show_participation_price, :boolean,
description: "Whether or not to show the participation price"
)
field(:show_start_time, :boolean, description: "Show event start time")
field(:show_end_time, :boolean, description: "Show event end time")
end
object :event_queries do

View File

@ -76,7 +76,7 @@
</td>
</tr>
<% end %>
<%= if MapSet.member?(@changes, :ends_on) do %>
<%= if MapSet.member?(@changes, :ends_on) && !is_nil(@event.ends_on) do %>
<tr>
<td bgcolor="#ffffff" align="left">
<%= gettext "Ending of event" %>

View File

@ -12,7 +12,7 @@
<%= gettext "New date and time for start of event: %{begins_on}", begins_on: datetime_to_string(@event.begins_on, @locale) %>
<% end %>
<%= if MapSet.member?(@changes, :ends_on) do %>
<%= if MapSet.member?(@changes, :ends_on) && !is_nil(@event.ends_on) do %>
<%= gettext "New date and time for ending of event: %{ends_on}", ends_on: datetime_to_string(@event.ends_on, @locale) %>
<% end %>

View File

@ -138,7 +138,7 @@ defmodule Mobilizon.Service.ActivityPub.Converter.Event do
|> Enum.map(&Utils.camelize/1)
Enum.reduce(object, %{}, fn {key, value}, acc ->
(value && key in keys && Map.put(acc, Utils.underscore(key), value)) ||
(!is_nil(value) && key in keys && Map.put(acc, Utils.underscore(key), value)) ||
acc
end)
end

View File

@ -346,7 +346,7 @@ defmodule Mobilizon.Service.ActivityPub.Utils do
options = Events.EventOptions |> struct(metadata.options) |> Map.from_struct()
Enum.reduce(options, res, fn {key, value}, acc ->
(value && Map.put(acc, camelize(key), value)) ||
(!is_nil(value) && Map.put(acc, camelize(key), value)) ||
acc
end)
end

View File

@ -1,5 +1,5 @@
# source: http://localhost:4000/api
# timestamp: Mon Oct 14 2019 10:27:57 GMT+0200 (Central European Summer Time)
# timestamp: Mon Oct 14 2019 19:26:36 GMT+0200 (Central European Summer Time)
schema {
query: RootQueryType
@ -400,11 +400,17 @@ type EventOptions {
"""The number of remaining seats for this event"""
remainingAttendeeCapacity: Int
"""Show event end time"""
showEndTime: Boolean
"""Whether or not to show the participation price"""
showParticipationPrice: Boolean
"""Whether or not to show the number of remaining seats for this event"""
showRemainingAttendeeCapacity: Boolean
"""Show event start time"""
showStartTime: Boolean
}
input EventOptionsInput {
@ -429,11 +435,17 @@ input EventOptionsInput {
"""The number of remaining seats for this event"""
remainingAttendeeCapacity: Int
"""Show event end time"""
showEndTime: Boolean
"""Whether or not to show the participation price"""
showParticipationPrice: Boolean
"""Whether or not to show the number of remaining seats for this event"""
showRemainingAttendeeCapacity: Boolean
"""Show event start time"""
showStartTime: Boolean
}
type EventParticipationCondition {

View File

@ -229,7 +229,8 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
category: "super_category",
options: {
maximumAttendeeCapacity: 30,
showRemainingAttendeeCapacity: true
showRemainingAttendeeCapacity: true,
showEndTime: false
}
) {
title,
@ -246,7 +247,8 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
category,
options {
maximumAttendeeCapacity,
showRemainingAttendeeCapacity
showRemainingAttendeeCapacity,
showEndTime
}
}
}
@ -273,6 +275,7 @@ defmodule MobilizonWeb.Resolvers.EventResolverTest do
assert event["category"] == "super_category"
assert event["options"]["maximumAttendeeCapacity"] == 30
assert event["options"]["showRemainingAttendeeCapacity"] == true
assert event["options"]["showEndTime"] == false
end
test "create_event/3 creates an event with tags", %{conn: conn, actor: actor, user: user} do