Front-end fixes and updates

Especially Join/Leave event, Vue-Markdown replacement

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel 2019-02-25 17:20:06 +01:00
parent 3507438f17
commit c4e327508b
No known key found for this signature in database
GPG key ID: A061B9DDE0CA0773
11 changed files with 772 additions and 950 deletions

1581
js/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -13,8 +13,8 @@
}, },
"dependencies": { "dependencies": {
"apollo-absinthe-upload-link": "^1.5.0", "apollo-absinthe-upload-link": "^1.5.0",
"apollo-cache-inmemory": "^1.4.2", "apollo-cache-inmemory": "^1.4.3",
"apollo-client": "^2.4.12", "apollo-client": "^2.4.13",
"apollo-link": "^1.2.8", "apollo-link": "^1.2.8",
"apollo-link-http": "^1.5.11", "apollo-link-http": "^1.5.11",
"apollo-link-state": "^0.4.2", "apollo-link-state": "^0.4.2",
@ -25,37 +25,37 @@
"lodash": "^4.17.11", "lodash": "^4.17.11",
"material-design-icons": "^3.0.1", "material-design-icons": "^3.0.1",
"ngeohash": "^0.6.3", "ngeohash": "^0.6.3",
"register-service-worker": "^1.6.1", "register-service-worker": "^1.6.2",
"vue": "^2.6.3", "vue": "^2.6.7",
"vue-apollo": "^3.0.0-beta.28", "vue-apollo": "^3.0.0-beta.28",
"vue-class-component": "^6.3.2", "vue-class-component": "^7.0.1",
"vue-gettext": "^2.1.2", "vue-gettext": "^2.1.2",
"vue-markdown": "^2.2.4",
"vue-property-decorator": "^7.3.0", "vue-property-decorator": "^7.3.0",
"vue-router": "^3.0.2", "vue-router": "^3.0.2",
"vue-simple-markdown": "^1.0.8",
"vuex": "^3.1.0" "vuex": "^3.1.0"
}, },
"devDependencies": { "devDependencies": {
"@types/chai": "^4.1.7", "@types/chai": "^4.1.7",
"@types/lodash": "^4.14.120", "@types/lodash": "^4.14.121",
"@types/mocha": "^5.2.5", "@types/mocha": "^5.2.6",
"@vue/cli-plugin-babel": "^3.4.0", "@vue/cli-plugin-babel": "^3.4.1",
"@vue/cli-plugin-e2e-nightwatch": "^3.4.0", "@vue/cli-plugin-e2e-nightwatch": "^3.4.1",
"@vue/cli-plugin-pwa": "^3.4.0", "@vue/cli-plugin-pwa": "^3.4.1",
"@vue/cli-plugin-typescript": "^3.4.0", "@vue/cli-plugin-typescript": "^3.4.1",
"@vue/cli-plugin-unit-mocha": "^3.4.0", "@vue/cli-plugin-unit-mocha": "^3.4.1",
"@vue/cli-service": "^3.4.0", "@vue/cli-service": "^3.4.1",
"@vue/eslint-config-typescript": "^4.0.0", "@vue/eslint-config-typescript": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.29", "@vue/test-utils": "^1.0.0-beta.29",
"chai": "^4.2.0", "chai": "^4.2.0",
"dotenv-webpack": "^1.7.0", "dotenv-webpack": "^1.7.0",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"patch-package": "^6.0.2", "patch-package": "^6.0.4",
"sass-loader": "^7.1.0", "sass-loader": "^7.1.0",
"tslint-config-airbnb": "^5.11.1", "tslint-config-airbnb": "^5.11.1",
"typescript": "^3.3.3", "typescript": "^3.3.3333",
"vue-template-compiler": "^2.6.3", "vue-template-compiler": "^2.6.7",
"webpack-bundle-analyzer": "^3.0.3" "webpack-bundle-analyzer": "^3.0.4"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",

View file

@ -5,7 +5,8 @@ const participantQuery = `
actor { actor {
preferredUsername, preferredUsername,
avatarUrl, avatarUrl,
name name,
id
} }
`; `;
@ -117,23 +118,20 @@ export const EDIT_EVENT = gql`
`; `;
export const JOIN_EVENT = gql` export const JOIN_EVENT = gql`
mutation JoinEvent($id: Int!, $actorId: Int!) { mutation JoinEvent($eventId: Int!, $actorId: Int!) {
joinEvent( joinEvent(
id: $id, eventId: $eventId,
actorId: $actorId actorId: $actorId
) { ) {
actor {
${participantQuery} ${participantQuery}
},
role
} }
} }
`; `;
export const LEAVE_EVENT = gql` export const LEAVE_EVENT = gql`
mutation LeaveEvent($id: Int!, $actorId: Int!) { mutation LeaveEvent($eventId: Int!, $actorId: Int!) {
leaveEvent( leaveEvent(
id: $id, eventId: $eventId,
actorId: $actorId actorId: $actorId
) { ) {
actor { actor {

View file

@ -2,7 +2,7 @@
// (runtime-only or standalone) has been set in webpack.base.conf with an alias. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'; import Vue from 'vue';
// import * as VueGoogleMaps from 'vue2-google-maps'; // import * as VueGoogleMaps from 'vue2-google-maps';
import VueMarkdown from 'vue-markdown'; import VueSimpleMarkdown from 'vue-simple-markdown';
import Buefy from 'buefy' import Buefy from 'buefy'
import 'buefy/dist/buefy.css'; import 'buefy/dist/buefy.css';
import GetTextPlugin from 'vue-gettext'; import GetTextPlugin from 'vue-gettext';
@ -14,7 +14,7 @@ const translations = require('@/i18n/translations.json');
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(VueMarkdown); Vue.use(VueSimpleMarkdown);
Vue.use(Buefy, { Vue.use(Buefy, {
defaultContainerElement: '#mobilizon' defaultContainerElement: '#mobilizon'
}); });

View file

@ -28,7 +28,8 @@ export const userRoutes = [
path: '/register/profile', path: '/register/profile',
name: UserRouteName.REGISTER_PROFILE, name: UserRouteName.REGISTER_PROFILE,
component: RegisterProfile, component: RegisterProfile,
props: true, // We can only pass string values through params, therefore
props: (route) => ({ email: route.params.email, userAlreadyActivated: route.params.userAlreadyActivated === 'true' }),
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{ {
@ -56,8 +57,7 @@ export const userRoutes = [
path: '/validate/:token', path: '/validate/:token',
name: UserRouteName.VALIDATE, name: UserRouteName.VALIDATE,
component: Validate, component: Validate,
// We can only pass string values through params, therefore props: true,
props: (route) => ({ email: route.params.email, userAlreadyActivated: route.params.userAlreadyActivated === 'true' }),
meta: { requiresAuth: false }, meta: { requiresAuth: false },
}, },
{ {

View file

@ -12,16 +12,6 @@
<div class="columns is-mobile"> <div class="columns is-mobile">
<div class="column"> <div class="column">
<form v-if="!validationSent"> <form v-if="!validationSent">
<div class="columns is-mobile is-centered">
<div class="column is-narrow">
<figure class="image is-64x64">
<transition name="avatar">
<v-gravatar v-bind="{email: email}" default-img="mp"></v-gravatar>
</transition>
</figure>
</div>
</div>
<b-field <b-field
:label="$gettext('Username')" :label="$gettext('Username')"
:type="errors.preferred_username ? 'is-danger' : null" :type="errors.preferred_username ? 'is-danger' : null"

View file

@ -34,18 +34,13 @@ import {EventJoinOptions} from "../../types/event.model";
<script lang="ts"> <script lang="ts">
// import Location from '@/components/Location'; // import Location from '@/components/Location';
import VueMarkdown from "vue-markdown";
import {CREATE_EVENT, EDIT_EVENT} from "@/graphql/event"; import {CREATE_EVENT, EDIT_EVENT} from "@/graphql/event";
import {Component, Prop, Vue} from "vue-property-decorator"; import {Component, Prop, Vue} from "vue-property-decorator";
import {Category, EventJoinOptions, EventStatus, EventVisibility, IEvent} from "@/types/event.model"; import {Category, EventJoinOptions, EventStatus, EventVisibility, IEvent} from "@/types/event.model";
import {LOGGED_PERSON} from "@/graphql/actor"; import {LOGGED_PERSON} from "@/graphql/actor";
import {IPerson} from "@/types/actor.model"; import {IPerson} from "@/types/actor.model";
@Component({ @Component({})
components: {
VueMarkdown
}
})
export default class CreateEvent extends Vue { export default class CreateEvent extends Vue {
@Prop({ required: false, type: String }) uuid!: string; @Prop({ required: false, type: String }) uuid!: string;

View file

@ -47,15 +47,22 @@
<span>{{ event.begins_on | formatDate }} - {{ event.ends_on | formatDate }}</span> <span>{{ event.begins_on | formatDate }} - {{ event.ends_on | formatDate }}</span>
</div> </div>
<p v-if="actorIsOrganizer()"> <p v-if="actorIsOrganizer()">
<translate>Vous êtes organisateur de cet événement.</translate> <translate>You are an organizer.</translate>
</p> </p>
<div v-else> <div v-else>
<p v-if="actorIsParticipant()"> <p v-if="actorIsParticipant()">
<translate>Vous avez annoncé aller à cet événement.</translate> <translate>You announced that you're going to this event.</translate>
</p> </p>
<p v-else> <p v-else>
Vous y allez ? <translate>Are you going to this event?</translate><br />
<span>{{ event.participants.length }} personnes y vont.</span> <span>
<translate
:translate-n="event.participants.length"
translate-plural="%{event.participants.length} persons are going"
>
One person is going.
</translate>
</span>
</p> </p>
</div> </div>
<div v-if="!actorIsOrganizer()"> <div v-if="!actorIsOrganizer()">
@ -66,7 +73,7 @@
</div> </div>
<h2 class="subtitle">Details</h2> <h2 class="subtitle">Details</h2>
<p v-if="event.description"> <p v-if="event.description">
<vue-markdown :source="event.description"></vue-markdown> <vue-simple-markdown :source="event.description"></vue-simple-markdown>
</p> </p>
<h2 class="subtitle">Participants</h2> <h2 class="subtitle">Participants</h2>
<span v-if="event.participants.length === 0">No participants yet.</span> <span v-if="event.participants.length === 0">No participants yet.</span>
@ -99,15 +106,10 @@ import { LOGGED_PERSON } from '@/graphql/actor';
import { IEvent, IParticipant } from '@/types/event.model'; import { IEvent, IParticipant } from '@/types/event.model';
import { JOIN_EVENT } from '@/graphql/event'; import { JOIN_EVENT } from '@/graphql/event';
import { IPerson } from '@/types/actor.model'; import { IPerson } from '@/types/actor.model';
import { RouteName } from '@/router' import { RouteName } from '@/router';
import 'vue-simple-markdown/dist/vue-simple-markdown.css';
// No typings for this component, so we use require
const VueMarkdown = require('vue-markdown');
@Component({ @Component({
components: {
VueMarkdown
},
apollo: { apollo: {
event: { event: {
query: FETCH_EVENT, query: FETCH_EVENT,
@ -152,13 +154,13 @@ export default class Event extends Vue {
await this.$apollo.mutate<IParticipant>({ await this.$apollo.mutate<IParticipant>({
mutation: JOIN_EVENT, mutation: JOIN_EVENT,
variables: { variables: {
id: this.event.id, eventId: this.event.id,
actorId: this.loggedPerson.id, actorId: this.loggedPerson.id,
}, },
update: (store, { data: { joinEvent } }) => { update: (store, { data: { joinEvent } }) => {
const event = store.readQuery<IEvent>({ query: FETCH_EVENT }); const event = store.readQuery<IEvent>({ query: FETCH_EVENT });
if (event === null) { if (event === null) {
console.error('Cannot update event participant cache, because of null value.') console.error('Cannot update event participant cache, because of null value.');
return return
} }
@ -177,7 +179,7 @@ export default class Event extends Vue {
await this.$apollo.mutate<IParticipant>({ await this.$apollo.mutate<IParticipant>({
mutation: LEAVE_EVENT, mutation: LEAVE_EVENT,
variables: { variables: {
id: this.event.id, eventId: this.event.id,
actorId: this.loggedPerson.id, actorId: this.loggedPerson.id,
}, },
update: (store, { data: { leaveEvent } }) => { update: (store, { data: { leaveEvent } }) => {

View file

@ -19,17 +19,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import ngeohash from 'ngeohash';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue'; import EventCard from '@/components/Event/EventCard.vue';
import { RouteName } from '@/router'; import { RouteName } from '@/router';
// VueMarkdown is untyped const ngeohash = require('ngeohash');
const VueMarkdown = require('vue-markdown');
@Component({ @Component({
components: { components: {
VueMarkdown,
EventCard EventCard
} }
}) })

View file

@ -28,14 +28,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue } from "vue-property-decorator";
// VueMarkdown is untyped @Component({})
const VueMarkdown = require('vue-markdown')
@Component({
components: {
VueMarkdown
}
})
export default class CreateGroup extends Vue { export default class CreateGroup extends Vue {
e1 = 0; e1 = 0;
// FIXME: correctly type group // FIXME: correctly type group

View file

@ -4,7 +4,7 @@ defmodule MobilizonWeb.Resolvers.Event do
""" """
alias Mobilizon.Activity alias Mobilizon.Activity
alias Mobilizon.Events.{Event, Participant} alias Mobilizon.Events.{Event, Participant}
alias Mobilizon.Actors.User alias Mobilizon.Actors.{User, Actor}
# We limit the max number of events that can be retrieved # We limit the max number of events that can be retrieved
@event_max_limit 100 @event_max_limit 100
@ -100,8 +100,7 @@ defmodule MobilizonWeb.Resolvers.Event do
{:ok, %Participant{} = participant} <- {:ok, %Participant{} = participant} <-
Mobilizon.Events.get_participant(event_id, actor_id), Mobilizon.Events.get_participant(event_id, actor_id),
{:only_organizer, false} <- {:only_organizer, false} <-
{:only_organizer, {:only_organizer, check_that_participant_is_not_only_organizer(event_id, actor_id)},
Mobilizon.Events.list_organizers_participants_for_event(event_id) |> length == 1},
{:ok, _} <- {:ok, _} <-
Mobilizon.Events.delete_participant(participant) do Mobilizon.Events.delete_participant(participant) do
{:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}} {:ok, %{event: %{id: event_id}, actor: %{id: actor_id}}}
@ -121,6 +120,19 @@ defmodule MobilizonWeb.Resolvers.Event do
{:error, "You need to be logged-in to leave an event"} {:error, "You need to be logged-in to leave an event"}
end end
# We check that the actor asking to leave the event is not it's only organizer
# We start by fetching the list of organizers and if there's only one of them
# and that it's the actor requesting leaving the event we return true
@spec check_that_participant_is_not_only_organizer(integer(), integer()) :: boolean()
defp check_that_participant_is_not_only_organizer(event_id, actor_id) do
with [%Participant{actor: %Actor{id: participant_actor_id}}] <-
Mobilizon.Events.list_organizers_participants_for_event(event_id) do
participant_actor_id == actor_id
else
_ -> false
end
end
@doc """ @doc """
Create an event Create an event
""" """