Merge branch 'feature/edit-event' into 'master'

Prepare front to edit events

See merge request framasoft/mobilizon!174
This commit is contained in:
Thomas Citharel 2019-09-02 15:26:39 +02:00
commit 7bfd85140b
10 changed files with 212 additions and 144 deletions

View File

@ -139,15 +139,15 @@ export const FETCH_EVENTS = gql`
export const CREATE_EVENT = gql` export const CREATE_EVENT = gql`
mutation CreateEvent( mutation CreateEvent(
$title: String!, $title: String!,
$description: String!, $description: String!,
$organizerActorId: ID!, $organizerActorId: ID!,
$category: String, $category: String,
$beginsOn: DateTime!, $beginsOn: DateTime!,
$picture: PictureInput, $picture: PictureInput,
$tags: [String], $tags: [String],
$physicalAddress: AddressInput, $physicalAddress: AddressInput,
$visibility: EventVisibility $visibility: EventVisibility
) { ) {
createEvent( createEvent(
title: $title, title: $title,

View File

@ -20,6 +20,7 @@ const language = (window.navigator as any).userLanguage || window.navigator.lang
Vue.use(GetTextPlugin, { Vue.use(GetTextPlugin, {
translations, translations,
defaultLanguage: 'en_US', defaultLanguage: 'en_US',
silent: true,
}); });
Vue.config.language = language.replace('-', '_'); Vue.config.language = language.replace('-', '_');

View File

@ -3,7 +3,7 @@ import Location from '@/views/Location.vue';
import { RouteConfig } from 'vue-router'; import { RouteConfig } from 'vue-router';
// tslint:disable:space-in-parens // tslint:disable:space-in-parens
const createEvent = () => import(/* webpackChunkName: "create-event" */ '@/views/Event/Create.vue'); const editEvent = () => import(/* webpackChunkName: "create-event" */ '@/views/Event/Edit.vue');
const event = () => import(/* webpackChunkName: "event" */ '@/views/Event/Event.vue'); const event = () => import(/* webpackChunkName: "event" */ '@/views/Event/Event.vue');
// tslint:enable // tslint:enable
@ -25,15 +25,15 @@ export const eventRoutes: RouteConfig[] = [
{ {
path: '/events/create', path: '/events/create',
name: EventRouteName.CREATE_EVENT, name: EventRouteName.CREATE_EVENT,
component: createEvent, component: editEvent,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
}, },
{ {
path: '/events/:id/edit', path: '/events/edit/:eventId',
name: EventRouteName.EDIT_EVENT, name: EventRouteName.EDIT_EVENT,
component: createEvent, component: editEvent,
props: true,
meta: { requiredAuth: true }, meta: { requiredAuth: true },
props: { isUpdate: true },
}, },
{ {
path: '/location/new', path: '/location/new',

View File

@ -1,7 +1,7 @@
import { Actor, IActor } from './actor'; import { Actor, IActor } from './actor';
import { IAddress } from '@/types/address.model'; import { IAddress } from '@/types/address.model';
import { ITag } from '@/types/tag.model'; import { ITag } from '@/types/tag.model';
import { IAbstractPicture, IPicture } from '@/types/picture.model'; import { IPicture } from '@/types/picture.model';
export enum EventStatus { export enum EventStatus {
TENTATIVE, TENTATIVE,
@ -53,10 +53,10 @@ export interface IEvent {
title: string; title: string;
slug: string; slug: string;
description: string; description: string;
category: Category|null; category: Category | null;
beginsOn: Date; beginsOn: Date;
endsOn: Date; endsOn: Date | null;
publishAt: Date; publishAt: Date;
status: EventStatus; status: EventStatus;
@ -64,7 +64,7 @@ export interface IEvent {
joinOptions: EventJoinOptions; joinOptions: EventJoinOptions;
picture: IAbstractPicture|null; picture: IPicture | null;
organizerActor: IActor; organizerActor: IActor;
attributedTo: IActor; attributedTo: IActor;
@ -79,27 +79,76 @@ export interface IEvent {
tags: ITag[]; tags: ITag[];
} }
export class EventModel implements IEvent { export class EventModel implements IEvent {
beginsOn: Date = new Date(); id?: number;
category: Category = Category.MEETING;
slug: string = ''; beginsOn = new Date();
description: string = ''; endsOn: Date | null = new Date();
endsOn: Date = new Date();
joinOptions: EventJoinOptions = EventJoinOptions.FREE; title = '';
local: boolean = true; url = '';
uuid = '';
slug = '';
description = '';
local = true;
onlineAddress: string | undefined = '';
phoneAddress: string | undefined = '';
physicalAddress?: IAddress;
picture: IPicture | null = null;
visibility = EventVisibility.PUBLIC;
category: Category | null = Category.MEETING;
joinOptions = EventJoinOptions.FREE;
status = EventStatus.CONFIRMED;
publishAt = new Date();
participants: IParticipant[] = []; participants: IParticipant[] = [];
publishAt: Date = new Date();
status: EventStatus = EventStatus.CONFIRMED;
title: string = '';
url: string = '';
uuid: string = '';
visibility: EventVisibility = EventVisibility.PUBLIC;
attributedTo: IActor = new Actor();
organizerActor: IActor = new Actor();
relatedEvents: IEvent[] = []; relatedEvents: IEvent[] = [];
onlineAddress: string = '';
phoneAddress: string = ''; attributedTo = new Actor();
picture: IAbstractPicture|null = null; organizerActor = new Actor();
tags: ITag[] = []; tags: ITag[] = [];
constructor(hash?: IEvent) {
if (!hash) return;
this.id = hash.id;
this.uuid = hash.uuid;
this.url = hash.url;
this.local = hash.local;
this.title = hash.title;
this.slug = hash.slug;
this.description = hash.description;
this.category = hash.category;
this.beginsOn = new Date(hash.beginsOn);
if (hash.endsOn) this.endsOn = new Date(hash.endsOn);
this.publishAt = new Date(hash.publishAt);
this.status = hash.status;
this.visibility = hash.visibility;
this.joinOptions = hash.joinOptions;
this.picture = hash.picture;
this.organizerActor = new Actor(hash.organizerActor);
this.attributedTo = new Actor(hash.attributedTo);
this.participants = hash.participants;
this.relatedEvents = hash.relatedEvents;
this.onlineAddress = hash.onlineAddress;
this.phoneAddress = hash.phoneAddress;
this.physicalAddress = hash.physicalAddress;
this.tags = hash.tags;
}
} }

View File

@ -1,16 +1,11 @@
export interface IAbstractPicture {
name;
alt;
}
export interface IPicture { export interface IPicture {
url; url: string;
name; name: string;
alt; alt: string;
} }
export interface IPictureUpload { export interface IPictureUpload {
file: File; file: File;
name: String; name: string;
alt: String|null; alt: string | null;
} }

24
js/src/utils/image.ts Normal file
View File

@ -0,0 +1,24 @@
import { IPicture } from '@/types/picture.model';
export async function buildFileFromIPicture(obj: IPicture | null) {
if (!obj) return null;
const response = await fetch(obj.url);
const blob = await response.blob();
return new File([blob], obj.name);
}
export function buildFileVariable<T>(file: File | null, name: string, alt?: string) {
if (!file) return {};
return {
[name]: {
picture: {
name: file.name,
alt: alt || file.name,
file,
},
},
};
}

5
js/src/utils/object.ts Normal file
View File

@ -0,0 +1,5 @@
export function buildObjectCollection<T, U>(collection: T[] | undefined, builder: (new (p: T) => U)) {
if (!collection || Array.isArray(collection) === false) return [];
return collection.map(v => new builder(v));
}

View File

@ -76,6 +76,7 @@ import PictureUpload from '@/components/PictureUpload.vue';
import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint'; import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint';
import { Dialog } from 'buefy/dist/components/dialog'; import { Dialog } from 'buefy/dist/components/dialog';
import { RouteName } from '@/router'; import { RouteName } from '@/router';
import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
@Component({ @Component({
components: { components: {
@ -113,7 +114,7 @@ export default class EditIdentity extends Vue {
if (this.identityName) { if (this.identityName) {
this.identity = await this.getIdentity(); this.identity = await this.getIdentity();
this.avatarFile = await this.getAvatarFileFromIdentity(this.identity); this.avatarFile = await buildFileFromIPicture(this.identity.avatar);
} }
} }
@ -259,15 +260,6 @@ export default class EditIdentity extends Vue {
return new Person(result.data.person); return new Person(result.data.person);
} }
private async getAvatarFileFromIdentity(identity: IPerson) {
if (!identity.avatar) return null;
const response = await fetch(identity.avatar.url);
const blob = await response.blob();
return new File([blob], identity.avatar.name);
}
private handleError(err: any) { private handleError(err: any) {
console.error(err); console.error(err);
@ -285,18 +277,7 @@ export default class EditIdentity extends Vue {
} }
private buildVariables() { private buildVariables() {
let avatarObj = {}; const avatarObj = buildFileVariable(this.avatarFile, 'avatar', `${this.identity.preferredUsername}'s avatar`);
if (this.avatarFile) {
avatarObj = {
avatar: {
picture: {
name: this.avatarFile.name,
alt: `${this.identity.preferredUsername}'s avatar`,
file: this.avatarFile,
},
},
};
}
return Object.assign({}, this.identity, avatarObj); return Object.assign({}, this.identity, avatarObj);
} }

