From 2cfb777a9dd442349b687bd4f1e70b1f9ddbde73 Mon Sep 17 00:00:00 2001
From: Chocobozzz <me@florianbigard.com>
Date: Mon, 2 Sep 2019 14:35:50 +0200
Subject: [PATCH] Prepare front to edit events

---
 js/src/graphql/event.ts                       |  18 +-
 js/src/main.ts                                |   1 +
 js/src/router/event.ts                        |  10 +-
 js/src/types/event.model.ts                   |  95 ++++++++---
 js/src/types/picture.model.ts                 |  15 +-
 js/src/utils/image.ts                         |  24 +++
 js/src/utils/object.ts                        |   5 +
 .../views/Account/children/EditIdentity.vue   |  25 +--
 js/src/views/Event/{Create.vue => Edit.vue}   | 161 ++++++++++--------
 js/src/vue-apollo.ts                          |   4 +-
 10 files changed, 213 insertions(+), 145 deletions(-)
 create mode 100644 js/src/utils/image.ts
 create mode 100644 js/src/utils/object.ts
 rename js/src/views/Event/{Create.vue => Edit.vue} (62%)

diff --git a/js/src/graphql/event.ts b/js/src/graphql/event.ts
index 3779894bd..6f7c10d45 100644
--- a/js/src/graphql/event.ts
+++ b/js/src/graphql/event.ts
@@ -139,15 +139,15 @@ export const FETCH_EVENTS = gql`
 
 export const CREATE_EVENT = gql`
   mutation CreateEvent(
-  $title: String!,
-  $description: String!,
-  $organizerActorId: ID!,
-  $category: String,
-  $beginsOn: DateTime!,
-  $picture: PictureInput,
-  $tags: [String],
-  $physicalAddress: AddressInput,
-  $visibility: EventVisibility
+    $title: String!,
+    $description: String!,
+    $organizerActorId: ID!,
+    $category: String,
+    $beginsOn: DateTime!,
+    $picture: PictureInput,
+    $tags: [String],
+    $physicalAddress: AddressInput,
+    $visibility: EventVisibility
   ) {
     createEvent(
       title: $title,
diff --git a/js/src/main.ts b/js/src/main.ts
index d4fc48765..25a5e6068 100644
--- a/js/src/main.ts
+++ b/js/src/main.ts
@@ -20,6 +20,7 @@ const language = (window.navigator as any).userLanguage || window.navigator.lang
 Vue.use(GetTextPlugin, {
   translations,
   defaultLanguage: 'en_US',
+  silent: true,
 });
 
 Vue.config.language = language.replace('-', '_');
diff --git a/js/src/router/event.ts b/js/src/router/event.ts
index 928119b1b..7b6f75741 100644
--- a/js/src/router/event.ts
+++ b/js/src/router/event.ts
@@ -3,7 +3,7 @@ import Location from '@/views/Location.vue';
 import { RouteConfig } from 'vue-router';
 
 // 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');
 // tslint:enable
 
@@ -25,15 +25,15 @@ export const eventRoutes: RouteConfig[] = [
   {
     path: '/events/create',
     name: EventRouteName.CREATE_EVENT,
-    component: createEvent,
+    component: editEvent,
     meta: { requiredAuth: true },
   },
   {
-    path: '/events/:id/edit',
+    path: '/events/edit/:eventId',
     name: EventRouteName.EDIT_EVENT,
-    component: createEvent,
-    props: true,
+    component: editEvent,
     meta: { requiredAuth: true },
+    props: { isUpdate: true },
   },
   {
     path: '/location/new',
diff --git a/js/src/types/event.model.ts b/js/src/types/event.model.ts
index 841e092bb..57f875bfe 100644
--- a/js/src/types/event.model.ts
+++ b/js/src/types/event.model.ts
@@ -1,7 +1,7 @@
 import { Actor, IActor } from './actor';
 import { IAddress } from '@/types/address.model';
 import { ITag } from '@/types/tag.model';
-import { IAbstractPicture, IPicture } from '@/types/picture.model';
+import { IPicture } from '@/types/picture.model';
 
 export enum EventStatus {
   TENTATIVE,
@@ -53,10 +53,10 @@ export interface IEvent {
   title: string;
   slug: string;
   description: string;
-  category: Category|null;
+  category: Category | null;
 
   beginsOn: Date;
-  endsOn: Date;
+  endsOn: Date | null;
   publishAt: Date;
 
   status: EventStatus;
@@ -64,7 +64,7 @@ export interface IEvent {
 
   joinOptions: EventJoinOptions;
 
-  picture: IAbstractPicture|null;
+  picture: IPicture | null;
 
   organizerActor: IActor;
   attributedTo: IActor;
@@ -79,27 +79,76 @@ export interface IEvent {
   tags: ITag[];
 }
 
-
 export class EventModel implements IEvent {
-  beginsOn: Date = new Date();
-  category: Category = Category.MEETING;
-  slug: string = '';
-  description: string = '';
-  endsOn: Date = new Date();
-  joinOptions: EventJoinOptions = EventJoinOptions.FREE;
-  local: boolean = true;
+  id?: number;
+
+  beginsOn = new Date();
+  endsOn: Date | null = new Date();
+
+  title = '';
+  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[] = [];
-  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[] = [];
-  onlineAddress: string = '';
-  phoneAddress: string = '';
-  picture: IAbstractPicture|null = null;
+
+  attributedTo = new Actor();
+  organizerActor = new Actor();
+
   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;
+  }
 }
diff --git a/js/src/types/picture.model.ts b/js/src/types/picture.model.ts
index 7311c6463..3670db29f 100644
--- a/js/src/types/picture.model.ts
+++ b/js/src/types/picture.model.ts
@@ -1,16 +1,11 @@
-export interface IAbstractPicture {
-  name;
-  alt;
-}
-
 export interface IPicture {
-  url;
-  name;
-  alt;
+  url: string;
+  name: string;
+  alt: string;
 }
 
 export interface IPictureUpload {
   file: File;
-  name: String;
-  alt: String|null;
+  name: string;
+  alt: string | null;
 }
diff --git a/js/src/utils/image.ts b/js/src/utils/image.ts
new file mode 100644
index 000000000..8eeba866b
--- /dev/null
+++ b/js/src/utils/image.ts
@@ -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,
+      },
+    },
+  };
+}
diff --git a/js/src/utils/object.ts b/js/src/utils/object.ts
new file mode 100644
index 000000000..aac64fa05
--- /dev/null
+++ b/js/src/utils/object.ts
@@ -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));
+}
diff --git a/js/src/views/Account/children/EditIdentity.vue b/js/src/views/Account/children/EditIdentity.vue
index 117ffcd02..ce13c540b 100644
--- a/js/src/views/Account/children/EditIdentity.vue
+++ b/js/src/views/Account/children/EditIdentity.vue
@@ -76,6 +76,7 @@ import PictureUpload from '@/components/PictureUpload.vue';
 import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint';
 import { Dialog } from 'buefy/dist/components/dialog';
 import { RouteName } from '@/router';
