working calendar ranges with a subset of features

This commit is contained in:
Julien Fastré 2022-06-24 17:24:56 +02:00
parent a845fddf2e
commit 75b2f6419e
7 changed files with 266 additions and 42 deletions

View File

@ -77,7 +77,6 @@ export const calendarRangeToFullCalendarEvent = (entity: CalendarRange): EventIn
} }
export const remoteToFullCalendarEvent = (entity: CalendarRemote): EventInput & {id: string} => { export const remoteToFullCalendarEvent = (entity: CalendarRemote): EventInput & {id: string} => {
console.log(entity);
return { return {
id: `range_${entity.id}`, id: `range_${entity.id}`,
title: entity.title, title: entity.title,

View File

@ -1,34 +1,72 @@
<template> <template>
<p>Il y a {{ranges.length}} plages</p> <p>Il y a {{ 'eventSources[0].length' }} plages</p>
<FullCalendar :options="calendarOptions"></FullCalendar> <div class="display-options row justify-content-between">
<div class="col-sm col-xs-12">
<div class="input-group mb-3">
<label class="input-group-text" for="slotDuration">Durée des créneaux</label>
<select v-model="slotDuration" id="slotDuration" class="form-select">
<option value="00:05:00">5 minutes</option>
<option value="00:10:00">10 minutes</option>
<option value="00:15:00">15 minutes</option>
<option value="00:30:00">30 minutes</option>
</select>
</div>
</div>
<div class="col-sm col-xs-12">
<div class="float-end">
<div class="input-group mb-3">
<div class="input-group-text">
<input id="showHideWE" class="form-check-input mt-0" type="checkbox" v-model="showWeekends">
<label for="showHideWE" class="form-check-label"> Masquer les week-ends</label>
</div>
</div>
</div>
</div>
</div>
<FullCalendar :options="calendarOptions" ref="calendarRef">
<template v-slot:eventContent='arg'>
<span :class="eventClasses(arg)">
<b v-if="arg.event.extendedProps.is === 'remote'">{{ arg.event.title}}</b>
<b v-else-if="arg.event.extendedProps.is === 'range'">{{ arg.timeText }}</b>
<b v-else >no 'is'</b>
<a v-if="arg.event.extendedProps.is === 'range'" class="fa fa-fw fa-times"
@click.prevent="onClickDelete(arg.event)">
</a>
</span>
</template>
</FullCalendar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {CalendarOptions, DatesSetArg, EventSourceInput, EventInput} from '@fullcalendar/vue3'; import type {CalendarOptions, DatesSetArg, EventSourceInput, EventInput} from '@fullcalendar/vue3';
import {reactive, computed, ref} from "vue"; import {reactive, computed, ref, watch} from "vue";
import type {DebuggerEvent} from "vue"; import {useStore} from "vuex";
import {mapGetters, useStore} from "vuex";
import {key} from './store'; import {key} from './store';
import '@fullcalendar/core/vdom'; // solves problem with Vite import '@fullcalendar/core/vdom'; // solves problem with Vite
import frLocale from '@fullcalendar/core/locales/fr'; import frLocale from '@fullcalendar/core/locales/fr';
import FullCalendar from '@fullcalendar/vue3'; import FullCalendar from '@fullcalendar/vue3';
import interactionPlugin from "@fullcalendar/interaction"; import interactionPlugin from "@fullcalendar/interaction";
import timeGridPlugin from "@fullcalendar/timegrid"; import timeGridPlugin from "@fullcalendar/timegrid";
import {EventApi} from "@fullcalendar/core";
const store = useStore(key); const store = useStore(key);
const baseOptions = reactive<CalendarOptions>({ const showWeekends = ref(true);
const slotDuration = ref('00:15:00');
// will work as long as calendar-vue is not changed
const calendarRef = ref<{renderId: number}|null>(null);
const baseOptions = ref<CalendarOptions>({
locale: frLocale, locale: frLocale,
plugins: [interactionPlugin, timeGridPlugin], plugins: [interactionPlugin, timeGridPlugin],
initialView: 'timeGridWeek', initialView: 'timeGridWeek',
initialDate: new Date(), initialDate: new Date(),
selectable: true, selectable: true,
datesSet: onDatesSet, datesSet: onDatesSet,
eventSources: [],
selectMirror: false, selectMirror: false,
editable: true, editable: true,
slotDuration: '00:15:00',
slotMinTime: "08:00:00", slotMinTime: "08:00:00",
slotMaxTime: "19:00:00", slotMaxTime: "19:00:00",
headerToolbar: { headerToolbar: {
@ -42,19 +80,24 @@ const ranges = computed<EventInput[]>(() => {
return store.state.calendarRanges.ranges; return store.state.calendarRanges.ranges;
}); });
/**
* return the show classes for the event
* @param arg
*/
const eventClasses = function(arg: EventApi): object {
console.log('eventClasses', arg);
return {'calendarRangeItems': true};
}
/*
// currently, all events are stored into calendarRanges, due to reactivity bug
const remotes = computed<EventInput[]>(() => {
return store.state.calendarRemotes.remotes;
});
const sources = computed<EventSourceInput[]>(() => { const sources = computed<EventSourceInput[]>(() => {
const sources = []; const sources = [];
const dummy: EventSourceInput = {
id: 'dummy',
events: [
{ id: '-1', start: '2022-06-23T12:00:00+02:00', end: '2022-06-23T14:00:00+02:00'},
{ id: '-2', start: '2022-06-24T12:00:00+02:00', end: '2022-06-24T14:00:00+02:00'},
]
}
sources.push(dummy);
const rangeSource: EventSourceInput = { const rangeSource: EventSourceInput = {
id: 'ranges', id: 'ranges',
events: ranges.value, events: ranges.value,
@ -64,26 +107,32 @@ const sources = computed<EventSourceInput[]>(() => {
return sources; return sources;
}); });
*/
const calendarOptions = computed((): CalendarOptions => { const calendarOptions = computed((): CalendarOptions => {
const o = { return {
...baseOptions, ...baseOptions.value,
weekends: showWeekends.value,
slotDuration: slotDuration.value,
events: ranges.value, events: ranges.value,
eventSources: sources.value,
}; };
console.log('calendarOptions', o);
return o;
}); });
/**
* launched when the calendar range date change
*/
function onDatesSet(event: DatesSetArg) { function onDatesSet(event: DatesSetArg) {
store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end}) store.dispatch('fullCalendar/setCurrentDatesView', {start: event.start, end: event.end});
.then(source => {
console.log('onDatesSet finished');
//this.eventSources.push(source);
});
} }
/**
* When a calendar range is deleted
*/
function onClickDelete(event: EventApi) {
console.log('onClickDelete', event);
}
</script> </script>
<style scoped> <style scoped>

View File

@ -7,6 +7,7 @@ import {InjectionKey} from "vue";
import me, {MeState} from './modules/me'; import me, {MeState} from './modules/me';
import fullCalendar, {FullCalendarState} from './modules/fullcalendar'; import fullCalendar, {FullCalendarState} from './modules/fullcalendar';
import calendarRanges, {CalendarRangesState} from './modules/calendarRanges'; import calendarRanges, {CalendarRangesState} from './modules/calendarRanges';
import calendarRemotes, {CalendarRemotesState} from './modules/calendarRemotes';
import {whoami} from 'ChillCalendarAssets/vuejs/Calendar/api'; import {whoami} from 'ChillCalendarAssets/vuejs/Calendar/api';
import {User} from 'ChillMainAssets/types'; import {User} from 'ChillMainAssets/types';
@ -21,7 +22,8 @@ export interface State {
*/ */
//key: number, //key: number,
calendarRanges: CalendarRangesState, calendarRanges: CalendarRangesState,
fullCalendar: FullCalendarState calendarRemotes: CalendarRemotesState,
fullCalendar: FullCalendarState,
} }
export const key: InjectionKey<Store<State>> = Symbol(); export const key: InjectionKey<Store<State>> = Symbol();
@ -40,6 +42,7 @@ const futureStore = function(): Promise<Store<State>> {
me, me,
fullCalendar, fullCalendar,
calendarRanges, calendarRanges,
calendarRemotes,
}, },
mutations: { mutations: {
increaseKey(state: State) { increaseKey(state: State) {

View File

@ -39,6 +39,18 @@ export default <Module<CalendarRangesState, State>> {
const toAdd = ranges const toAdd = ranges
.map(cr => calendarRangeToFullCalendarEvent(cr)) .map(cr => calendarRangeToFullCalendarEvent(cr))
.map(cr => ({...cr, backgroundColor: 'white', borderColor:'#3788d8',
textColor: 'black'}))
.filter(r => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => {
state.rangesIndex.add(r.id);
state.ranges.push(r);
});
state.key = state.key + toAdd.length;
},
addExternals(state, externalEvents: (EventInput & {id: string})[] ) {
const toAdd = externalEvents
.filter(r => !state.rangesIndex.has(r.id)); .filter(r => !state.rangesIndex.has(r.id));
toAdd.forEach((r) => { toAdd.forEach((r) => {
@ -83,11 +95,11 @@ export default <Module<CalendarRangesState, State>> {
console.log('me is not there'); console.log('me is not there');
return Promise.resolve(ctx.getters.getRangeSource); return Promise.resolve(ctx.getters.getRangeSource);
} }
/*
if (ctx.getters.isRangeLoaded({start, end})) { if (ctx.getters.isRangeLoaded({start, end})) {
console.log('range already loaded'); console.log('range already loaded');
return Promise.resolve(ctx.getters.getRangeSource); return Promise.resolve(ctx.getters.getRangeSource);
}*/ }
ctx.commit('addLoaded', { ctx.commit('addLoaded', {
start: start, start: start,

View File

@ -0,0 +1,120 @@
import {State} from './../index';
import {ActionContext, Module} from 'vuex';
import {CalendarRemote} from 'ChillCalendarAssets/types';
import {fetchCalendarRemoteForUser} from 'ChillCalendarAssets/vuejs/Calendar/api';
import {calendarRangeToFullCalendarEvent} from 'ChillCalendarAssets/vuejs/Calendar/store/utils';
import {EventInput, EventSource} from '@fullcalendar/vue3';
import {remoteToFullCalendarEvent} from "../../../Calendar/store/utils";
import {TransportExceptionInterface} from "ChillMainAssets/lib/api/apiMethods";
import {COLORS} from "ChillCalendarAssets/vuejs/Calendar/const";
export interface CalendarRemotesState {
remotes: EventInput[],
remotesLoaded: {start: number, end: number}[],
remotesIndex: Set<string>,
key: number
}
type Context = ActionContext<CalendarRemotesState, State>;
export default <Module<CalendarRemotesState, State>> {
namespaced: true,
state: (): CalendarRemotesState => ({
remotes: [],
remotesLoaded: [],
remotesIndex: new Set<string>(),
key: 0
}),
getters: {
isRemotesLoaded: (state: CalendarRemotesState) => ({start, end}: {start: Date, end: Date}): boolean => {
for (let range of state.remotesLoaded) {
if (start.getTime() === range.start && end.getTime() === range.end) {
return true;
}
}
return false;
},
},
mutations: {
addRemotes(state: CalendarRemotesState, ranges: CalendarRemote[]) {
console.log('addRemotes', ranges);
const toAdd = ranges
.map(cr => remoteToFullCalendarEvent(cr))
.filter(r => !state.remotesIndex.has(r.id));
toAdd.forEach((r) => {
state.remotesIndex.add(r.id);
state.remotes.push(r);
});
state.key = state.key + toAdd.length;
},
addLoaded(state: CalendarRemotesState, payload: {start: Date, end: Date}) {
state.remotesLoaded.push({start: payload.start.getTime(), end: payload.end.getTime()});
},/*
setRangesToCopy(state: State, payload: CalendarRange[]) {
state.rangesToCopy = payload
},*/
addRemote(state: CalendarRemotesState, payload: CalendarRemote) {
const asEvent = remoteToFullCalendarEvent(payload);
state.remotes.push(asEvent);
state.remotesIndex.add(asEvent.id);
state.key = state.key + 1;
},
removeRemote(state: CalendarRemotesState, payload: EventInput) {
/*
state.ranges = state.ranges.filter(
(r) => r.id !== payload.id
);
if (typeof payload.id === "string") {
state.rangesIndex.delete(payload.id);
}
state.key = state.key + 1;
*/
},
},
actions: {
fetchRemotes(ctx: Context, payload: {start: Date, end: Date}): Promise<null> {
console.log('fetchRanges', payload);
const start = payload.start;
const end = payload.end;
if (ctx.rootGetters['me/getMe'] === null) {
console.log('me is not there');
return Promise.resolve(null);
}
if (ctx.getters.isRemotesLoaded({start, end})) {
console.log('range already loaded');
return Promise.resolve(ctx.getters.getRangeSource);
}
ctx.commit('addLoaded', {
start: start,
end: end,
});
return fetchCalendarRemoteForUser(
ctx.rootGetters['me/getMe'],
start,
end
)
.then((remotes: CalendarRemote[]) => {
//ctx.commit('addRemotes', remotes);
const inputs = remotes
.map(cr => remoteToFullCalendarEvent(cr))
.map(cr => ({...cr, backgroundColor: COLORS[0], textColor: 'black', editable: false}))
ctx.commit('calendarRanges/addExternals', inputs, {root: true});
return Promise.resolve(null);
})
.catch((e: TransportExceptionInterface) => {
console.error(e);
return Promise.resolve(null);
});
}
}
};

View File

@ -38,8 +38,13 @@ export default {
} }
if (start !== null && end !== null) { if (start !== null && end !== null) {
console.log('start, end', {start, end});
return ctx.dispatch('calendarRanges/fetchRanges', {start, end}, {root: true}).then(_ => Promise.resolve(null)); return Promise.all([
ctx.dispatch('calendarRanges/fetchRanges', {start, end}, {root: true}).then(_ => Promise.resolve(null)),
ctx.dispatch('calendarRemotes/fetchRemotes', {start, end}, {root: true}).then(_ => Promise.resolve(null))
]
).then(_ => Promise.resolve(null));
} else { } else {
return Promise.resolve(null); return Promise.resolve(null);
} }

View File

@ -20,7 +20,11 @@ export interface FetchParams {
[K: string]: string|number|null; [K: string]: string|number|null;
}; };
export interface ValidationExceptionInterface { export interface TransportExceptionInterface {
name: string;
}
export interface ValidationExceptionInterface extends TransportExceptionInterface {
name: 'ValidationException'; name: 'ValidationException';
error: object; error: object;
violations: string[]; violations: string[];
@ -28,18 +32,27 @@ export interface ValidationExceptionInterface {
propertyPaths: string[]; propertyPaths: string[];
} }
export interface ValidationErrorResponse { export interface ValidationErrorResponse extends TransportExceptionInterface {
violations: { violations: {
title: string; title: string;
propertyPath: string; propertyPath: string;
}[]; }[];
} }
export interface AccessExceptionInterface { export interface AccessExceptionInterface extends TransportExceptionInterface {
name: 'AccessException'; name: 'AccessException';
violations: string[]; violations: string[];
} }
export interface NotFoundExceptionInterface extends TransportExceptionInterface {
name: 'NotFoundException';
}
export interface ServerExceptionInterface extends TransportExceptionInterface {
name: 'ServerException';
message: string;
}
/** /**
* Generic api method that can be adapted to any fetch request * Generic api method that can be adapted to any fetch request
@ -115,8 +128,16 @@ function _fetchAction<T>(page: number, uri: string, params?: FetchParams): Promi
}, },
}).then((response) => { }).then((response) => {
if (response.ok) { return response.json(); } if (response.ok) { return response.json(); }
throw new Error(response.statusText); if (response.status === 404) {
}); throw NotFoundException(response);
}
console.error(response);
throw ServerException();
}).catch((reason: any) => {
console.error(reason);
throw ServerException();
});
}; };
export const fetchResults = async<T> (uri: string, params?: FetchParams): Promise<T[]> => { export const fetchResults = async<T> (uri: string, params?: FetchParams): Promise<T[]> => {
@ -130,7 +151,8 @@ export const fetchResults = async<T> (uri: string, params?: FetchParams): Promis
do { do {
page = ++page; page = ++page;
promises.push( promises.push(
_fetchAction<T>(page, uri, params).then(r => Promise.resolve(r.results)) _fetchAction<T>(page, uri, params)
.then(r => Promise.resolve(r.results))
); );
} while (page * firstData.pagination.items_per_page < firstData.count) } while (page * firstData.pagination.items_per_page < firstData.count)
} }
@ -162,3 +184,17 @@ const AccessException = (response: Response): AccessExceptionInterface => {
return error; return error;
} }
const NotFoundException = (response: Response): NotFoundExceptionInterface => {
const error = {} as NotFoundExceptionInterface;
error.name = 'NotFoundException';
return error;
}
const ServerException = (): ServerExceptionInterface => {
const error = {} as ServerExceptionInterface;
error.name = 'ServerException';
return error;
}