View File

@ -1,14 +1,17 @@
<template> <template>
<section class="container"> <section class="container">
<h1 class="title"> <h1 class="title">
<translate>Create a new event</translate> <translate v-if="isUpdate === false">Create a new event</translate>
<translate v-else>Update event {{ event.name }}</translate>
</h1> </h1>
<div v-if="$apollo.loading">Loading...</div> <div v-if="$apollo.loading">Loading...</div>
<div class="columns is-centered" v-else> <div class="columns is-centered" v-else>
<form class="column is-two-thirds-desktop" @submit="createEvent"> <form class="column is-two-thirds-desktop" @submit="createOrUpdate">
<h2 class="subtitle"> <h2 class="subtitle">
<translate> <translate>
General informations General information
</translate> </translate>
</h2> </h2>
<picture-upload v-model="pictureFile" /> <picture-upload v-model="pictureFile" />
@ -46,22 +49,20 @@
</h2> </h2>
<label class="label">{{ $gettext('Event visibility') }}</label> <label class="label">{{ $gettext('Event visibility') }}</label>
<div class="field"> <div class="field">
<b-radio v-model="event.visibility" <b-radio v-model="event.visibility" name="name" :native-value="EventVisibility.PUBLIC">
name="name"
:native-value="EventVisibility.PUBLIC">
<translate>Visible everywhere on the web (public)</translate> <translate>Visible everywhere on the web (public)</translate>
</b-radio> </b-radio>
</div> </div>
<div class="field"> <div class="field">
<b-radio v-model="event.visibility" <b-radio v-model="event.visibility" name="name" :native-value="EventVisibility.PRIVATE">
name="name"
:native-value="EventVisibility.PRIVATE">
<translate>Only accessible through link and search (private)</translate> <translate>Only accessible through link and search (private)</translate>
</b-radio> </b-radio>
</div> </div>
<button class="button is-primary"> <button class="button is-primary">
<translate>Create my event</translate>
<translate v-if="isUpdate === false">Create my event</translate>
<translate v-else>Update my event</translate>
</button> </button>
</form> </form>
</div> </div>
@ -69,15 +70,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
// import Location from '@/components/Location'; import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
import { CREATE_EVENT, EDIT_EVENT } from '@/graphql/event'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { Component, Prop, Vue } from 'vue-property-decorator'; import { EventModel, EventVisibility, IEvent } from '@/types/event.model';
import {
Category,
IEvent,
EventModel,
EventVisibility,
} from '@/types/event.model';
import { LOGGED_PERSON } from '@/graphql/actor'; import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor'; import { IPerson, Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue'; import PictureUpload from '@/components/PictureUpload.vue';
@ -87,6 +82,7 @@ import TagInput from '@/components/Event/TagInput.vue';
import { TAGS } from '@/graphql/tags'; import { TAGS } from '@/graphql/tags';
import { ITag } from '@/types/tag.model'; import { ITag } from '@/types/tag.model';
import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue'; import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
@Component({ @Component({
components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor }, components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
@ -99,57 +95,81 @@ import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
}, },
}, },
}) })
export default class CreateEvent extends Vue { export default class EditEvent extends Vue {
@Prop({ type: Boolean, default: false }) isUpdate!: boolean;
@Prop({ required: false, type: String }) uuid!: string; @Prop({ required: false, type: String }) uuid!: string;
loggedPerson: IPerson = new Person(); eventId!: string | undefined;
/*categories: string[] = Object.keys(Category);*/
event: IEvent = new EventModel(); loggedPerson = new Person();
event = new EventModel();
pictureFile: File | null = null; pictureFile: File | null = null;
EventVisibility = EventVisibility; EventVisibility = EventVisibility;
// categories: string[] = Object.keys(Category);
@Watch('$route.params.eventId', { immediate: true })
async onEventIdParamChanged (val: string) {
if (this.isUpdate !== true) return;
this.eventId = val;
if (this.eventId) {
this.event = await this.getEvent();
this.pictureFile = await buildFileFromIPicture(this.event.picture);
}
}
created() { created() {
const now = new Date(); const now = new Date();
const end = new Date(); const end = new Date();
end.setUTCHours(now.getUTCHours() + 3); end.setUTCHours(now.getUTCHours() + 3);
this.event.beginsOn = now; this.event.beginsOn = now;
this.event.endsOn = end; this.event.endsOn = end;
} }
createEvent(e: Event) { createOrUpdate(e: Event) {
e.preventDefault(); e.preventDefault();
if (this.event.uuid === '') { if (this.eventId) return this.updateEvent();
console.log('event', this.event);
this.$apollo return this.createEvent();
.mutate({ }
mutation: CREATE_EVENT,
variables: this.buildVariables(), async createEvent() {
}) try {
.then(data => { const data = await this.$apollo.mutate({
console.log('event created', data); mutation: CREATE_EVENT,
this.$router.push({ variables: this.buildVariables(),
name: 'Event', });
params: { uuid: data.data.createEvent.uuid },
}); console.log('Event created', data);
})
.catch(error => { this.$router.push({
console.error(error); name: 'Event',
}); params: { uuid: data.createEvent.uuid },
} else { });
this.$apollo } catch (err) {
.mutate({ console.error(err);
mutation: EDIT_EVENT, }
}) }
.then(data => {
this.$router.push({ async updateEvent() {
name: 'Event', try {
params: { uuid: data.data.uuid }, await this.$apollo.mutate({
}); mutation: EDIT_EVENT,
}) variables: this.buildVariables(),
.catch(error => { });
console.error(error);
}); this.$router.push({
name: 'Event',
params: { uuid: this.eventId as string },
});
} catch (err) {
console.error(err);
} }
} }
@ -157,10 +177,6 @@ export default class CreateEvent extends Vue {
* Build variables for Event GraphQL creation query * Build variables for Event GraphQL creation query
*/ */
private buildVariables() { private buildVariables() {
/**
* Transform general variables
*/
let pictureObj = {};
const obj = { const obj = {
organizerActorId: this.loggedPerson.id, organizerActorId: this.loggedPerson.id,
beginsOn: this.event.beginsOn.toISOString(), beginsOn: this.event.beginsOn.toISOString(),
@ -172,23 +188,22 @@ export default class CreateEvent extends Vue {
delete this.event.physicalAddress['__typename']; delete this.event.physicalAddress['__typename'];
} }
/** const pictureObj = buildFileVariable(this.pictureFile, 'picture');
* Transform picture files
*/
if (this.pictureFile) {
pictureObj = {
picture: {
picture: {
name: this.pictureFile.name,
file: this.pictureFile,
},
},
};
}
return Object.assign({}, res, pictureObj); return Object.assign({}, res, pictureObj);
} }
private async getEvent() {
const result = await this.$apollo.query({
query: FETCH_EVENT,
variables: {
uuid: this.eventId,
},
});
return new EventModel(result.data.event);
}
// getAddressData(addressData) { // getAddressData(addressData) {
// if (addressData !== null) { // if (addressData !== null) {
// this.event.address = { // this.event.address = {
@ -208,4 +223,4 @@ export default class CreateEvent extends Vue {
// } // }
// } // }
} }
</script> </script>

View File

@ -6,10 +6,8 @@ import { onError } from 'apollo-link-error';
import { createLink } from 'apollo-absinthe-upload-link'; import { createLink } from 'apollo-absinthe-upload-link';
import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint'; import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
import { ApolloClient } from 'apollo-client'; import { ApolloClient } from 'apollo-client';
import { DollarApollo } from 'vue-apollo/types/vue-apollo';
import { buildCurrentUserResolver } from '@/apollo/user'; import { buildCurrentUserResolver } from '@/apollo/user';
import { isServerError } from '@/types/apollo'; import { isServerError } from '@/types/apollo';
import { inspect } from 'util';
import { REFRESH_TOKEN } from '@/graphql/auth'; import { REFRESH_TOKEN } from '@/graphql/auth';
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants'; import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants';
import { logout, saveTokenData } from '@/utils/auth'; import { logout, saveTokenData } from '@/utils/auth';