+import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
 
 @Component({
   components: {
@@ -113,7 +114,7 @@ export default class EditIdentity extends Vue {
     if (this.identityName) {
       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);
   }
 
-  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) {
     console.error(err);
 
@@ -285,18 +277,7 @@ export default class EditIdentity extends Vue {
   }
 
   private buildVariables() {
-    let avatarObj = {};
-    if (this.avatarFile) {
-      avatarObj = {
-        avatar: {
-          picture: {
-            name: this.avatarFile.name,
-            alt: `${this.identity.preferredUsername}'s avatar`,
-            file: this.avatarFile,
-          },
-        },
-      };
-    }
+    const avatarObj = buildFileVariable(this.avatarFile, 'avatar', `${this.identity.preferredUsername}'s avatar`);
 
     return Object.assign({}, this.identity, avatarObj);
   }
diff --git a/js/src/views/Event/Create.vue b/js/src/views/Event/Edit.vue
similarity index 62%
rename from js/src/views/Event/Create.vue
rename to js/src/views/Event/Edit.vue
index b3c73c416..b605936af 100644
--- a/js/src/views/Event/Create.vue
+++ b/js/src/views/Event/Edit.vue
@@ -1,14 +1,17 @@
 <template>
   <section class="container">
     <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>
+
     <div v-if="$apollo.loading">Loading...</div>
+
     <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">
           <translate>
-            General informations
+            General information
           </translate>
         </h2>
         <picture-upload v-model="pictureFile" />
@@ -46,22 +49,20 @@
         </h2>
           <label class="label">{{ $gettext('Event visibility') }}</label>
           <div class="field">
-            <b-radio v-model="event.visibility"
-                     name="name"
-                     :native-value="EventVisibility.PUBLIC">
+            <b-radio v-model="event.visibility" name="name" :native-value="EventVisibility.PUBLIC">
               <translate>Visible everywhere on the web (public)</translate>
             </b-radio>
           </div>
           <div class="field">
-            <b-radio v-model="event.visibility"
-                     name="name"
-                     :native-value="EventVisibility.PRIVATE">
+            <b-radio v-model="event.visibility" name="name" :native-value="EventVisibility.PRIVATE">
               <translate>Only accessible through link and search (private)</translate>
             </b-radio>
           </div>
 
           <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>
       </form>
     </div>
@@ -69,15 +70,9 @@
 </template>
 
 <script lang="ts">
-// import Location from '@/components/Location';
-import { CREATE_EVENT, EDIT_EVENT } from '@/graphql/event';
-import { Component, Prop, Vue } from 'vue-property-decorator';
-import {
-      Category,
-      IEvent,
-      EventModel,
-      EventVisibility,
-    } from '@/types/event.model';
+import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
+import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
+import { EventModel, EventVisibility, IEvent } from '@/types/event.model';
 import { LOGGED_PERSON } from '@/graphql/actor';
 import { IPerson, Person } from '@/types/actor';
 import PictureUpload from '@/components/PictureUpload.vue';
@@ -87,6 +82,7 @@ import TagInput from '@/components/Event/TagInput.vue';
 import { TAGS } from '@/graphql/tags';
 import { ITag } from '@/types/tag.model';
 import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
+import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
 
 @Component({
   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;
 
-  loggedPerson: IPerson = new Person();
-  /*categories: string[] = Object.keys(Category);*/
-  event: IEvent = new EventModel();
+  eventId!: string | undefined;
+
+  loggedPerson = new Person();
+  event = new EventModel();
   pictureFile: File | null = null;
+
   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() {
     const now = new Date();
     const end = new Date();
     end.setUTCHours(now.getUTCHours() + 3);
+
     this.event.beginsOn = now;
     this.event.endsOn = end;
   }
 
-  createEvent(e: Event) {
+  createOrUpdate(e: Event) {
     e.preventDefault();
 
-    if (this.event.uuid === '') {
-      console.log('event', this.event);
-      this.$apollo
-        .mutate({
-          mutation: CREATE_EVENT,
-          variables: this.buildVariables(),
-        })
-        .then(data => {
-          console.log('event created', data);
-          this.$router.push({
-            name: 'Event',
-            params: { uuid: data.data.createEvent.uuid },
-          });
-        })
-        .catch(error => {
-          console.error(error);
-        });
-    } else {
-      this.$apollo
-        .mutate({
-          mutation: EDIT_EVENT,
-        })
-        .then(data => {
-          this.$router.push({
-            name: 'Event',
-            params: { uuid: data.data.uuid },
-          });
-        })
-        .catch(error => {
-          console.error(error);
-        });
+    if (this.eventId) return this.updateEvent();
+
+    return this.createEvent();
+  }
+
+  async createEvent() {
+    try {
+      const data = await this.$apollo.mutate({
+        mutation: CREATE_EVENT,
+        variables: this.buildVariables(),
+      });
+
+      console.log('Event created', data);
+
+      this.$router.push({
+        name: 'Event',
+        params: { uuid: data.createEvent.uuid },
+      });
+    } catch (err) {
+      console.error(err);
+    }
+  }
+
+  async updateEvent() {
+    try {
+      await this.$apollo.mutate({
+        mutation: EDIT_EVENT,
+        variables: this.buildVariables(),
+      });
+
+      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
    */
   private buildVariables() {
-    /**
-     * Transform general variables
-     */
-    let pictureObj = {};
     const obj = {
       organizerActorId: this.loggedPerson.id,
       beginsOn: this.event.beginsOn.toISOString(),
@@ -172,23 +188,22 @@ export default class CreateEvent extends Vue {
       delete this.event.physicalAddress['__typename'];
     }
 
-    /**
-     * Transform picture files
-     */
-    if (this.pictureFile) {
-      pictureObj = {
-        picture: {
-          picture: {
-            name: this.pictureFile.name,
-            file: this.pictureFile,
-          },
-        },
-      };
-    }
+    const pictureObj = buildFileVariable(this.pictureFile, 'picture');
 
     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) {
   //   if (addressData !== null) {
   //     this.event.address = {
@@ -208,4 +223,4 @@ export default class CreateEvent extends Vue {
   //   }
   // }
 }
-</script>
\ No newline at end of file
+</script>
diff --git a/js/src/vue-apollo.ts b/js/src/vue-apollo.ts
index d39a67361..699755eb3 100644
--- a/js/src/vue-apollo.ts
+++ b/js/src/vue-apollo.ts
@@ -6,10 +6,8 @@ import { onError } from 'apollo-link-error';
 import { createLink } from 'apollo-absinthe-upload-link';
 import { GRAPHQL_API_ENDPOINT, GRAPHQL_API_FULL_PATH } from './api/_entrypoint';
 import { ApolloClient } from 'apollo-client';
-import { DollarApollo } from 'vue-apollo/types/vue-apollo';
 import { buildCurrentUserResolver } from '@/apollo/user';
 import { isServerError } from '@/types/apollo';
-import { inspect } from 'util';
 import { REFRESH_TOKEN } from '@/graphql/auth';
 import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from '@/constants';
 import { logout, saveTokenData } from '@/utils/auth';
@@ -107,7 +105,7 @@ const apolloClient = new ApolloClient({
   cache,
   link,
   connectToDevTools: true,
-  resolvers: buildCurrentUserResolver(cache)
+  resolvers: buildCurrentUserResolver(cache),
 });
 
 export const apolloProvider = new VueApollo({