Introduce group basic federation, event new page and notifications
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -1,135 +1,194 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="hero intro is-medium is-primary">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">{{ $t('About Mobilizon') }}</h1>
|
||||
<p class="subtitle">
|
||||
{{ $t('A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising.') }}
|
||||
</p>
|
||||
<b-button icon-left="open-in-new" size="is-large" type="is-secondary" tag="a" href="https://joinmobilizon.org">{{ $t('Learn more') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" id="mobilizon">
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column has-text-left-desktop">
|
||||
<h3 class="title">{{ $t('Gather ⋅ Organize ⋅ Mobilize') }}</h3>
|
||||
<p class="content" v-html="$t('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?')" />
|
||||
<p v-html="$t('Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.')" />
|
||||
</div>
|
||||
<div class="column has-text-right-desktop has-text-centered-mobile">
|
||||
<img src="img/about/action-mobilizon.png" width="300" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column has-text-right-desktop">
|
||||
<h3 class="title">{{ $t("Let's create a new common") }}</h3>
|
||||
<p v-html="$t('We want to develop a <b>digital common</b>, that everyone can make their own, which respects <b>privacy and activism by design</b>.')" />
|
||||
<p>
|
||||
<span v-html="$t('Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating <b>their own event platform</b>.')" />
|
||||
<i18n tag="span" path="This installation (called “instance“) can easily {interconnect}, thanks to {protocol}.">
|
||||
<b slot="interconnect">{{ $t('interconnect with others like it') }}</b>
|
||||
<a slot="protocol" href="https://en.wikipedia.org/wiki/ActivityPub">{{ $t('a decentralised federation protocol') }}</a>
|
||||
</i18n>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column has-text-left-desktop has-text-centered-mobile">
|
||||
<img src="img/about/common-mobilizon.png" width="300" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="hero quote is-secondary">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h3 class="title">{{ $t('To change the world, change the software') }}</h3>
|
||||
<blockquote>
|
||||
{{ $t('We won’t change the world from Facebook. The tool we dream of, surveillance capitalism corporations won’t develop it, as they couldn’t profit from it. This is an opportunity to build something better, by taking another approach.') }}</blockquote>
|
||||
<footer class="blockquote-footer">
|
||||
<a href="https://framablog.org/2019/05/14/mobilizon-lets-finance-a-software-to-free-our-events-from-facebook/">
|
||||
{{ $t('Read Framasoft’s statement of intent on the Framablog') }}
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="hero intro is-medium is-primary">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h3 class="title">{{ $t('Software to the people') }}</h3>
|
||||
<i18n tag="p" path="{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency.">
|
||||
<a slot="license" href="https://choosealicense.com/licenses/agpl-3.0/">{{ $t('Mobilizon’s licence') }}</a>
|
||||
<b slot="respect">{{ $t('respect of the fundamental freedoms') }}</b>
|
||||
<a slot="source" href="https://framagit.org/framasoft/mobilizon">{{ $t('its source code is public') }}</a>
|
||||
</i18n>
|
||||
<p>
|
||||
If the direction given by the development team does not suit you,
|
||||
you have the legal right to create your own version of the software,
|
||||
with your own governance choices.
|
||||
</p>
|
||||
<p>
|
||||
Mobilizon is not developed by a secretive start-up, but by a group
|
||||
of friends who strive to <a href="https://framasoft.org">change the world, one byte at a time</a>.
|
||||
So while we do work slower, we remain attentive and in touch with our users.
|
||||
</p>
|
||||
</div>
|
||||
<div class="column has-text-right-desktop has-text-centered-mobile">
|
||||
<img src="img/about/software-to-the-people-mobilizon.png" width="300" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column has-text-right-desktop">
|
||||
<h3 class="title">Concieved with care for humans</h3>
|
||||
<p>We asked professional designers to help us develop our vision for Mobilizon.
|
||||
We took time to study the <b>digital habits of activists</b> in order to
|
||||
understand the features they need to gather, organize, and mobilize.
|
||||
</p>
|
||||
<p>
|
||||
So that, right from its conception, Mobilizon would <b>fit the needs and uses of
|
||||
the people</b> who are going to use it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="column has-text-left-desktop has-text-centered-mobile">
|
||||
<img src="img/about/concieved-mobilizon.png" width="300" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<h1 class="title">{{ $t("About Mobilizon") }}</h1>
|
||||
<p class="subtitle">
|
||||
{{
|
||||
$t(
|
||||
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
<b-button
|
||||
icon-left="open-in-new"
|
||||
size="is-large"
|
||||
type="is-secondary"
|
||||
tag="a"
|
||||
href="https://joinmobilizon.org"
|
||||
>{{ $t("Learn more") }}</b-button
|
||||
>
|
||||
</div>
|
||||
<!-- We hide the "Find an instance button until https://joinmobilizon.org gets a instance picker -->
|
||||
<div class="hero register is-primary is-medium" v-if="config && config.registrationsOpen">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h2 class="title">Register on this instance</h2>
|
||||
<b-button type="is-secondary" size="is-large" tag="router-link" :to="{ name: RouteName.REGISTER }">{{ $t('Register') }}</b-button>
|
||||
</div>
|
||||
<!-- <div class="column">
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" id="mobilizon">
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column has-text-left-desktop">
|
||||
<h2 class="title">{{ $t("Gather ⋅ Organize ⋅ Mobilize") }}</h2>
|
||||
<p
|
||||
class="content"
|
||||
v-html="
|
||||
$t(
|
||||
'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?'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<p
|
||||
v-html="
|
||||
$t(
|
||||
'Mobilizon is a free/libre software that will allow communities to create <b>their own spaces</b> to publish events in order to better emancipate themselves from tech giants.'
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="column has-text-right-desktop has-text-centered-mobile">
|
||||
<img src="img/about/action-mobilizon.png" width="300" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column has-text-right-desktop">
|
||||
<h2 class="title">{{ $t("Let's create a new common") }}</h2>
|
||||
<p
|
||||
v-html="
|
||||
$t(
|
||||
'We want to develop a <b>digital common</b>, that everyone can make their own, which respects <b>privacy and activism by design</b>.'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<p>
|
||||
<span
|
||||
v-html="
|
||||
$t(
|
||||
'Installing Mobilizon will allow communities to free themselves from the services of tech giants by creating <b>their own event platform</b>.'
|
||||
)
|
||||
"
|
||||
/>
|
||||
<i18n
|
||||
tag="span"
|
||||
path="This installation (called “instance“) can easily {interconnect}, thanks to {protocol}."
|
||||
>
|
||||
<b slot="interconnect">{{ $t("interconnect with others like it") }}</b>
|
||||
<a slot="protocol" href="https://en.wikipedia.org/wiki/ActivityPub">{{
|
||||
$t("a decentralised federation protocol")
|
||||
}}</a>
|
||||
</i18n>
|
||||
</p>
|
||||
</div>
|
||||
<div class="column has-text-left-desktop has-text-centered-mobile">
|
||||
<img src="img/about/common-mobilizon.png" width="300" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="hero quote is-secondary">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2 class="title">{{ $t("To change the world, change the software") }}</h2>
|
||||
<blockquote>
|
||||
{{
|
||||
$t(
|
||||
"We won’t change the world from Facebook. The tool we dream of, surveillance capitalism corporations won’t develop it, as they couldn’t profit from it. This is an opportunity to build something better, by taking another approach."
|
||||
)
|
||||
}}
|
||||
</blockquote>
|
||||
<footer class="blockquote-footer">
|
||||
<a
|
||||
href="https://framablog.org/2019/05/14/mobilizon-lets-finance-a-software-to-free-our-events-from-facebook/"
|
||||
>{{ $t("Read Framasoft’s statement of intent on the Framablog") }}</a
|
||||
>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h2 class="title">{{ $t("Software to the people") }}</h2>
|
||||
<i18n
|
||||
tag="p"
|
||||
path="{license} guarantees {respect} of the people who will use it. Since {source}, anyone can audit it, which guarantees its transparency."
|
||||
>
|
||||
<a slot="license" href="https://choosealicense.com/licenses/agpl-3.0/">{{
|
||||
$t("Mobilizon’s licence")
|
||||
}}</a>
|
||||
<b slot="respect">{{ $t("respect of the fundamental freedoms") }}</b>
|
||||
<a slot="source" href="https://framagit.org/framasoft/mobilizon">{{
|
||||
$t("its source code is public")
|
||||
}}</a>
|
||||
</i18n>
|
||||
<p>
|
||||
If the direction given by the development team does not suit you, you have the legal
|
||||
right to create your own version of the software, with your own governance choices.
|
||||
</p>
|
||||
<p>
|
||||
Mobilizon is not developed by a secretive start-up, but by a group of friends who
|
||||
strive to
|
||||
<a href="https://framasoft.org">change the world, one byte at a time</a>. So while we
|
||||
do work slower, we remain attentive and in touch with our users.
|
||||
</p>
|
||||
</div>
|
||||
<div class="column has-text-right-desktop has-text-centered-mobile">
|
||||
<img src="img/about/software-to-the-people-mobilizon.png" width="300" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column has-text-right-desktop">
|
||||
<h2 class="title">Concieved with care for humans</h2>
|
||||
<p>
|
||||
We asked professional designers to help us develop our vision for Mobilizon. We took
|
||||
time to study the
|
||||
<b>digital habits of activists</b> in order to understand the features they need to
|
||||
gather, organize, and mobilize.
|
||||
</p>
|
||||
<p>
|
||||
So that, right from its conception, Mobilizon would
|
||||
<b>fit the needs and uses of the people</b> who are going to use it.
|
||||
</p>
|
||||
</div>
|
||||
<div class="column has-text-left-desktop has-text-centered-mobile">
|
||||
<img src="img/about/concieved-mobilizon.png" width="300" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- We hide the "Find an instance button until https://joinmobilizon.org gets a instance picker -->
|
||||
<div class="hero register is-primary is-medium" v-if="config && config.registrationsOpen">
|
||||
<div class="hero-body">
|
||||
<div class="container has-text-centered">
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<h2 class="title">Register on this instance</h2>
|
||||
<b-button
|
||||
type="is-secondary"
|
||||
size="is-large"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.REGISTER }"
|
||||
>{{ $t("Register") }}</b-button
|
||||
>
|
||||
</div>
|
||||
<!-- <div class="column">
|
||||
<h2 class="title">Find an instance</h2>
|
||||
<b-button type="is-secondary" size="is-large" tag="a" href="https://joinmobilizon.org">{{ $t('Register') }}</b-button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Component,
|
||||
Vue,
|
||||
} from 'vue-property-decorator';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { CONFIG } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import RouteName from "../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -146,54 +205,54 @@ export default class About extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/variables.scss";
|
||||
@import "@/variables.scss";
|
||||
|
||||
.hero.register {
|
||||
background: lighten($primary, 20%);
|
||||
.hero.register {
|
||||
background: lighten($primary, 20%);
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 3rem 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 0.1rem dotted #777;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&:nth-child(odd) .columns {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.hero.quote {
|
||||
background: lighten($secondary, 20%);
|
||||
h2 {
|
||||
background: initial;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.2em solid #333;
|
||||
display: block;
|
||||
padding-left: 1em;
|
||||
|
||||
&:before {
|
||||
content: "« ";
|
||||
}
|
||||
|
||||
section {
|
||||
padding: 3rem 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 0.1rem dotted #777;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&:nth-child(odd) .columns {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
&:after {
|
||||
content: " »";
|
||||
}
|
||||
}
|
||||
|
||||
.hero.quote {
|
||||
background: lighten($secondary, 20%);
|
||||
h3 {
|
||||
background: initial;
|
||||
}
|
||||
.blockquote-footer a {
|
||||
color: #6c757d;
|
||||
|
||||
blockquote {
|
||||
border-left: .2em solid #333;
|
||||
display: block;
|
||||
padding-left: 1em;
|
||||
|
||||
&:before {
|
||||
content: "« ";
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: " »";
|
||||
}
|
||||
}
|
||||
|
||||
.blockquote-footer a {
|
||||
color: #6c757d;
|
||||
|
||||
&:before {
|
||||
content: "\2014\00A0";
|
||||
}
|
||||
}
|
||||
&:before {
|
||||
content: "\2014\00A0";
|
||||
}
|
||||
</style>
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
<template>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t('Pick an identity') }}</p>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="list is-hoverable">
|
||||
<a class="list-item" v-for="identity in identities" :class="{ 'is-active': identity.id === currentIdentity.id }" @click="changeCurrentIdentity(identity)">
|
||||
<div class="media">
|
||||
<img class="media-left image is-48x48" v-if="identity.avatar" :src="identity.avatar.url" alt="" />
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<h3>@{{ identity.preferredUsername }}</h3>
|
||||
<small>{{ identity.name }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="modal-card">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t("Pick an identity") }}</p>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<div class="list is-hoverable">
|
||||
<a
|
||||
class="list-item"
|
||||
v-for="identity in identities"
|
||||
:class="{ 'is-active': identity.id === currentIdentity.id }"
|
||||
@click="changeCurrentIdentity(identity)"
|
||||
>
|
||||
<div class="media">
|
||||
<img
|
||||
class="media-left image is-48x48"
|
||||
v-if="identity.avatar"
|
||||
:src="identity.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
<div class="media-content">
|
||||
<h3>@{{ identity.preferredUsername }}</h3>
|
||||
<small>{{ identity.name }}</small>
|
||||
</div>
|
||||
</section>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { IActor } from '@/types/actor';
|
||||
import { IDENTITIES } from '@/graphql/actor';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IActor } from "@/types/actor";
|
||||
import { IDENTITIES } from "@/graphql/actor";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -34,13 +44,14 @@ import { IDENTITIES } from '@/graphql/actor';
|
||||
})
|
||||
export default class IdentityPicker extends Vue {
|
||||
@Prop() value!: IActor;
|
||||
|
||||
identities: IActor[] = [];
|
||||
|
||||
currentIdentity: IActor = this.value;
|
||||
|
||||
changeCurrentIdentity(identity: IActor) {
|
||||
this.currentIdentity = identity;
|
||||
this.$emit('input', identity);
|
||||
this.$emit("input", identity);
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,60 +1,86 @@
|
||||
<template>
|
||||
<div class="identity-picker">
|
||||
<span v-if="inline" class="inline">
|
||||
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/>
|
||||
<b-icon v-else size="is-small" icon="account-circle" />
|
||||
{{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
|
||||
<b-button type="is-text" @click="isComponentModalActive = true">
|
||||
{{ $t('Change') }}
|
||||
</b-button>
|
||||
</span>
|
||||
<span v-else class="block" @click="isComponentModalActive = true">
|
||||
<img class="image is-48x48" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</span>
|
||||
<b-modal :active.sync="isComponentModalActive" has-modal-card>
|
||||
<identity-picker v-model="currentIdentity" @input="relay" />
|
||||
</b-modal>
|
||||
<div class="identity-picker">
|
||||
<div
|
||||
v-if="inline"
|
||||
class="inline box"
|
||||
:class="{ 'has-background-grey-lighter': masked }"
|
||||
@click="isComponentModalActive = true"
|
||||
>
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="currentIdentity.avatar">
|
||||
<img class="image" :src="currentIdentity.avatar.url" alt="" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content" v-if="currentIdentity.name">
|
||||
<p class="is-4">{{ currentIdentity.name }}</p>
|
||||
<p class="is-6 has-text-grey">
|
||||
{{ `@${currentIdentity.preferredUsername}` }}
|
||||
<span v-if="masked">{{ $t("(Masked)") }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="media-content" v-else>
|
||||
{{ `@${currentIdentity.preferredUsername}` }}
|
||||
</div>
|
||||
<b-button type="is-text" @click="isComponentModalActive = true">
|
||||
{{ $t("Change") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else class="block" @click="isComponentModalActive = true">
|
||||
<img
|
||||
class="image is-48x48"
|
||||
v-if="currentIdentity.avatar"
|
||||
:src="currentIdentity.avatar.url"
|
||||
alt=""
|
||||
/>
|
||||
<b-icon v-else size="is-large" icon="account-circle" />
|
||||
</span>
|
||||
<b-modal :active.sync="isComponentModalActive" has-modal-card>
|
||||
<identity-picker v-model="currentIdentity" @input="relay" />
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { IActor } from '@/types/actor';
|
||||
import IdentityPicker from './IdentityPicker.vue';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { IActor } from "../../types/actor";
|
||||
import IdentityPicker from "./IdentityPicker.vue";
|
||||
|
||||
@Component({
|
||||
components: { IdentityPicker },
|
||||
})
|
||||
export default class IdentityPickerWrapper extends Vue {
|
||||
@Prop() value!: IActor;
|
||||
|
||||
@Prop({ default: true, type: Boolean }) inline!: boolean;
|
||||
isComponentModalActive: boolean = false;
|
||||
|
||||
@Prop({ type: Boolean, required: false, default: false }) masked = false;
|
||||
|
||||
isComponentModalActive = false;
|
||||
|
||||
currentIdentity: IActor = this.value;
|
||||
|
||||
@Watch('value')
|
||||
updateCurrentActor(value) {
|
||||
@Watch("value")
|
||||
updateCurrentActor(value: IActor) {
|
||||
this.currentIdentity = value;
|
||||
}
|
||||
|
||||
relay(identity: IActor) {
|
||||
this.currentIdentity = identity;
|
||||
this.$emit('input', identity);
|
||||
this.$emit("input", identity);
|
||||
this.isComponentModalActive = false;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.identity-picker {
|
||||
.identity-picker {
|
||||
.block {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.block {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.inline img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
}
|
||||
.inline {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
<div v-if="person">
|
||||
<div class="card-image" v-if="person.banner">
|
||||
<figure class="image">
|
||||
<img :src="person.banner.url">
|
||||
<img :src="person.banner.url" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="card-content">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="person.avatar">
|
||||
<img :src="person.avatar.url">
|
||||
<img :src="person.avatar.url" />
|
||||
</figure>
|
||||
</div>
|
||||
<div class="media-content">
|
||||
@@ -24,46 +24,46 @@
|
||||
|
||||
<b-dropdown hoverable has-link aria-role="list">
|
||||
<button class="button is-primary" slot="trigger">
|
||||
{{ $t('Public feeds') }}
|
||||
{{ $t("Public feeds") }}
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
</button>
|
||||
|
||||
<b-dropdown-item aria-role="listitem">
|
||||
<a :href="feedUrls('atom', true)">
|
||||
{{ $t('Public RSS/Atom Feed') }}
|
||||
{{ $t("Public RSS/Atom Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item aria-role="listitem">
|
||||
<a :href="feedUrls('ics', true)">
|
||||
{{ $t('Public iCal Feed') }}
|
||||
{{ $t("Public iCal Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<b-dropdown hoverable has-link aria-role="list" v-if="person.feedTokens.length > 0">
|
||||
<button class="button is-info" slot="trigger">
|
||||
{{ $t('Private feeds') }}
|
||||
{{ $t("Private feeds") }}
|
||||
<b-icon icon="menu-down"></b-icon>
|
||||
</button>
|
||||
|
||||
<b-dropdown-item aria-role="listitem">
|
||||
<a :href="feedUrls('atom', false)">
|
||||
{{ $t('RSS/Atom Feed') }}
|
||||
{{ $t("RSS/Atom Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item aria-role="listitem">
|
||||
<a :href="feedUrls('ics', false)">
|
||||
{{ $t('iCal Feed') }}
|
||||
{{ $t("iCal Feed") }}
|
||||
</a>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
<a class="button" v-if="currentActor.id === person.id" @click="createToken">
|
||||
{{ $t('Create token') }}
|
||||
{{ $t("Create token") }}
|
||||
</a>
|
||||
</div>
|
||||
<section v-if="person.organizedEvents.length > 0">
|
||||
<h2 class="subtitle">
|
||||
{{ $t('Organized') }}
|
||||
{{ $t("Organized") }}
|
||||
</h2>
|
||||
<div class="columns">
|
||||
<EventCard
|
||||
@@ -81,7 +81,7 @@
|
||||
@click="deleteProfile()"
|
||||
v-if="currentActor && currentActor.id === person.id"
|
||||
>
|
||||
{{ $t('Delete') }}
|
||||
{{ $t("Delete") }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
@@ -91,12 +91,12 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { FETCH_PERSON, CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { FETCH_PERSON, CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import { MOBILIZON_INSTANCE_HOST } from "../../api/_entrypoint";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { CREATE_FEED_TOKEN_ACTOR } from "../../graphql/feed_tokens";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -120,23 +120,24 @@ export default class Profile extends Vue {
|
||||
@Prop({ type: String, required: true }) name!: string;
|
||||
|
||||
person!: IPerson;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
// call again the method if the route changes
|
||||
@Watch('$route')
|
||||
onRouteChange() {
|
||||
// this.fetchData()
|
||||
}
|
||||
// // call again the method if the route changes
|
||||
// @Watch('$route')
|
||||
// onRouteChange() {
|
||||
// // this.fetchData()
|
||||
// }
|
||||
|
||||
feedUrls(format, isPublic = true): string {
|
||||
let url = format === 'ics' ? 'webcal:' : '';
|
||||
feedUrls(format: "ics" | "webcal:" | "atom", isPublic = true): string {
|
||||
let url = format === "ics" ? "webcal:" : "";
|
||||
url += `//${MOBILIZON_INSTANCE_HOST}/`;
|
||||
if (isPublic === true) {
|
||||
url += `@${this.person.preferredUsername}/feed/`;
|
||||
} else {
|
||||
url += `events/going/${this.person.feedTokens[0].token}/`;
|
||||
}
|
||||
return url + (format === 'ics' ? 'ics' : 'atom');
|
||||
return url + (format === "ics" ? "ics" : "atom");
|
||||
}
|
||||
|
||||
async createToken() {
|
||||
@@ -150,7 +151,7 @@ export default class Profile extends Vue {
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "../../variables";
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
@import "~bulma/sass/components/dropdown.sass";
|
||||
@import "../../variables";
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
@import "~bulma/sass/components/dropdown.sass";
|
||||
</style>
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
<section class="section container">
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t('Register an account on Mobilizon!') }}
|
||||
</h1>
|
||||
<b-message v-if="userAlreadyActivated">
|
||||
{{ $t('To achieve your registration, please create a first identity profile.')}}
|
||||
</b-message>
|
||||
<h1 class="title">{{ $t("Register an account on Mobilizon!") }}</h1>
|
||||
<b-message v-if="userAlreadyActivated">{{
|
||||
$t("To achieve your registration, please create a first identity profile.")
|
||||
}}</b-message>
|
||||
<form v-if="!validationSent" @submit.prevent="submit">
|
||||
<b-field :label="$t('Display name')">
|
||||
<b-input aria-required="true" required v-model="identity.name" @input="autoUpdateUsername($event)"/>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
v-model="identity.name"
|
||||
@input="autoUpdateUsername($event)"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-field
|
||||
@@ -32,26 +35,32 @@
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Description')">
|
||||
<b-input type="textarea" v-model="identity.summary"/>
|
||||
<b-input type="textarea" v-model="identity.summary" />
|
||||
</b-field>
|
||||
|
||||
<p class="control has-text-centered">
|
||||
<b-button type="is-primary" size="is-large" native-type="submit">
|
||||
{{ $t('Create my profile') }}
|
||||
</b-button>
|
||||
<b-button type="is-primary" size="is-large" native-type="submit">{{
|
||||
$t("Create my profile")
|
||||
}}</b-button>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
<div v-if="validationSent && !userAlreadyActivated">
|
||||
<b-message title="Success" type="is-success" closable="false">
|
||||
<h2 class="title">
|
||||
{{ $t('Your account is nearly ready, {username}', { username: identity.preferredUsername }) }}
|
||||
{{
|
||||
$t("Your account is nearly ready, {username}", {
|
||||
username: identity.preferredUsername,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
<p>{{ $t("A validation email was sent to {email}", { email }) }}</p>
|
||||
<p>
|
||||
{{ $t('A validation email was sent to {email}', { email }) }}
|
||||
</p>
|
||||
<p>
|
||||
{{ $t('Before you can login, you need to click on the link inside it to validate your account') }}
|
||||
{{
|
||||
$t(
|
||||
"Before you can login, you need to click on the link inside it to validate your account"
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</b-message>
|
||||
</div>
|
||||
@@ -61,25 +70,29 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop } from 'vue-property-decorator';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { IDENTITIES, REGISTER_PERSON } from '@/graphql/actor';
|
||||
import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint';
|
||||
import { RouteName } from '@/router';
|
||||
import { changeIdentity } from '@/utils/auth';
|
||||
import { mixins } from 'vue-class-component';
|
||||
import identityEditionMixin from '@/mixins/identityEdition';
|
||||
import { Component, Prop } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { IDENTITIES, REGISTER_PERSON } from "../../graphql/actor";
|
||||
import { MOBILIZON_INSTANCE_HOST } from "../../api/_entrypoint";
|
||||
import RouteName from "../../router/name";
|
||||
import { changeIdentity } from "../../utils/auth";
|
||||
import identityEditionMixin from "../../mixins/identityEdition";
|
||||
|
||||
@Component
|
||||
export default class Register extends mixins(identityEditionMixin) {
|
||||
@Prop({ type: String, required: true }) email!: string;
|
||||
@Prop({ type: Boolean, required: false, default: false }) userAlreadyActivated!: boolean;
|
||||
|
||||
@Prop({ type: Boolean, required: false, default: false })
|
||||
userAlreadyActivated!: boolean;
|
||||
|
||||
host?: string = MOBILIZON_INSTANCE_HOST;
|
||||
|
||||
errors: object = {};
|
||||
validationSent: boolean = false;
|
||||
sendingValidation: boolean = false;
|
||||
|
||||
validationSent = false;
|
||||
|
||||
sendingValidation = false;
|
||||
|
||||
async created() {
|
||||
// Make sure no one goes to this page if we don't want to
|
||||
@@ -94,13 +107,15 @@ export default class Register extends mixins(identityEditionMixin) {
|
||||
this.errors = {};
|
||||
const { data } = await this.$apollo.mutate<{ registerPerson: IPerson }>({
|
||||
mutation: REGISTER_PERSON,
|
||||
variables: Object.assign({ email: this.email }, this.identity),
|
||||
update: (store, { data }) => {
|
||||
variables: { email: this.email, ...this.identity },
|
||||
update: (store, { data: localData }) => {
|
||||
if (this.userAlreadyActivated) {
|
||||
const identitiesData = store.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
||||
const identitiesData = store.readQuery<{ identities: IPerson[] }>({
|
||||
query: IDENTITIES,
|
||||
});
|
||||
|
||||
if (identitiesData && data) {
|
||||
identitiesData.identities.push(data.registerPerson);
|
||||
if (identitiesData && localData) {
|
||||
identitiesData.identities.push(localData.registerPerson);
|
||||
store.writeQuery({ query: IDENTITIES, data: identitiesData });
|
||||
}
|
||||
}
|
||||
@@ -108,7 +123,7 @@ export default class Register extends mixins(identityEditionMixin) {
|
||||
});
|
||||
if (data) {
|
||||
this.validationSent = true;
|
||||
window.localStorage.setItem('new-registered-user', 'yes');
|
||||
window.localStorage.setItem("new-registered-user", "yes");
|
||||
|
||||
if (this.userAlreadyActivated) {
|
||||
await changeIdentity(this.$apollo.provider.defaultClient, data.registerPerson);
|
||||
@@ -116,13 +131,16 @@ export default class Register extends mixins(identityEditionMixin) {
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.errors = error.graphQLErrors.reduce((acc, error) => {
|
||||
acc[error.details] = error.message;
|
||||
return acc;
|
||||
}, {});
|
||||
console.error('Error while registering person', error);
|
||||
console.error('Errors while registering person', this.errors);
|
||||
} catch (errorCatched) {
|
||||
this.errors = errorCatched.graphQLErrors.reduce(
|
||||
(acc: { [key: string]: string }, error: any) => {
|
||||
acc[error.details] = error.message;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
console.error("Error while registering person", errorCatched);
|
||||
console.error("Errors while registering person", this.errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,7 +160,7 @@ export default class Register extends mixins(identityEditionMixin) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,18 +2,36 @@
|
||||
<div class="root" v-if="identity">
|
||||
<h1 class="title">
|
||||
<span v-if="isUpdate">{{ identity.displayName() }}</span>
|
||||
<span v-else>{{ $t('I create an identity') }}</span>
|
||||
<span v-else>{{ $t("I create an identity") }}</span>
|
||||
</h1>
|
||||
|
||||
<picture-upload v-model="avatarFile" class="picture-upload" />
|
||||
|
||||
<b-field horizontal :label="$t('Display name')">
|
||||
<b-input aria-required="true" required v-model="identity.name" @input="autoUpdateUsername($event)"/>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
v-model="identity.name"
|
||||
@input="autoUpdateUsername($event)"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal custom-class="username-field" expanded :label="$t('Username')" :message="message">
|
||||
<b-field
|
||||
horizontal
|
||||
custom-class="username-field"
|
||||
expanded
|
||||
:label="$t('Username')"
|
||||
:message="message"
|
||||
>
|
||||
<b-field expanded>
|
||||
<b-input aria-required="true" required v-model="identity.preferredUsername" :disabled="isUpdate" :use-html5-validation="!isUpdate" pattern="[a-z0-9_]+"/>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
v-model="identity.preferredUsername"
|
||||
:disabled="isUpdate"
|
||||
:use-html5-validation="!isUpdate"
|
||||
pattern="[a-z0-9_]+"
|
||||
/>
|
||||
|
||||
<p class="control">
|
||||
<span class="button is-static">@{{ getInstanceHost }}</span>
|
||||
@@ -22,69 +40,65 @@
|
||||
</b-field>
|
||||
|
||||
<b-field horizontal :label="$t('Description')">
|
||||
<b-input type="textarea" aria-required="false" v-model="identity.summary"/>
|
||||
<b-input type="textarea" aria-required="false" v-model="identity.summary" />
|
||||
</b-field>
|
||||
|
||||
<b-notification
|
||||
type="is-danger"
|
||||
has-icon
|
||||
aria-close-label="Close notification"
|
||||
role="alert"
|
||||
:key="error"
|
||||
v-for="error in errors"
|
||||
type="is-danger"
|
||||
has-icon
|
||||
aria-close-label="Close notification"
|
||||
role="alert"
|
||||
:key="error"
|
||||
v-for="error in errors"
|
||||
>{{ error }}</b-notification
|
||||
>
|
||||
{{ error }}
|
||||
</b-notification>
|
||||
|
||||
<b-field class="submit">
|
||||
<div class="control">
|
||||
<button type="button" class="button is-primary" @click="submit()">
|
||||
{{ $t('Save') }}
|
||||
</button>
|
||||
<button type="button" class="button is-primary" @click="submit()">{{ $t("Save") }}</button>
|
||||
</div>
|
||||
</b-field>
|
||||
|
||||
<div class="delete-identity" v-if="isUpdate">
|
||||
<span @click="openDeleteIdentityConfirmation()">
|
||||
{{ $t('Delete this identity') }}
|
||||
</span>
|
||||
<span @click="openDeleteIdentityConfirmation()">{{ $t("Delete this identity") }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped type="scss">
|
||||
h1 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
h1 {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.picture-upload {
|
||||
margin: 30px 0;
|
||||
}
|
||||
.picture-upload {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.submit,
|
||||
.delete-identity {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
.submit,
|
||||
.delete-identity {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.submit {
|
||||
margin: 30px 0;
|
||||
}
|
||||
.submit {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.delete-identity {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.delete-identity {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.username-field + .field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.username-field + .field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Watch } from 'vue-property-decorator';
|
||||
import { Component, Prop, Watch } from "vue-property-decorator";
|
||||
import { mixins } from "vue-class-component";
|
||||
import {
|
||||
CREATE_PERSON,
|
||||
CURRENT_ACTOR_CLIENT,
|
||||
@@ -92,21 +106,18 @@ import {
|
||||
FETCH_PERSON,
|
||||
IDENTITIES,
|
||||
UPDATE_PERSON,
|
||||
} from '@/graphql/actor';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
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, readFileAsync } from '@/utils/image';
|
||||
import { changeIdentity } from '@/utils/auth';
|
||||
import { mixins } from 'vue-class-component';
|
||||
import identityEditionMixin from '@/mixins/identityEdition';
|
||||
} from "../../../graphql/actor";
|
||||
import { IPerson, Person } from "../../../types/actor";
|
||||
import PictureUpload from "../../../components/PictureUpload.vue";
|
||||
import { MOBILIZON_INSTANCE_HOST } from "../../../api/_entrypoint";
|
||||
import RouteName from "../../../router/name";
|
||||
import { buildFileFromIPicture, buildFileVariable, readFileAsync } from "../../../utils/image";
|
||||
import { changeIdentity } from "../../../utils/auth";
|
||||
import identityEditionMixin from "../../../mixins/identityEdition";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
PictureUpload,
|
||||
Dialog,
|
||||
},
|
||||
apollo: {
|
||||
currentActor: {
|
||||
@@ -119,13 +130,16 @@ import identityEditionMixin from '@/mixins/identityEdition';
|
||||
username: this.identityName,
|
||||
};
|
||||
},
|
||||
skip() { return !this.identityName; },
|
||||
update: data => new Person(data.fetchPerson),
|
||||
skip() {
|
||||
return !this.identityName;
|
||||
},
|
||||
update: (data) => new Person(data.fetchPerson),
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
@Prop({ type: Boolean }) isUpdate!: boolean;
|
||||
|
||||
@Prop({ type: String }) identityName!: string;
|
||||
|
||||
errors: string[] = [];
|
||||
@@ -136,23 +150,23 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
|
||||
get message() {
|
||||
if (this.isUpdate) return null;
|
||||
return this.$t('Only alphanumeric characters and underscores are supported.');
|
||||
return this.$t("Only alphanumeric characters and underscores are supported.");
|
||||
}
|
||||
|
||||
@Watch('isUpdate')
|
||||
async isUpdateChanged () {
|
||||
@Watch("isUpdate")
|
||||
async isUpdateChanged() {
|
||||
this.resetFields();
|
||||
}
|
||||
|
||||
@Watch('identityName', { immediate: true })
|
||||
async onIdentityParamChanged(val) {
|
||||
@Watch("identityName", { immediate: true })
|
||||
async onIdentityParamChanged(val: string) {
|
||||
// Only used when we update the identity
|
||||
if (!this.isUpdate) return;
|
||||
|
||||
await this.redirectIfNoIdentitySelected(val);
|
||||
|
||||
if (!this.identityName) {
|
||||
return await this.$router.push({ name: 'CreateIdentity' });
|
||||
return await this.$router.push({ name: "CreateIdentity" });
|
||||
}
|
||||
|
||||
if (this.identityName && this.identity) {
|
||||
@@ -177,10 +191,12 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
id: this.identity.id,
|
||||
},
|
||||
update: (store) => {
|
||||
const data = store.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
||||
const data = store.readQuery<{ identities: IPerson[] }>({
|
||||
query: IDENTITIES,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
data.identities = data.identities.filter(i => i.id !== this.identity.id);
|
||||
data.identities = data.identities.filter((i) => i.id !== this.identity.id);
|
||||
|
||||
store.writeQuery({ query: IDENTITIES, data });
|
||||
}
|
||||
@@ -188,12 +204,16 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
});
|
||||
|
||||
this.$notifier.success(
|
||||
this.$t('Identity {displayName} deleted', { displayName: this.identity.displayName() }) as string,
|
||||
this.$t("Identity {displayName} deleted", {
|
||||
displayName: this.identity.displayName(),
|
||||
}) as string
|
||||
);
|
||||
/**
|
||||
* If we just deleted the current identity, we need to change it to the next one
|
||||
*/
|
||||
const data = this.$apollo.provider.defaultClient.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
||||
const data = this.$apollo.provider.defaultClient.readQuery<{
|
||||
identities: IPerson[];
|
||||
}>({ query: IDENTITIES });
|
||||
if (data) {
|
||||
await this.maybeUpdateCurrentActorCache(data.identities[0]);
|
||||
}
|
||||
@@ -212,10 +232,12 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
mutation: UPDATE_PERSON,
|
||||
variables,
|
||||
update: (store, { data: { updatePerson } }) => {
|
||||
const data = store.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
||||
const data = store.readQuery<{ identities: IPerson[] }>({
|
||||
query: IDENTITIES,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const index = data.identities.findIndex(i => i.id === this.identity.id);
|
||||
const index = data.identities.findIndex((i) => i.id === this.identity.id);
|
||||
|
||||
this.$set(data.identities, index, updatePerson);
|
||||
this.maybeUpdateCurrentActorCache(updatePerson);
|
||||
@@ -226,7 +248,9 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
});
|
||||
|
||||
this.$notifier.success(
|
||||
this.$t('Identity {displayName} updated', { displayName: this.identity.displayName() }) as string,
|
||||
this.$t("Identity {displayName} updated", {
|
||||
displayName: this.identity.displayName(),
|
||||
}) as string
|
||||
);
|
||||
} catch (err) {
|
||||
this.handleError(err);
|
||||
@@ -241,7 +265,9 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
mutation: CREATE_PERSON,
|
||||
variables,
|
||||
update: (store, { data: { createPerson } }) => {
|
||||
const data = store.readQuery<{ identities: IPerson[] }>({ query: IDENTITIES });
|
||||
const data = store.readQuery<{ identities: IPerson[] }>({
|
||||
query: IDENTITIES,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
data.identities.push(createPerson);
|
||||
@@ -252,10 +278,15 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
});
|
||||
|
||||
this.$notifier.success(
|
||||
this.$t('Identity {displayName} created', { displayName: this.identity.displayName() }) as string,
|
||||
this.$t("Identity {displayName} created", {
|
||||
displayName: this.identity.displayName(),
|
||||
}) as string
|
||||
);
|
||||
|
||||
await this.$router.push({ name: RouteName.UPDATE_IDENTITY, params: { identityName: this.identity.preferredUsername } });
|
||||
await this.$router.push({
|
||||
name: RouteName.UPDATE_IDENTITY,
|
||||
params: { identityName: this.identity.preferredUsername },
|
||||
});
|
||||
} catch (err) {
|
||||
this.handleError(err);
|
||||
}
|
||||
@@ -267,18 +298,25 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
|
||||
openDeleteIdentityConfirmation() {
|
||||
this.$buefy.dialog.prompt({
|
||||
type: 'is-danger',
|
||||
title: this.$t('Delete your identity') as string,
|
||||
message: `${this.$t('This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.')}
|
||||
type: "is-danger",
|
||||
title: this.$t("Delete your identity") as string,
|
||||
message: `${this.$t(
|
||||
"This will delete / anonymize all content (events, comments, messages, participations…) created from this identity."
|
||||
)}
|
||||
<br /><br />
|
||||
${this.$t('If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.')}
|
||||
${this.$t('Otherwise this identity will just be removed from the group administrators.')}
|
||||
${this.$t(
|
||||
"If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity."
|
||||
)}
|
||||
${this.$t(
|
||||
"Otherwise this identity will just be removed from the group administrators."
|
||||
)}
|
||||
<br /><br />
|
||||
${this.$t('To confirm, type your identity username "{preferredUsername}"', { preferredUsername: this.identity.preferredUsername })}`,
|
||||
confirmText: this.$t(
|
||||
'Delete {preferredUsername}',
|
||||
{ preferredUsername: this.identity.preferredUsername },
|
||||
) as string,
|
||||
${this.$t('To confirm, type your identity username "{preferredUsername}"', {
|
||||
preferredUsername: this.identity.preferredUsername,
|
||||
})}`,
|
||||
confirmText: this.$t("Delete {preferredUsername}", {
|
||||
preferredUsername: this.identity.preferredUsername,
|
||||
}) as string,
|
||||
inputAttrs: {
|
||||
placeholder: this.identity.preferredUsername,
|
||||
pattern: this.identity.preferredUsername,
|
||||
@@ -292,20 +330,24 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
console.error(err);
|
||||
|
||||
if (err.graphQLErrors !== undefined) {
|
||||
err.graphQLErrors.forEach(({ message }) => {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
this.$notifier.error(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async buildVariables() {
|
||||
const avatarObj = buildFileVariable(this.avatarFile, 'avatar', `${this.identity.preferredUsername}'s avatar`);
|
||||
const res = Object.assign({}, this.identity, avatarObj);
|
||||
const avatarObj = buildFileVariable(
|
||||
this.avatarFile,
|
||||
"avatar",
|
||||
`${this.identity.preferredUsername}'s avatar`
|
||||
);
|
||||
const res = { ...this.identity, ...avatarObj };
|
||||
/**
|
||||
* If the avatar didn't change, no need to try reuploading it
|
||||
*/
|
||||
if (this.identity.avatar) {
|
||||
const oldAvatarFile = await buildFileFromIPicture(this.identity.avatar) as File;
|
||||
const oldAvatarFile = (await buildFileFromIPicture(this.identity.avatar)) as File;
|
||||
const oldAvatarFileContent = await readFileAsync(oldAvatarFile);
|
||||
const newAvatarFileContent = await readFileAsync(this.avatarFile as File);
|
||||
if (oldAvatarFileContent === newAvatarFileContent) {
|
||||
@@ -316,12 +358,14 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
}
|
||||
|
||||
private async redirectIfNoIdentitySelected(identityParam?: string) {
|
||||
if (!!identityParam) return;
|
||||
if (identityParam) return;
|
||||
|
||||
await this.loadLoggedPersonIfNeeded();
|
||||
|
||||
if (!!this.currentActor) {
|
||||
await this.$router.push({ params: { identityName: this.currentActor.preferredUsername } });
|
||||
if (this.currentActor) {
|
||||
await this.$router.push({
|
||||
params: { identityName: this.currentActor.preferredUsername },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,18 +378,18 @@ export default class EditIdentity extends mixins(identityEditionMixin) {
|
||||
}
|
||||
}
|
||||
|
||||
private async loadLoggedPersonIfNeeded (bypassCache = false) {
|
||||
private async loadLoggedPersonIfNeeded(bypassCache = false) {
|
||||
if (this.currentActor) return;
|
||||
|
||||
const result = await this.$apollo.query({
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
fetchPolicy: bypassCache ? 'network-only' : undefined,
|
||||
fetchPolicy: bypassCache ? "network-only" : undefined,
|
||||
});
|
||||
|
||||
this.currentActor = result.data.currentActor;
|
||||
}
|
||||
|
||||
private resetFields () {
|
||||
private resetFields() {
|
||||
this.identity = new Person();
|
||||
this.oldDisplayName = null;
|
||||
this.avatarFile = null;
|
||||
|
||||
@@ -1,52 +1,57 @@
|
||||
<template>
|
||||
<section>
|
||||
<h1 class="title">{{ $t('Administration') }}</h1>
|
||||
<div class="tile is-ancestor" v-if="dashboard">
|
||||
<div class="tile is-vertical">
|
||||
<div class="tile">
|
||||
<div class="tile is-parent is-vertical is-6">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfEvents }}</p>
|
||||
<p>{{ $t('Published events')}}</p>
|
||||
</article>
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfComments}}</p>
|
||||
<p>{{ $t('Comments')}}</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
|
||||
<p>{{ $t('Users')}}</p>
|
||||
</article>
|
||||
<router-link :to="{ name: RouteName.REPORTS}">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfReports }}</p>
|
||||
<p>{{ $t('Opened reports')}}</p>
|
||||
</article>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: dashboard.lastPublicEventPublished.uuid } }">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ $t('Last published event') }}</p>
|
||||
<p class="subtitle">{{ dashboard.lastPublicEventPublished.title }}</p>
|
||||
<figure class="image is-4by3" v-if="dashboard.lastPublicEventPublished.picture">
|
||||
<img :src="dashboard.lastPublicEventPublished.picture.url" />
|
||||
</figure>
|
||||
</article>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
<section>
|
||||
<h1 class="title">{{ $t("Administration") }}</h1>
|
||||
<div class="tile is-ancestor" v-if="dashboard">
|
||||
<div class="tile is-vertical">
|
||||
<div class="tile">
|
||||
<div class="tile is-parent is-vertical is-6">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfEvents }}</p>
|
||||
<p>{{ $t("Published events") }}</p>
|
||||
</article>
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfComments }}</p>
|
||||
<p>{{ $t("Comments") }}</p>
|
||||
</article>
|
||||
</div>
|
||||
<div class="tile is-parent is-vertical">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfUsers }}</p>
|
||||
<p>{{ $t("Users") }}</p>
|
||||
</article>
|
||||
<router-link :to="{ name: RouteName.REPORTS }">
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ dashboard.numberOfReports }}</p>
|
||||
<p>{{ $t("Opened reports") }}</p>
|
||||
</article>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: dashboard.lastPublicEventPublished.uuid },
|
||||
}"
|
||||
>
|
||||
<article class="tile is-child box">
|
||||
<p class="dashboard-number">{{ $t("Last published event") }}</p>
|
||||
<p class="subtitle">{{ dashboard.lastPublicEventPublished.title }}</p>
|
||||
<figure class="image is-4by3" v-if="dashboard.lastPublicEventPublished.picture">
|
||||
<img :src="dashboard.lastPublicEventPublished.picture.url" />
|
||||
</figure>
|
||||
</article>
|
||||
</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { DASHBOARD } from '@/graphql/admin';
|
||||
import { IDashboard } from '@/types/admin.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { DASHBOARD } from "@/graphql/admin";
|
||||
import { IDashboard } from "@/types/admin.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -56,22 +61,23 @@ import { RouteName } from '@/router';
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Administration') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
title: this.$t("Administration") as string,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Dashboard extends Vue {
|
||||
dashboard!: IDashboard;
|
||||
|
||||
RouteName = RouteName;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard-number {
|
||||
color: #3C376E;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1.125;
|
||||
}
|
||||
</style>
|
||||
.dashboard-number {
|
||||
color: #3c376e;
|
||||
font-size: 40px;
|
||||
font-weight: 700;
|
||||
line-height: 1.125;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,57 +1,75 @@
|
||||
<template>
|
||||
<section>
|
||||
<h1 class="title">{{ $t('Instances') }}</h1>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<router-link tag="li" active-class="is-active" :to="{name: RouteName.RELAY_FOLLOWINGS}" exact>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-down"></b-icon>
|
||||
<span>{{ $t('Followings') }} <b-tag rounded> {{ relayFollowings.total }} </b-tag> </span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link tag="li" active-class="is-active" :to="{name: RouteName.RELAY_FOLLOWERS}" exact>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-up"></b-icon>
|
||||
<span>{{ $t('Followers') }} <b-tag rounded> {{ relayFollowers.total }} </b-tag> </span>
|
||||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</section>
|
||||
<section>
|
||||
<h1 class="title">{{ $t("Instances") }}</h1>
|
||||
<div class="tabs is-boxed">
|
||||
<ul>
|
||||
<router-link
|
||||
tag="li"
|
||||
active-class="is-active"
|
||||
:to="{ name: RouteName.RELAY_FOLLOWINGS }"
|
||||
exact
|
||||
>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-down"></b-icon>
|
||||
<span>
|
||||
{{ $t("Followings") }}
|
||||
<b-tag rounded>{{ relayFollowings.total }}</b-tag>
|
||||
</span>
|
||||
</a>
|
||||
</router-link>
|
||||
<router-link
|
||||
tag="li"
|
||||
active-class="is-active"
|
||||
:to="{ name: RouteName.RELAY_FOLLOWERS }"
|
||||
exact
|
||||
>
|
||||
<a>
|
||||
<b-icon icon="inbox-arrow-up"></b-icon>
|
||||
<span>
|
||||
{{ $t("Followers") }}
|
||||
<b-tag rounded>{{ relayFollowers.total }}</b-tag>
|
||||
</span>
|
||||
</a>
|
||||
</router-link>
|
||||
</ul>
|
||||
</div>
|
||||
<router-view></router-view>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { RouteName } from '@/router';
|
||||
import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from '@/graphql/admin';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
import { IFollower } from '@/types/actor/follower.model';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from "@/graphql/admin";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IFollower } from "@/types/actor/follower.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
relayFollowings: {
|
||||
query: RELAY_FOLLOWINGS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
fetchPolicy: "cache-and-network",
|
||||
},
|
||||
relayFollowers: {
|
||||
query: RELAY_FOLLOWERS,
|
||||
fetchPolicy: 'cache-and-network',
|
||||
fetchPolicy: "cache-and-network",
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Follows extends Vue {
|
||||
RouteName = RouteName;
|
||||
activeTab: number = 0;
|
||||
|
||||
activeTab = 0;
|
||||
|
||||
relayFollowings: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
|
||||
relayFollowers: Paginate<IFollower> = { elements: [], total: 0 };
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.tab-item {
|
||||
form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
.tab-item {
|
||||
form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,64 +1,112 @@
|
||||
<template>
|
||||
<section v-if="adminSettings">
|
||||
<form @submit.prevent="updateSettings">
|
||||
<b-field :label="$t('Instance Name')">
|
||||
<b-input v-model="adminSettings.instanceName" />
|
||||
<section v-if="adminSettings">
|
||||
<form @submit.prevent="updateSettings">
|
||||
<b-field :label="$t('Instance Name')">
|
||||
<b-input v-model="adminSettings.instanceName" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Instance Description')">
|
||||
<b-input type="textarea" v-model="adminSettings.instanceDescription" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Allow registrations')">
|
||||
<b-switch v-model="adminSettings.registrationsOpen">
|
||||
<p class="content" v-if="adminSettings.registrationsOpen">
|
||||
{{ $t("Registration is allowed, anyone can register.") }}
|
||||
</p>
|
||||
<p class="content" v-else>{{ $t("Registration is closed.") }}</p>
|
||||
</b-switch>
|
||||
</b-field>
|
||||
<b-field :label="$t('Instance Terms Source')">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third-desktop">
|
||||
<b-field>
|
||||
<b-radio
|
||||
v-model="adminSettings.instanceTermsType"
|
||||
name="instanceTermsType"
|
||||
:native-value="InstanceTermsType.DEFAULT"
|
||||
>{{ $t("Default Mobilizon.org terms") }}</b-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field :label="$t('Instance Description')">
|
||||
<b-input type="textarea" v-model="adminSettings.instanceDescription" />
|
||||
<b-field>
|
||||
<b-radio
|
||||
v-model="adminSettings.instanceTermsType"
|
||||
name="instanceTermsType"
|
||||
:native-value="InstanceTermsType.URL"
|
||||
>{{ $t("Custom URL") }}</b-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field :label="$t('Allow registrations')">
|
||||
<b-switch v-model="adminSettings.registrationsOpen">
|
||||
<p class="content" v-if="adminSettings.registrationsOpen">{{ $t('Registration is allowed, anyone can register.')}}</p>
|
||||
<p class="content" v-else>{{ $t('Registration is closed.')}}</p>
|
||||
</b-switch>
|
||||
<b-field>
|
||||
<b-radio
|
||||
v-model="adminSettings.instanceTermsType"
|
||||
name="instanceTermsType"
|
||||
:native-value="InstanceTermsType.CUSTOM"
|
||||
>{{ $t("Custom text") }}</b-radio
|
||||
>
|
||||
</b-field>
|
||||
<b-field :label="$t('Instance Terms Source')">
|
||||
<div class="columns">
|
||||
<div class="column is-one-quarter-desktop">
|
||||
<b-field>
|
||||
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.DEFAULT">{{ $t('Default Mobilizon.org terms')}}</b-radio>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.URL">{{ $t('Custom URL')}}</b-radio>
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-radio v-model="adminSettings.instanceTermsType" name="instanceTermsType" :native-value="InstanceTermsType.CUSTOM">{{ $t('Custom text')}}</b-radio>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.DEFAULT">
|
||||
<b>{{ $t('Default')}}</b>
|
||||
<i18n tag="p" class="content" path="The {default_terms} will be used. They will be translated in the user's language.">
|
||||
<a slot="default_terms" href="https://mobilizon.org/terms" target="_blank" rel="noopener">{{ $t('default Mobilizon terms')}}</a>
|
||||
</i18n>
|
||||
</div>
|
||||
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.URL">
|
||||
<b>{{ $t('URL')}}</b>
|
||||
<p class="content">{{ $t("Set an URL to a page with your own terms.") }}</p>
|
||||
</div>
|
||||
<div class="notification" v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM">
|
||||
<b>{{ $t('Custom')}}</b>
|
||||
<p class="content">{{ $t("Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template.") }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-field>
|
||||
<b-field :label="$t('Instance Terms URL')" v-if="adminSettings.instanceTermsType === InstanceTermsType.URL">
|
||||
<b-input type="URL" v-model="adminSettings.instanceTermsUrl" />
|
||||
</b-field>
|
||||
<b-field :label="$t('Instance Terms')" v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM">
|
||||
<b-input type="textarea" v-model="adminSettings.instanceTerms" />
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-primary">{{ $t('Save')}}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
<div class="column">
|
||||
<div
|
||||
class="notification"
|
||||
v-if="adminSettings.instanceTermsType === InstanceTermsType.DEFAULT"
|
||||
>
|
||||
<b>{{ $t("Default") }}</b>
|
||||
<i18n
|
||||
tag="p"
|
||||
class="content"
|
||||
path="The {default_terms} will be used. They will be translated in the user's language."
|
||||
>
|
||||
<a
|
||||
slot="default_terms"
|
||||
href="https://mobilizon.org/terms"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>{{ $t("default Mobilizon terms") }}</a
|
||||
>
|
||||
</i18n>
|
||||
</div>
|
||||
<div
|
||||
class="notification"
|
||||
v-if="adminSettings.instanceTermsType === InstanceTermsType.URL"
|
||||
>
|
||||
<b>{{ $t("URL") }}</b>
|
||||
<p class="content">{{ $t("Set an URL to a page with your own terms.") }}</p>
|
||||
</div>
|
||||
<div
|
||||
class="notification"
|
||||
v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM"
|
||||
>
|
||||
<b>{{ $t("Custom") }}</b>
|
||||
<p class="content">
|
||||
{{
|
||||
$t(
|
||||
"Enter your own terms. HTML tags allowed. Mobilizon.org's terms are provided as template."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</b-field>
|
||||
<b-field
|
||||
:label="$t('Instance Terms URL')"
|
||||
v-if="adminSettings.instanceTermsType === InstanceTermsType.URL"
|
||||
>
|
||||
<b-input type="URL" v-model="adminSettings.instanceTermsUrl" />
|
||||
</b-field>
|
||||
<b-field
|
||||
:label="$t('Instance Terms')"
|
||||
v-if="adminSettings.instanceTermsType === InstanceTermsType.CUSTOM"
|
||||
>
|
||||
<b-input type="textarea" v-model="adminSettings.instanceTerms" />
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-primary">{{ $t("Save") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS } from '@/graphql/admin';
|
||||
import { IAdminSettings, InstanceTermsType } from '@/types/admin.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS } from "@/graphql/admin";
|
||||
import { IAdminSettings, InstanceTermsType } from "@/types/admin.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -67,7 +115,9 @@ import { RouteName } from '@/router';
|
||||
})
|
||||
export default class Settings extends Vue {
|
||||
adminSettings!: IAdminSettings;
|
||||
|
||||
InstanceTermsType = InstanceTermsType;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async updateSettings() {
|
||||
@@ -78,11 +128,11 @@ export default class Settings extends Vue {
|
||||
...this.adminSettings,
|
||||
},
|
||||
});
|
||||
this.$notifier.success(this.$t('Admin settings successfully saved.') as string);
|
||||
this.$notifier.success(this.$t("Admin settings successfully saved.") as string);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.$notifier.error(this.$t('Failed to save admin settings') as string);
|
||||
this.$notifier.error(this.$t("Failed to save admin settings") as string);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
243
js/src/views/Conversations/Conversation.vue
Normal file
243
js/src/views/Conversations/Conversation.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<div class="container section" v-if="conversation">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: conversation.actor.preferredUsername },
|
||||
}"
|
||||
>{{ `@${conversation.actor.preferredUsername}` }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
params: { preferredUsername: conversation.actor.preferredUsername },
|
||||
}"
|
||||
>{{ $t("Conversations") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link :to="{ name: RouteName.CONVERSATION, params: { id: conversation.id } }">{{
|
||||
conversation.title
|
||||
}}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<div class="conversation-title">
|
||||
<h2 class="title" v-if="!editTitleMode">
|
||||
{{ conversation.title }}
|
||||
<span
|
||||
@click="
|
||||
() => {
|
||||
newTitle = conversation.title;
|
||||
editTitleMode = true;
|
||||
}
|
||||
"
|
||||
>
|
||||
<b-icon icon="pencil" />
|
||||
</span>
|
||||
</h2>
|
||||
<form v-else @submit.prevent="updateConversation" class="title-edit">
|
||||
<b-input :value="conversation.title" v-model="newTitle" />
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" native-type="submit" icon-right="check" />
|
||||
<b-button
|
||||
@click="
|
||||
() => {
|
||||
editTitleMode = false;
|
||||
newTitle = '';
|
||||
}
|
||||
"
|
||||
icon-right="close"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<conversation-comment
|
||||
v-for="comment in conversation.comments.elements"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
/>
|
||||
<b-button
|
||||
v-if="conversation.comments.elements.length < conversation.comments.total"
|
||||
@click="loadMoreComments"
|
||||
>Fetch more</b-button
|
||||
>
|
||||
<form @submit.prevent="reply">
|
||||
<b-field :label="$t('Text')">
|
||||
<editor v-model="newComment" />
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-primary">{{ $t("Reply") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import {
|
||||
GET_CONVERSATION,
|
||||
REPLY_TO_CONVERSATION,
|
||||
UPDATE_CONVERSATION,
|
||||
} from "@/graphql/conversation";
|
||||
import { IConversation } from "@/types/conversations";
|
||||
import ConversationComment from "@/components/Conversation/ConversationComment.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
conversation: {
|
||||
query: GET_CONVERSATION,
|
||||
variables() {
|
||||
return {
|
||||
id: this.id,
|
||||
page: 1,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ConversationComment,
|
||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
},
|
||||
})
|
||||
export default class Conversation extends Vue {
|
||||
@Prop({ type: String, required: true }) id!: string;
|
||||
|
||||
conversation!: IConversation;
|
||||
|
||||
newComment = "";
|
||||
|
||||
newTitle = "";
|
||||
|
||||
editTitleMode = false;
|
||||
|
||||
page = 1;
|
||||
|
||||
hasMoreComments = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async reply() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: REPLY_TO_CONVERSATION,
|
||||
variables: {
|
||||
conversationId: this.conversation.id,
|
||||
text: this.newComment,
|
||||
},
|
||||
update: (store, { data: { replyToConversation } }) => {
|
||||
const conversationData = store.readQuery<{
|
||||
conversation: IConversation;
|
||||
}>({
|
||||
query: GET_CONVERSATION,
|
||||
variables: {
|
||||
id: this.id,
|
||||
page: this.page,
|
||||
},
|
||||
});
|
||||
if (!conversationData) return;
|
||||
const { conversation } = conversationData;
|
||||
conversation.lastComment = replyToConversation.lastComment;
|
||||
conversation.comments.elements.push(replyToConversation.lastComment);
|
||||
conversation.comments.total += 1;
|
||||
store.writeQuery({
|
||||
query: GET_CONVERSATION,
|
||||
variables: { id: this.id, page: this.page },
|
||||
data: { conversation },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.newComment = "";
|
||||
}
|
||||
|
||||
async loadMoreComments() {
|
||||
this.page += 1;
|
||||
try {
|
||||
console.log(this.$apollo.queries.conversation);
|
||||
await this.$apollo.queries.conversation.fetchMore({
|
||||
// New variables
|
||||
variables: {
|
||||
id: this.id,
|
||||
page: this.page,
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return previousResult;
|
||||
const newComments = fetchMoreResult.conversation.comments.elements;
|
||||
this.hasMoreComments = newComments.length === 1;
|
||||
const { conversation } = previousResult;
|
||||
conversation.comments.elements = [
|
||||
...previousResult.conversation.comments.elements,
|
||||
...newComments,
|
||||
];
|
||||
|
||||
return { conversation };
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async updateConversation() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_CONVERSATION,
|
||||
variables: {
|
||||
conversationId: this.conversation.id,
|
||||
title: this.newTitle,
|
||||
},
|
||||
update: (store, { data: { updateConversation } }) => {
|
||||
const conversationData = store.readQuery<{
|
||||
conversation: IConversation;
|
||||
}>({
|
||||
query: GET_CONVERSATION,
|
||||
variables: {
|
||||
id: this.id,
|
||||
page: this.page,
|
||||
},
|
||||
});
|
||||
if (!conversationData) return;
|
||||
const { conversation } = conversationData;
|
||||
conversation.title = updateConversation.title;
|
||||
store.writeQuery({
|
||||
query: GET_CONVERSATION,
|
||||
variables: { id: this.id, page: this.page },
|
||||
data: { conversation },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.editTitleMode = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
div.container.section {
|
||||
background: white;
|
||||
|
||||
div.conversation-title {
|
||||
margin-bottom: 0.75rem;
|
||||
|
||||
h2.title {
|
||||
span {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
form.title-edit {
|
||||
div.control {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
84
js/src/views/Conversations/ConversationsList.vue
Normal file
84
js/src/views/Conversations/ConversationsList.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<div class="container section" v-if="group">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ `@${group.preferredUsername}` }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("Conversations") }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<div v-if="group.conversations.elements.length > 0">
|
||||
<conversation-list-item
|
||||
:conversation="conversation"
|
||||
v-for="conversation in group.conversations.elements"
|
||||
:key="conversation.id"
|
||||
/>
|
||||
</div>
|
||||
<b-button
|
||||
tag="router-link"
|
||||
:to="{
|
||||
name: RouteName.CREATE_CONVERSATION,
|
||||
params: { preferredUsername: this.preferredUsername },
|
||||
}"
|
||||
>{{ $t("New conversation") }}</b-button
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { FETCH_GROUP } from "@/graphql/actor";
|
||||
import { IGroup, usernameWithDomain } from "@/types/actor";
|
||||
import ConversationListItem from "@/components/Conversation/ConversationListItem.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: { ConversationListItem },
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class ConversationsList extends Vue {
|
||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
div.container.section {
|
||||
background: white;
|
||||
}
|
||||
</style>
|
||||
87
js/src/views/Conversations/Create.vue
Normal file
87
js/src/views/Conversations/Create.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1>{{ $t("Create a new conversation") }}</h1>
|
||||
|
||||
<form @submit.prevent="createConversation">
|
||||
<b-field :label="$t('Title')">
|
||||
<b-input aria-required="true" required v-model="conversation.title" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Text')">
|
||||
<editor v-model="conversation.text" />
|
||||
</b-field>
|
||||
|
||||
<button class="button is-primary" type="submit">{{ $t("Create my group") }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { IGroup, IPerson } from "@/types/actor";
|
||||
import { CURRENT_ACTOR_CLIENT, FETCH_GROUP } from "@/graphql/actor";
|
||||
import { CREATE_CONVERSATION } from "@/graphql/conversation";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
|
||||
},
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class CreateConversation extends Vue {
|
||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
conversation = { title: "", text: "" };
|
||||
|
||||
async createConversation() {
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: CREATE_CONVERSATION,
|
||||
variables: {
|
||||
title: this.conversation.title,
|
||||
text: this.conversation.text,
|
||||
actorId: this.group.id,
|
||||
creatorId: this.currentActor.id,
|
||||
},
|
||||
// update: (store, { data: { createConversation } }) => {
|
||||
// // TODO: update group list cache
|
||||
// },
|
||||
});
|
||||
|
||||
await this.$router.push({
|
||||
name: RouteName.CONVERSATION,
|
||||
params: {
|
||||
id: data.createConversation.id,
|
||||
slug: data.createConversation.slug,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.markdown-render h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
</style>
|
||||
@@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<span v-if="code === ErrorCode.REGISTRATION_CLOSED">
|
||||
{{ $t('Registration is currently closed.') }}
|
||||
{{ $t("Registration is currently closed.") }}
|
||||
</span>
|
||||
|
||||
<span v-else>
|
||||
{{ $t('Unknown error.') }}
|
||||
{{ $t("Unknown error.") }}
|
||||
</span>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { ErrorCode } from '@/types/error-code.model';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { ErrorCode } from "@/types/error-code.model";
|
||||
|
||||
@Component
|
||||
export default class ErrorPage extends Vue {
|
||||
@@ -21,7 +21,7 @@ export default class ErrorPage extends Vue {
|
||||
ErrorCode = ErrorCode;
|
||||
|
||||
mounted() {
|
||||
this.code = this.$route.query['code'] as ErrorCode;
|
||||
this.code = this.$route.query.code as ErrorCode;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="container">
|
||||
<h1 class="title" v-if="isUpdate === false">
|
||||
{{ $t('Create a new event') }}
|
||||
</h1>
|
||||
<h1 class="title" v-else>
|
||||
{{ $t('Update event {name}', { name: event.title }) }}
|
||||
</h1>
|
||||
<h1 class="title" v-if="isUpdate === false">{{ $t("Create a new event") }}</h1>
|
||||
<h1 class="title" v-else>{{ $t("Update event {name}", { name: event.title }) }}</h1>
|
||||
|
||||
<form ref="form">
|
||||
<subtitle>
|
||||
{{ $t('General information') }}
|
||||
</subtitle>
|
||||
<subtitle>{{ $t("General information") }}</subtitle>
|
||||
<picture-upload v-model="pictureFile" :textFallback="$t('Headline picture')" />
|
||||
|
||||
<b-field :label="$t('Title')" :type="checkTitleLength[0]" :message="checkTitleLength[1]">
|
||||
@@ -21,18 +15,20 @@
|
||||
<tag-input v-model="event.tags" :data="tags" path="title" />
|
||||
|
||||
<date-time-picker v-model="event.beginsOn" :label="$t('Starts on…')" />
|
||||
<date-time-picker :min-datetime="event.beginsOn" 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>
|
||||
<date-time-picker
|
||||
:min-datetime="event.beginsOn"
|
||||
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" />
|
||||
|
||||
<b-field :label="$t('Organizer')">
|
||||
<identity-picker-wrapper v-model="event.organizerActor" />
|
||||
</b-field>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('Description') }}</label>
|
||||
<label class="label">{{ $t("Description") }}</label>
|
||||
<editor v-model="event.description" />
|
||||
</div>
|
||||
|
||||
@@ -40,6 +36,34 @@
|
||||
<b-input icon="link" type="url" v-model="event.onlineAddress" placeholder="URL" />
|
||||
</b-field>
|
||||
|
||||
<subtitle>{{ $t("Organizers") }}</subtitle>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<b-field :label="$t('Organizer')">
|
||||
<identity-picker-wrapper
|
||||
v-model="event.organizerActor"
|
||||
:masked="event.options.hideOrganizerWhenGroupEvent"
|
||||
@input="resetAttributedToOnOrganizerChange"
|
||||
/>
|
||||
</b-field>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-field :label="$t('Group')" v-if="event.organizerActor">
|
||||
<group-picker-wrapper v-model="event.attributedTo" :identity="event.organizerActor" />
|
||||
</b-field>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="field" v-if="event.attributedTo.id">-->
|
||||
<!-- <label class="label">{{ $t('Hide the organizer') }}</label>-->
|
||||
<!-- <b-switch v-model="event.options.hideOrganizerWhenGroupEvent">-->
|
||||
<!-- {{ $t("Don't show @{organizer} as event host alongside @{group}", {organizer: event.organizerActor.preferredUsername, group: event.attributedTo.preferredUsername}) }}-->
|
||||
<!-- <small>-->
|
||||
<!-- <br>-->
|
||||
<!-- {{ $t('All group members and other eventual server admins will still be able to view this information.') }}-->
|
||||
<!-- </small>-->
|
||||
<!-- </b-switch>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<!--<b-field :label="$t('Category')">
|
||||
<b-select placeholder="Select a category" v-model="event.category">
|
||||
<option
|
||||
@@ -50,61 +74,67 @@
|
||||
</b-select>
|
||||
</b-field>-->
|
||||
|
||||
<subtitle>
|
||||
{{ $t('Who can view this event and participate') }}
|
||||
</subtitle>
|
||||
<div class="field">
|
||||
<b-radio v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.PUBLIC">
|
||||
{{ $t('Visible everywhere on the web (public)') }}
|
||||
</b-radio>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.UNLISTED">
|
||||
{{ $t('Only accessible through link and search (private)') }}
|
||||
</b-radio>
|
||||
</div>
|
||||
<subtitle>{{ $t("Who can view this event and participate") }}</subtitle>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.PUBLIC"
|
||||
>{{ $t("Visible everywhere on the web (public)") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-radio
|
||||
v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.UNLISTED"
|
||||
>{{ $t("Only accessible through link and search (private)") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
<!-- <div class="field">
|
||||
<b-radio v-model="event.visibility"
|
||||
name="eventVisibility"
|
||||
:native-value="EventVisibility.PRIVATE">
|
||||
{{ $t('Page limited to my group (asks for auth)') }}
|
||||
</b-radio>
|
||||
</div> -->
|
||||
</div>-->
|
||||
|
||||
<div class="field" v-if="config && config.anonymous.participation.allowed">
|
||||
<label class="label">{{ $t('Anonymous participations') }}</label>
|
||||
<label class="label">{{ $t("Anonymous participations") }}</label>
|
||||
<b-switch v-model="event.options.anonymousParticipation">
|
||||
{{ $t('I want to allow people to participate without an account.') }}
|
||||
{{ $t("I want to allow people to participate without an account.") }}
|
||||
<small v-if="config.anonymous.participation.validation.email.confirmationRequired">
|
||||
<br>
|
||||
{{ $t('Anonymous participants will be asked to confirm their participation through e-mail.') }}
|
||||
<br />
|
||||
{{
|
||||
$t(
|
||||
"Anonymous participants will be asked to confirm their participation through e-mail."
|
||||
)
|
||||
}}
|
||||
</small>
|
||||
</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('Participation approval') }}</label>
|
||||
<b-switch v-model="needsApproval">
|
||||
{{ $t('I want to approve every participation request') }}
|
||||
</b-switch>
|
||||
<label class="label">{{ $t("Participation approval") }}</label>
|
||||
<b-switch v-model="needsApproval">{{
|
||||
$t("I want to approve every participation request")
|
||||
}}</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="label">{{ $t('Number of places') }}</label>
|
||||
<b-switch v-model="limitedPlaces">
|
||||
{{ $t('Limited number of places') }}
|
||||
</b-switch>
|
||||
<label class="label">{{ $t("Number of places") }}</label>
|
||||
<b-switch v-model="limitedPlaces">{{ $t("Limited number of places") }}</b-switch>
|
||||
</div>
|
||||
|
||||
<div class="box" v-if="limitedPlaces">
|
||||
<b-field :label="$t('Number of places')">
|
||||
<b-numberinput controls-position="compact" min="1" v-model="event.options.maximumAttendeeCapacity" />
|
||||
<b-numberinput
|
||||
controls-position="compact"
|
||||
min="1"
|
||||
v-model="event.options.maximumAttendeeCapacity"
|
||||
/>
|
||||
</b-field>
|
||||
<!--
|
||||
<!--
|
||||
<b-field>
|
||||
<b-switch v-model="event.options.showRemainingAttendeeCapacity">
|
||||
{{ $t('Show remaining number of places') }}
|
||||
@@ -115,60 +145,64 @@
|
||||
<b-switch v-model="event.options.showParticipationPrice">
|
||||
{{ $t('Display participation price') }}
|
||||
</b-switch>
|
||||
</b-field> -->
|
||||
</b-field>-->
|
||||
</div>
|
||||
|
||||
<subtitle>
|
||||
{{ $t('Public comment moderation') }}
|
||||
</subtitle>
|
||||
<subtitle>{{ $t("Public comment moderation") }}</subtitle>
|
||||
|
||||
<div class="field">
|
||||
<b-radio v-model="event.options.commentModeration"
|
||||
name="commentModeration"
|
||||
:native-value="CommentModeration.ALLOW_ALL">
|
||||
{{ $t('Allow all comments') }}
|
||||
</b-radio>
|
||||
<b-radio
|
||||
v-model="event.options.commentModeration"
|
||||
name="commentModeration"
|
||||
:native-value="CommentModeration.ALLOW_ALL"
|
||||
>{{ $t("Allow all comments") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
|
||||
<!-- <div class="field">-->
|
||||
<!-- <b-radio v-model="event.options.commentModeration"-->
|
||||
<!-- name="commentModeration"-->
|
||||
<!-- :native-value="CommentModeration.MODERATED">-->
|
||||
<!-- {{ $t('Moderated comments (shown after approval)') }}-->
|
||||
<!-- </b-radio>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="field">-->
|
||||
<!-- <b-radio v-model="event.options.commentModeration"-->
|
||||
<!-- name="commentModeration"-->
|
||||
<!-- :native-value="CommentModeration.MODERATED">-->
|
||||
<!-- {{ $t('Moderated comments (shown after approval)') }}-->
|
||||
<!-- </b-radio>-->
|
||||
<!-- </div>-->
|
||||
|
||||
<div class="field">
|
||||
<b-radio v-model="event.options.commentModeration"
|
||||
name="commentModeration"
|
||||
:native-value="CommentModeration.CLOSED">
|
||||
{{ $t('Close comments for all (except for admins)') }}
|
||||
</b-radio>
|
||||
<b-radio
|
||||
v-model="event.options.commentModeration"
|
||||
name="commentModeration"
|
||||
:native-value="CommentModeration.CLOSED"
|
||||
>{{ $t("Close comments for all (except for admins)") }}</b-radio
|
||||
>
|
||||
</div>
|
||||
|
||||
<subtitle>
|
||||
{{ $t('Status') }}
|
||||
</subtitle>
|
||||
<subtitle>{{ $t("Status") }}</subtitle>
|
||||
|
||||
<b-field>
|
||||
<b-radio-button v-model="event.status"
|
||||
name="status"
|
||||
type="is-warning"
|
||||
:native-value="EventStatus.TENTATIVE">
|
||||
<b-radio-button
|
||||
v-model="event.status"
|
||||
name="status"
|
||||
type="is-warning"
|
||||
:native-value="EventStatus.TENTATIVE"
|
||||
>
|
||||
<b-icon icon="calendar-question" />
|
||||
{{ $t('Tentative: Will be confirmed later') }}
|
||||
{{ $t("Tentative: Will be confirmed later") }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="event.status"
|
||||
name="status"
|
||||
type="is-success"
|
||||
:native-value="EventStatus.CONFIRMED">
|
||||
<b-radio-button
|
||||
v-model="event.status"
|
||||
name="status"
|
||||
type="is-success"
|
||||
:native-value="EventStatus.CONFIRMED"
|
||||
>
|
||||
<b-icon icon="calendar-check" />
|
||||
{{ $t('Confirmed: Will happen') }}
|
||||
{{ $t("Confirmed: Will happen") }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="event.status"
|
||||
name="status"
|
||||
type="is-danger"
|
||||
:native-value="EventStatus.CANCELLED">
|
||||
<b-radio-button
|
||||
v-model="event.status"
|
||||
name="status"
|
||||
type="is-danger"
|
||||
:native-value="EventStatus.CANCELLED"
|
||||
>
|
||||
<b-icon icon="calendar-remove" />
|
||||
{{ $t("Cancelled: Won't happen") }}
|
||||
</b-radio-button>
|
||||
@@ -176,54 +210,62 @@
|
||||
</form>
|
||||
</div>
|
||||
<b-modal :active.sync="dateSettingsIsOpen" has-modal-card trap-focus>
|
||||
<form action="">
|
||||
<div class="modal-card" style="width: auto">
|
||||
<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>
|
||||
<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-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-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>
|
||||
<button class="button" type="button" @click="dateSettingsIsOpen = false">
|
||||
{{ $t("OK") }}
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
</form>
|
||||
</b-modal>
|
||||
<span ref="bottomObserver" />
|
||||
<nav role="navigation" aria-label="main navigation" class="navbar" :class="{'is-fixed-bottom': showFixedNavbar }">
|
||||
<nav
|
||||
role="navigation"
|
||||
aria-label="main navigation"
|
||||
class="navbar"
|
||||
:class="{ 'is-fixed-bottom': showFixedNavbar }"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-start">
|
||||
<span class="navbar-item" v-if="isEventModified">{{ $t('Unsaved changes') }}</span>
|
||||
<span class="navbar-item" v-if="isEventModified">{{ $t("Unsaved changes") }}</span>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-text" @click="confirmGoBack">
|
||||
{{ $t('Cancel') }}
|
||||
</b-button>
|
||||
<b-button type="is-text" @click="confirmGoBack">{{ $t("Cancel") }}</b-button>
|
||||
</span>
|
||||
<!-- If an event has been published we can't make it draft anymore -->
|
||||
<span class="navbar-item" v-if="event.draft === true">
|
||||
<b-button type="is-primary" outlined @click="createOrUpdateDraft">
|
||||
{{ $t('Save draft') }}
|
||||
</b-button>
|
||||
<b-button type="is-primary" outlined @click="createOrUpdateDraft">{{
|
||||
$t("Save draft")
|
||||
}}</b-button>
|
||||
</span>
|
||||
<span class="navbar-item">
|
||||
<b-button type="is-primary" @click="createOrUpdatePublish" @keyup.enter="createOrUpdatePublish">
|
||||
<span v-if="isUpdate === false">{{ $t('Create my event') }}</span>
|
||||
<span v-else-if="event.draft === true"> {{ $t('Publish') }}</span>
|
||||
<span v-else> {{ $t('Update my event') }}</span>
|
||||
<b-button
|
||||
type="is-primary"
|
||||
@click="createOrUpdatePublish"
|
||||
@keyup.enter="createOrUpdatePublish"
|
||||
>
|
||||
<span v-if="isUpdate === false">{{ $t("Create my event") }}</span>
|
||||
<span v-else-if="event.draft === true">{{ $t("Publish") }}</span>
|
||||
<span v-else>{{ $t("Update my event") }}</span>
|
||||
</b-button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -234,146 +276,191 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@/variables.scss";
|
||||
@import "@/variables.scss";
|
||||
|
||||
main section > .container {
|
||||
background: $white;
|
||||
main section > .container {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
h2.subtitle {
|
||||
margin: 10px 0;
|
||||
|
||||
span {
|
||||
padding: 5px 7px;
|
||||
display: inline;
|
||||
background: $secondary;
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
& > .container {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
|
||||
h2.subtitle {
|
||||
margin: 10px 0;
|
||||
nav.navbar {
|
||||
min-height: 2rem !important;
|
||||
background: lighten($secondary, 10%);
|
||||
|
||||
span {
|
||||
padding: 5px 7px;
|
||||
display: inline;
|
||||
background: $secondary;
|
||||
}
|
||||
}
|
||||
.container {
|
||||
min-height: 2rem;
|
||||
|
||||
section {
|
||||
& > .container {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
.navbar-menu,
|
||||
.navbar-end {
|
||||
display: flex !important;
|
||||
background: lighten($secondary, 10%);
|
||||
}
|
||||
|
||||
nav.navbar {
|
||||
min-height: 2rem !important;
|
||||
background: lighten($secondary, 10%);
|
||||
|
||||
.container {
|
||||
min-height: 2rem;
|
||||
|
||||
.navbar-menu, .navbar-end {
|
||||
display: flex !important;
|
||||
background: lighten($secondary, 10%);
|
||||
}
|
||||
|
||||
.navbar-end {
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
.navbar-end {
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { CREATE_EVENT, EDIT_EVENT, EVENT_PERSON_PARTICIPATION, FETCH_EVENT } from '@/graphql/event';
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import EditorComponent from "@/components/Editor.vue";
|
||||
import DateTimePicker from "@/components/Event/DateTimePicker.vue";
|
||||
import TagInput from "@/components/Event/TagInput.vue";
|
||||
import AddressAutoComplete from "@/components/Event/AddressAutoComplete.vue";
|
||||
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import GroupPickerWrapper from "@/components/Group/GroupPickerWrapper.vue";
|
||||
import { Route } from "vue-router";
|
||||
import {
|
||||
CommentModeration,
|
||||
EventJoinOptions,
|
||||
EventModel,
|
||||
EventStatus,
|
||||
EventVisibility,
|
||||
IEvent, ParticipantRole,
|
||||
} from '@/types/event.model';
|
||||
import { CURRENT_ACTOR_CLIENT, LOGGED_USER_DRAFTS, LOGGED_USER_PARTICIPATIONS } from '@/graphql/actor';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import PictureUpload from '@/components/PictureUpload.vue';
|
||||
import EditorComponent from '@/components/Editor.vue';
|
||||
import DateTimePicker from '@/components/Event/DateTimePicker.vue';
|
||||
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, readFileAsync } from '@/utils/image';
|
||||
import IdentityPickerWrapper from '@/views/Account/IdentityPickerWrapper.vue';
|
||||
import { RouteName } from '@/router';
|
||||
import 'intersection-observer';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
CREATE_EVENT,
|
||||
EDIT_EVENT,
|
||||
EVENT_PERSON_PARTICIPATION,
|
||||
FETCH_EVENT,
|
||||
} from "../../graphql/event";
|
||||
import {
|
||||
CommentModeration,
|
||||
EventJoinOptions,
|
||||
EventModel,
|
||||
EventStatus,
|
||||
EventVisibility,
|
||||
IEvent,
|
||||
ParticipantRole,
|
||||
} from "../../types/event.model";
|
||||
import {
|
||||
CURRENT_ACTOR_CLIENT,
|
||||
LOGGED_USER_DRAFTS,
|
||||
LOGGED_USER_PARTICIPATIONS,
|
||||
} from "../../graphql/actor";
|
||||
import { Group, IPerson, Person } from "../../types/actor";
|
||||
import { TAGS } from "../../graphql/tags";
|
||||
import { ITag } from "../../types/tag.model";
|
||||
import { buildFileFromIPicture, buildFileVariable, readFileAsync } from "../../utils/image";
|
||||
import RouteName from "../../router/name";
|
||||
import "intersection-observer";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
|
||||
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
||||
|
||||
@Component({
|
||||
components: { Subtitle, IdentityPickerWrapper, AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor: EditorComponent },
|
||||
components: {
|
||||
GroupPickerWrapper,
|
||||
Subtitle,
|
||||
IdentityPickerWrapper,
|
||||
AddressAutoComplete,
|
||||
TagInput,
|
||||
DateTimePicker,
|
||||
PictureUpload,
|
||||
Editor: EditorComponent,
|
||||
},
|
||||
apollo: {
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
tags: TAGS,
|
||||
config: CONFIG,
|
||||
event: {
|
||||
query: FETCH_EVENT,
|
||||
variables() {
|
||||
return {
|
||||
uuid: this.eventId,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
return new EventModel(data.event);
|
||||
},
|
||||
skip() {
|
||||
return !this.eventId;
|
||||
},
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: (this.$props.isUpdate ? this.$t('Event edition') : this.$t('Event creation')) as string,
|
||||
// @ts-ignore
|
||||
title: (this.isUpdate ? this.$t("Event edition") : this.$t("Event creation")) as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class EditEvent extends Vue {
|
||||
@Prop({ type: Boolean, default: false }) isUpdate!: boolean;
|
||||
@Prop({ required: false, type: String }) uuid!: string;
|
||||
|
||||
eventId!: string | undefined;
|
||||
@Prop({ required: false, type: String }) eventId: undefined | string;
|
||||
|
||||
currentActor = new Person();
|
||||
|
||||
tags: ITag[] = [];
|
||||
|
||||
event: IEvent = new EventModel();
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
unmodifiedEvent!: IEvent;
|
||||
|
||||
pictureFile: File | null = null;
|
||||
|
||||
EventStatus = EventStatus;
|
||||
|
||||
EventVisibility = EventVisibility;
|
||||
needsApproval: boolean = false;
|
||||
canPromote: boolean = true;
|
||||
limitedPlaces: boolean = false;
|
||||
|
||||
needsApproval = false;
|
||||
|
||||
canPromote = true;
|
||||
|
||||
limitedPlaces = false;
|
||||
|
||||
CommentModeration = CommentModeration;
|
||||
showFixedNavbar: boolean = true;
|
||||
|
||||
showFixedNavbar = true;
|
||||
|
||||
observer!: IntersectionObserver;
|
||||
dateSettingsIsOpen: boolean = false;
|
||||
endsOnNull: boolean = false;
|
||||
|
||||
// categories: string[] = Object.keys(Category);
|
||||
dateSettingsIsOpen = false;
|
||||
|
||||
@Watch('$route.params.eventId', { immediate: true })
|
||||
async onEventIdParamChanged (val: string) {
|
||||
if (!this.isUpdate) return;
|
||||
|
||||
this.eventId = val;
|
||||
|
||||
if (this.eventId) {
|
||||
this.event = await this.getEvent();
|
||||
this.unmodifiedEvent = JSON.parse(JSON.stringify(this.event.toEditJSON()));
|
||||
|
||||
this.pictureFile = await buildFileFromIPicture(this.event.picture);
|
||||
this.limitedPlaces = this.event.options.maximumAttendeeCapacity > 0;
|
||||
}
|
||||
}
|
||||
endsOnNull = false;
|
||||
|
||||
created() {
|
||||
this.initializeEvent();
|
||||
this.unmodifiedEvent = JSON.parse(JSON.stringify(this.event.toEditJSON()));
|
||||
}
|
||||
|
||||
private initializeEvent() {
|
||||
const roundUpTo = roundTo => x => new Date(Math.ceil(x / roundTo) * roundTo);
|
||||
const roundUpTo15Minutes = roundUpTo(1000 * 60 * 15);
|
||||
get isUpdate(): boolean {
|
||||
return this.eventId !== undefined;
|
||||
}
|
||||
|
||||
const now = roundUpTo15Minutes(new Date());
|
||||
const end = roundUpTo15Minutes(new Date());
|
||||
@Watch("eventId", { immediate: true })
|
||||
resetFormForCreation(eventId: string) {
|
||||
if (eventId === undefined) {
|
||||
this.event = new EventModel();
|
||||
}
|
||||
}
|
||||
|
||||
private initializeEvent() {
|
||||
// TODO : Check me
|
||||
// const roundUpTo = (roundTo) => (x: number) => new Date(Math.ceil(x / roundTo) * roundTo);
|
||||
// const roundUpTo15Minutes = roundUpTo(1000 * 60 * 15);
|
||||
|
||||
// const now = roundUpTo15Minutes(new Date());
|
||||
// const end = roundUpTo15Minutes(new Date());
|
||||
const now = new Date();
|
||||
const end = new Date();
|
||||
end.setUTCHours(now.getUTCHours() + 3);
|
||||
|
||||
this.event.beginsOn = now;
|
||||
@@ -381,17 +468,24 @@ export default class EditEvent extends Vue {
|
||||
this.event.organizerActor = this.event.organizerActor || this.currentActor;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.observer = new IntersectionObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry) {
|
||||
this.showFixedNavbar = !entry.isIntersecting;
|
||||
async mounted() {
|
||||
this.observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry) {
|
||||
this.showFixedNavbar = !entry.isIntersecting;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "-50px 0px -50px",
|
||||
}
|
||||
}, {
|
||||
rootMargin: '-50px 0px -50px',
|
||||
});
|
||||
);
|
||||
this.observer.observe(this.$refs.bottomObserver as Element);
|
||||
this.unmodifiedEvent = JSON.parse(JSON.stringify(this.event.toEditJSON()));
|
||||
|
||||
this.pictureFile = await buildFileFromIPicture(this.event.picture);
|
||||
this.limitedPlaces = this.event.options.maximumAttendeeCapacity > 0;
|
||||
}
|
||||
|
||||
createOrUpdateDraft(e: Event) {
|
||||
@@ -410,11 +504,22 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('currentActor')
|
||||
@Watch("currentActor")
|
||||
setCurrentActor() {
|
||||
this.event.organizerActor = this.currentActor;
|
||||
}
|
||||
|
||||
resetAttributedToOnOrganizerChange() {
|
||||
this.event.attributedTo = new Group();
|
||||
}
|
||||
|
||||
// @Watch('event.attributedTo', { deep: true })
|
||||
// updateHideOrganizerWhenGroupEventOption(attributedTo) {
|
||||
// if (!attributedTo.preferredUsername) {
|
||||
// this.event.options.hideOrganizerWhenGroupEvent = false;
|
||||
// }
|
||||
// }
|
||||
|
||||
private validateForm() {
|
||||
const form = this.$refs.form as HTMLFormElement;
|
||||
if (form.checkValidity()) {
|
||||
@@ -436,15 +541,15 @@ export default class EditEvent extends Vue {
|
||||
});
|
||||
|
||||
this.$buefy.notification.open({
|
||||
message: (this.event.draft ?
|
||||
this.$i18n.t('The event has been created as a draft') :
|
||||
this.$i18n.t('The event has been published')) as string,
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
message: (this.event.draft
|
||||
? this.$i18n.t("The event has been created as a draft")
|
||||
: this.$i18n.t("The event has been published")) as string,
|
||||
type: "is-success",
|
||||
position: "is-bottom-right",
|
||||
duration: 5000,
|
||||
});
|
||||
await this.$router.push({
|
||||
name: 'Event',
|
||||
name: "Event",
|
||||
params: { uuid: data.createEvent.uuid },
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -465,12 +570,12 @@ export default class EditEvent extends Vue {
|
||||
|
||||
this.$buefy.notification.open({
|
||||
message: this.updateEventMessage,
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
type: "is-success",
|
||||
position: "is-bottom-right",
|
||||
duration: 5000,
|
||||
});
|
||||
await this.$router.push({
|
||||
name: 'Event',
|
||||
name: "Event",
|
||||
params: { uuid: this.eventId as string },
|
||||
});
|
||||
} catch (err) {
|
||||
@@ -479,63 +584,79 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
|
||||
get updateEventMessage(): string {
|
||||
if (this.unmodifiedEvent.draft && !this.event.draft) return this.$i18n.t('The event has been updated and published') as string;
|
||||
return (this.event.draft ? this.$i18n.t('The draft event has been updated') : this.$i18n.t('The event has been updated')) as string;
|
||||
if (this.unmodifiedEvent.draft && !this.event.draft)
|
||||
return this.$i18n.t("The event has been updated and published") as string;
|
||||
return (this.event.draft
|
||||
? this.$i18n.t("The draft event has been updated")
|
||||
: this.$i18n.t("The event has been updated")) as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Put in cache the updated or created event.
|
||||
* If the event is not a draft anymore, also put in cache the participation
|
||||
*/
|
||||
private postCreateOrUpdate(store, updateEvent) {
|
||||
const resultEvent: IEvent = Object.assign({}, updateEvent);
|
||||
private postCreateOrUpdate(store: any, updateEvent: IEvent) {
|
||||
const resultEvent: IEvent = { ...updateEvent };
|
||||
const organizerActor: IPerson = this.event.organizerActor as Person;
|
||||
resultEvent.organizerActor = organizerActor;
|
||||
resultEvent.relatedEvents = [];
|
||||
|
||||
store.writeQuery({ query: FETCH_EVENT, variables: { uuid: updateEvent.uuid }, data: { event: resultEvent } });
|
||||
store.writeQuery({
|
||||
query: FETCH_EVENT,
|
||||
variables: { uuid: updateEvent.uuid },
|
||||
data: { event: resultEvent },
|
||||
});
|
||||
if (!updateEvent.draft) {
|
||||
store.writeQuery({
|
||||
query: EVENT_PERSON_PARTICIPATION,
|
||||
variables: { eventId: updateEvent.id, name: organizerActor.preferredUsername },
|
||||
variables: {
|
||||
eventId: updateEvent.id,
|
||||
name: organizerActor.preferredUsername,
|
||||
},
|
||||
data: {
|
||||
person: {
|
||||
__typename: 'Person',
|
||||
__typename: "Person",
|
||||
id: organizerActor.id,
|
||||
participations: [{
|
||||
__typename: 'Participant',
|
||||
id: 'unknown',
|
||||
role: ParticipantRole.CREATOR,
|
||||
actor: {
|
||||
__typename: 'Actor',
|
||||
id: organizerActor.id,
|
||||
participations: [
|
||||
{
|
||||
__typename: "Participant",
|
||||
id: "unknown",
|
||||
role: ParticipantRole.CREATOR,
|
||||
actor: {
|
||||
__typename: "Actor",
|
||||
id: organizerActor.id,
|
||||
},
|
||||
event: {
|
||||
__typename: "Event",
|
||||
id: updateEvent.id,
|
||||
},
|
||||
},
|
||||
event: {
|
||||
__typename: 'Event',
|
||||
id: updateEvent.id,
|
||||
},
|
||||
}],
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh drafts or participation cache depending if the event is still draft or not
|
||||
*/
|
||||
private postRefetchQueries(updateEvent) {
|
||||
/**
|
||||
* Refresh drafts or participation cache depending if the event is still draft or not
|
||||
*/
|
||||
private postRefetchQueries(updateEvent: IEvent) {
|
||||
if (updateEvent.draft) {
|
||||
return [{
|
||||
query: LOGGED_USER_DRAFTS,
|
||||
}];
|
||||
return [
|
||||
{
|
||||
query: LOGGED_USER_DRAFTS,
|
||||
},
|
||||
];
|
||||
}
|
||||
return [{
|
||||
query: LOGGED_USER_PARTICIPATIONS,
|
||||
variables: {
|
||||
afterDateTime: new Date(),
|
||||
return [
|
||||
{
|
||||
query: LOGGED_USER_PARTICIPATIONS,
|
||||
variables: {
|
||||
afterDateTime: new Date(),
|
||||
},
|
||||
},
|
||||
}];
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -544,25 +665,34 @@ export default class EditEvent extends Vue {
|
||||
private async buildVariables() {
|
||||
let res = this.event.toEditJSON();
|
||||
if (this.event.organizerActor) {
|
||||
res = Object.assign(res, { organizerActorId: this.event.organizerActor.id });
|
||||
res = Object.assign(res, {
|
||||
organizerActorId: this.event.organizerActor.id,
|
||||
});
|
||||
}
|
||||
if (this.event.attributedTo) {
|
||||
res = Object.assign(res, { attributedToId: this.event.attributedTo.id });
|
||||
}
|
||||
|
||||
delete this.event.options['__typename'];
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete this.event.options.__typename;
|
||||
|
||||
if (this.event.physicalAddress) {
|
||||
delete this.event.physicalAddress['__typename'];
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore
|
||||
delete this.event.physicalAddress.__typename;
|
||||
}
|
||||
|
||||
if (this.endsOnNull) {
|
||||
res.endsOn = null;
|
||||
}
|
||||
|
||||
const pictureObj = buildFileVariable(this.pictureFile, 'picture');
|
||||
res = Object.assign({}, res, pictureObj);
|
||||
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
||||
res = { ...res, ...pictureObj };
|
||||
|
||||
try {
|
||||
if (this.event.picture) {
|
||||
const oldPictureFile = await buildFileFromIPicture(this.event.picture) as File;
|
||||
const oldPictureFile = (await buildFileFromIPicture(this.event.picture)) as File;
|
||||
const oldPictureFileContent = await readFileAsync(oldPictureFile);
|
||||
const newPictureFileContent = await readFileAsync(this.pictureFile as File);
|
||||
if (oldPictureFileContent === newPictureFileContent) {
|
||||
@@ -589,19 +719,20 @@ export default class EditEvent extends Vue {
|
||||
return new EventModel(result.data.event);
|
||||
}
|
||||
|
||||
@Watch('limitedPlaces')
|
||||
@Watch("limitedPlaces")
|
||||
updatedEventCapacityOptions(limitedPlaces: boolean) {
|
||||
if (!limitedPlaces) {
|
||||
this.event.options.maximumAttendeeCapacity = 0;
|
||||
this.event.options.remainingAttendeeCapacity = 0;
|
||||
this.event.options.showRemainingAttendeeCapacity = false;
|
||||
} else {
|
||||
this.event.options.maximumAttendeeCapacity = this.event.options.maximumAttendeeCapacity || DEFAULT_LIMIT_NUMBER_OF_PLACES;
|
||||
this.event.options.maximumAttendeeCapacity =
|
||||
this.event.options.maximumAttendeeCapacity || DEFAULT_LIMIT_NUMBER_OF_PLACES;
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('needsApproval')
|
||||
updateEventJoinOptions(needsApproval) {
|
||||
@Watch("needsApproval")
|
||||
updateEventJoinOptions(needsApproval: boolean) {
|
||||
if (needsApproval === true) {
|
||||
this.event.joinOptions = EventJoinOptions.RESTRICTED;
|
||||
} else {
|
||||
@@ -610,31 +741,37 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
|
||||
get checkTitleLength() {
|
||||
return this.event.title.length > 80 ? ['is-info', this.$t('The event title will be ellipsed.')] : [undefined, undefined];
|
||||
return this.event.title.length > 80
|
||||
? ["is-info", this.$t("The event title will be ellipsed.")]
|
||||
: [undefined, undefined];
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm cancel
|
||||
*/
|
||||
confirmGoElsewhere(callback) {
|
||||
confirmGoElsewhere(callback: (value?: string) => any) {
|
||||
if (!this.isEventModified) {
|
||||
return callback();
|
||||
}
|
||||
const title: string = this.isUpdate ?
|
||||
this.$t('Cancel edition') as string :
|
||||
this.$t('Cancel creation') as string;
|
||||
const message: string = this.isUpdate ?
|
||||
this.$t("Are you sure you want to cancel the event edition? You'll lose all modifications.",
|
||||
{ title: this.event.title }) as string :
|
||||
this.$t("Are you sure you want to cancel the event creation? You'll lose all modifications.",
|
||||
{ title: this.event.title }) as string;
|
||||
const title: string = this.isUpdate
|
||||
? (this.$t("Cancel edition") as string)
|
||||
: (this.$t("Cancel creation") as string);
|
||||
const message: string = this.isUpdate
|
||||
? (this.$t(
|
||||
"Are you sure you want to cancel the event edition? You'll lose all modifications.",
|
||||
{ title: this.event.title }
|
||||
) as string)
|
||||
: (this.$t(
|
||||
"Are you sure you want to cancel the event creation? You'll lose all modifications.",
|
||||
{ title: this.event.title }
|
||||
) as string);
|
||||
|
||||
this.$buefy.dialog.confirm({
|
||||
title,
|
||||
message,
|
||||
confirmText: this.$t('Abandon edition') as string,
|
||||
cancelText: this.$t('Continue editing') as string,
|
||||
type: 'is-warning',
|
||||
confirmText: this.$t("Abandon edition") as string,
|
||||
cancelText: this.$t("Continue editing") as string,
|
||||
type: "is-warning",
|
||||
hasIcon: true,
|
||||
onConfirm: callback,
|
||||
});
|
||||
@@ -647,7 +784,7 @@ export default class EditEvent extends Vue {
|
||||
this.confirmGoElsewhere(() => this.$router.go(-1));
|
||||
}
|
||||
|
||||
beforeRouteLeave(to, from, next) {
|
||||
beforeRouteLeave(to: Route, from: Route, next: Function) {
|
||||
if (to.name === RouteName.EVENT) return next();
|
||||
this.confirmGoElsewhere(() => next());
|
||||
}
|
||||
@@ -656,10 +793,12 @@ export default class EditEvent extends Vue {
|
||||
return JSON.stringify(this.event.toEditJSON()) !== JSON.stringify(this.unmodifiedEvent);
|
||||
}
|
||||
|
||||
get beginsOn(): Date { return this.event.beginsOn; }
|
||||
get beginsOn(): Date {
|
||||
return this.event.beginsOn;
|
||||
}
|
||||
|
||||
@Watch('beginsOn', { deep: true })
|
||||
onBeginsOnChanged(beginsOn) {
|
||||
@Watch("beginsOn", { deep: true })
|
||||
onBeginsOnChanged(beginsOn: string) {
|
||||
if (!this.event.endsOn) return;
|
||||
const dateBeginsOn = new Date(beginsOn);
|
||||
const dateEndsOn = new Date(this.event.endsOn);
|
||||
@@ -682,4 +821,3 @@ export default class EditEvent extends Vue {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,6 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<h1>
|
||||
{{ $t('Event list') }}
|
||||
</h1>
|
||||
<h1>{{ $t("Event list") }}</h1>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<div v-if="events.length > 0" class="columns is-multiline">
|
||||
<EventCard
|
||||
@@ -12,18 +10,19 @@
|
||||
class="column is-one-quarter-desktop is-half-mobile"
|
||||
/>
|
||||
</div>
|
||||
<b-message v-if-else="events.length === 0 && $apollo.loading === false" type="is-danger">
|
||||
{{ $t('No events found') }}
|
||||
</b-message>
|
||||
<b-message v-if-else="events.length === 0 && $apollo.loading === false" type="is-danger">{{
|
||||
$t("No events found")
|
||||
}}</b-message>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import EventCard from "../../components/Event/EventCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
import { IEvent } from "../../types/event.model";
|
||||
|
||||
const ngeohash = require('ngeohash');
|
||||
const ngeohash = require("ngeohash");
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -34,78 +33,80 @@ export default class EventList extends Vue {
|
||||
@Prop(String) location!: string;
|
||||
|
||||
events = [];
|
||||
|
||||
loading = true;
|
||||
|
||||
locationChip = false;
|
||||
locationText = '';
|
||||
|
||||
created() {
|
||||
this.fetchData(this.$router.currentRoute.params['location']);
|
||||
}
|
||||
locationText = "";
|
||||
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.fetchData(to.params.location);
|
||||
next();
|
||||
}
|
||||
// created() {
|
||||
// this.fetchData(this.$router.currentRoute.params.location);
|
||||
// }
|
||||
|
||||
@Watch('locationChip')
|
||||
onLocationChipChange(val) {
|
||||
if (val === false) {
|
||||
this.$router.push({ name: RouteName.EVENT_LIST });
|
||||
}
|
||||
}
|
||||
// beforeRouteUpdate(to, from, next) {
|
||||
// this.fetchData(to.params.location);
|
||||
// next();
|
||||
// }
|
||||
|
||||
geocode(lat, lon) {
|
||||
console.log({ lat, lon });
|
||||
console.log(ngeohash.encode(lat, lon, 10));
|
||||
return ngeohash.encode(lat, lon, 10);
|
||||
}
|
||||
// @Watch('locationChip')
|
||||
// onLocationChipChange(val) {
|
||||
// if (val === false) {
|
||||
// this.$router.push({ name: RouteName.EVENT_LIST });
|
||||
// }
|
||||
// }
|
||||
|
||||
fetchData(location) {
|
||||
let queryString = '/events';
|
||||
if (location) {
|
||||
queryString += `?geohash=${location}`;
|
||||
const { latitude, longitude } = ngeohash.decode(location);
|
||||
this.locationText = `${latitude.toString()} : ${longitude.toString()}`;
|
||||
}
|
||||
this.locationChip = true;
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(queryString, this.$store)
|
||||
// .then(response => response.json())
|
||||
// .then((response) => {
|
||||
// this.loading = false;
|
||||
// this.events = response.data;
|
||||
// console.log(this.events);
|
||||
// });
|
||||
}
|
||||
// geocode(lat, lon) {
|
||||
// console.log({ lat, lon });
|
||||
// console.log(ngeohash.encode(lat, lon, 10));
|
||||
// return ngeohash.encode(lat, lon, 10);
|
||||
// }
|
||||
|
||||
deleteEvent(event) {
|
||||
const router = this.$router;
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(`/events/${event.uuid}`, this.$store, { method: 'DELETE' })
|
||||
// .then(() => router.push('/events'));
|
||||
}
|
||||
// fetchData(location: string) {
|
||||
// let queryString = '/events';
|
||||
// if (location) {
|
||||
// queryString += `?geohash=${location}`;
|
||||
// const { latitude, longitude } = ngeohash.decode(location);
|
||||
// this.locationText = `${latitude.toString()} : ${longitude.toString()}`;
|
||||
// }
|
||||
// this.locationChip = true;
|
||||
// // FIXME: remove eventFetch
|
||||
// // eventFetch(queryString, this.$store)
|
||||
// // .then(response => response.json())
|
||||
// // .then((response) => {
|
||||
// // this.loading = false;
|
||||
// // this.events = response.data;
|
||||
// // console.log(this.events);
|
||||
// // });
|
||||
// }
|
||||
|
||||
viewEvent(event) {
|
||||
// deleteEvent(event: IEvent) {
|
||||
// const router = this.$router;
|
||||
// // FIXME: remove eventFetch
|
||||
// // eventFetch(`/events/${event.uuid}`, this.$store, { method: 'DELETE' })
|
||||
// // .then(() => router.push('/events'));
|
||||
// }
|
||||
|
||||
viewEvent(event: IEvent) {
|
||||
this.$router.push({ name: RouteName.EVENT, params: { uuid: event.uuid } });
|
||||
}
|
||||
|
||||
downloadIcsEvent(event) {
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(`/events/${event.uuid}/ics`, this.$store, { responseType: 'arraybuffer' })
|
||||
// .then(response => response.text())
|
||||
// .then((response) => {
|
||||
// const blob = new Blob([ response ], { type: 'text/calendar' });
|
||||
// const link = document.createElement('a');
|
||||
// link.href = window.URL.createObjectURL(blob);
|
||||
// link.download = `${event.title}.ics`;
|
||||
// document.body.appendChild(link);
|
||||
// link.click();
|
||||
// document.body.removeChild(link);
|
||||
// });
|
||||
}
|
||||
// downloadIcsEvent(event: IEvent) {
|
||||
// // FIXME: remove eventFetch
|
||||
// // eventFetch(`/events/${event.uuid}/ics`, this.$store, { responseType: 'arraybuffer' })
|
||||
// // .then(response => response.text())
|
||||
// // .then((response) => {
|
||||
// // const blob = new Blob([ response ], { type: 'text/calendar' });
|
||||
// // const link = document.createElement('a');
|
||||
// // link.href = window.URL.createObjectURL(blob);
|
||||
// // link.download = `${event.title}.ics`;
|
||||
// // document.body.appendChild(link);
|
||||
// // link.click();
|
||||
// // document.body.removeChild(link);
|
||||
// // });
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
||||
<style scoped></style>
|
||||
|
||||
@@ -1,13 +1,33 @@
|
||||
<template>
|
||||
<div class="section container">
|
||||
<h1 class="title">{{ $t('Explore') }}</h1>
|
||||
<h1 class="title">{{ $t("Explore") }}</h1>
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<form @submit.prevent="submit()">
|
||||
<b-field :label="$t('Event')" grouped group-multiline label-position="on-border">
|
||||
<b-input icon="magnify" type="search" size="is-large" expanded v-model="searchTerm" :placeholder="$t('For instance: London, Taekwondo, Architecture…')" />
|
||||
<b-field
|
||||
:label="$t('Event')"
|
||||
grouped
|
||||
group-multiline
|
||||
label-position="on-border"
|
||||
label-for="search"
|
||||
>
|
||||
<b-input
|
||||
icon="magnify"
|
||||
type="search"
|
||||
id="search"
|
||||
size="is-large"
|
||||
expanded
|
||||
v-model="searchTerm"
|
||||
:placeholder="$t('For instance: London, Taekwondo, Architecture…')"
|
||||
/>
|
||||
<p class="control">
|
||||
<b-button @click="submit" type="is-info" size="is-large" v-bind:disabled="searchTerm.trim().length === 0">{{ $t('Search') }}</b-button>
|
||||
<b-button
|
||||
@click="submit"
|
||||
type="is-info"
|
||||
size="is-large"
|
||||
v-bind:disabled="searchTerm.trim().length === 0"
|
||||
>{{ $t("Search") }}</b-button
|
||||
>
|
||||
</p>
|
||||
</b-field>
|
||||
</form>
|
||||
@@ -15,27 +35,25 @@
|
||||
</section>
|
||||
<section class="events-featured">
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<h3 class="title">{{ $t('Featured events') }}</h3>
|
||||
<h2 class="title">{{ $t("Featured events") }}</h2>
|
||||
<div v-if="events.length > 0" class="columns is-multiline">
|
||||
<div class="column is-one-third-desktop" v-for="event in events" :key="event.uuid">
|
||||
<EventCard
|
||||
:event="event"
|
||||
/>
|
||||
<EventCard :event="event" />
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-else-if="events.length === 0 && $apollo.loading === false" type="is-danger">
|
||||
{{ $t('No events found') }}
|
||||
</b-message>
|
||||
<b-message v-else-if="events.length === 0 && $apollo.loading === false" type="is-danger">{{
|
||||
$t("No events found")
|
||||
}}</b-message>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import { FETCH_EVENTS } from '@/graphql/event';
|
||||
import { IEvent } from '@/types/event.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { FETCH_EVENTS } from "@/graphql/event";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -49,46 +67,50 @@ import { RouteName } from '@/router';
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t('Explore') as string,
|
||||
title: this.$t("Explore") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Explore extends Vue {
|
||||
events: IEvent[] = [];
|
||||
searchTerm: string = '';
|
||||
|
||||
searchTerm = "";
|
||||
|
||||
submit() {
|
||||
this.$router.push({ name: RouteName.SEARCH, params: { searchTerm: this.searchTerm } });
|
||||
this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
params: { searchTerm: this.searchTerm },
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/variables.scss";
|
||||
@import "@/variables.scss";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
main > .container {
|
||||
background: $white;
|
||||
|
||||
.hero-body {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.hero-body {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h1.title {
|
||||
margin-top: 1.5rem;
|
||||
h1.title {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
h3.title {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.events-featured {
|
||||
margin: 25px auto;
|
||||
|
||||
.columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
|
||||
h3.title {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.events-featured {
|
||||
margin: 25px auto;
|
||||
|
||||
.columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,80 +1,104 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1 class="title">
|
||||
{{ $t('My events') }}
|
||||
</h1>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<section v-if="futureParticipations.length > 0">
|
||||
<subtitle>
|
||||
{{ $t('Upcoming') }}
|
||||
</subtitle>
|
||||
<transition-group name="list" tag="p">
|
||||
<div v-for="month in monthlyFutureParticipations" :key="month[0]">
|
||||
<h3 class="upcoming-month">{{ month[0] }}</h3>
|
||||
<EventListCard
|
||||
v-for="participation in month[1]"
|
||||
:key="participation.id"
|
||||
:participation="participation"
|
||||
:options="{ hideDate: false }"
|
||||
@eventDeleted="eventDeleted"
|
||||
class="participation"
|
||||
/>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div class="columns is-centered">
|
||||
<b-button class="column is-narrow"
|
||||
v-if="hasMoreFutureParticipations && (futureParticipations.length === limit)" @click="loadMoreFutureParticipations" size="is-large" type="is-primary">{{ $t('Load more') }}</b-button>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="drafts.length > 0">
|
||||
<subtitle>
|
||||
{{ $t('Drafts') }}
|
||||
</subtitle>
|
||||
<div class="columns is-multiline">
|
||||
<EventCard
|
||||
v-for="draft in drafts"
|
||||
:key="draft.uuid"
|
||||
:event="draft"
|
||||
class="is-one-quarter-desktop column"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="pastParticipations.length > 0">
|
||||
<subtitle>
|
||||
{{ $t('Past events') }}
|
||||
</subtitle>
|
||||
<transition-group name="list" tag="p">
|
||||
<div v-for="month in monthlyPastParticipations" :key="month[0]">
|
||||
<h3>{{ month[0] }}</h3>
|
||||
<EventListCard
|
||||
v-for="participation in month[1]"
|
||||
:key="participation.id"
|
||||
:participation="participation"
|
||||
:options="{ hideDate: false }"
|
||||
@eventDeleted="eventDeleted"
|
||||
class="participation"
|
||||
/>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div class="columns is-centered">
|
||||
<b-button class="column is-narrow"
|
||||
v-if="hasMorePastParticipations && (pastParticipations.length === limit)" @click="loadMorePastParticipations" size="is-large" type="is-primary">{{ $t('Load more') }}</b-button>
|
||||
</div>
|
||||
</section>
|
||||
<b-message v-if="futureParticipations.length === 0 && pastParticipations.length === 0 && $apollo.loading === false" type="is-danger">
|
||||
{{ $t('No events found') }}
|
||||
</b-message>
|
||||
<section class="section container">
|
||||
<h1 class="title">
|
||||
{{ $t("My events") }}
|
||||
</h1>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<section v-if="futureParticipations.length > 0">
|
||||
<subtitle>
|
||||
{{ $t("Upcoming") }}
|
||||
</subtitle>
|
||||
<transition-group name="list" tag="p">
|
||||
<div v-for="month in monthlyFutureParticipations" :key="month[0]">
|
||||
<span class="upcoming-month">{{ month[0] }}</span>
|
||||
<EventListCard
|
||||
v-for="participation in month[1]"
|
||||
:key="participation.id"
|
||||
:participation="participation"
|
||||
:options="{ hideDate: false }"
|
||||
@eventDeleted="eventDeleted"
|
||||
class="participation"
|
||||
/>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div class="columns is-centered">
|
||||
<b-button
|
||||
class="column is-narrow"
|
||||
v-if="hasMoreFutureParticipations && futureParticipations.length === limit"
|
||||
@click="loadMoreFutureParticipations"
|
||||
size="is-large"
|
||||
type="is-primary"
|
||||
>{{ $t("Load more") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="drafts.length > 0">
|
||||
<subtitle>
|
||||
{{ $t("Drafts") }}
|
||||
</subtitle>
|
||||
<div class="columns is-multiline">
|
||||
<EventCard
|
||||
v-for="draft in drafts"
|
||||
:key="draft.uuid"
|
||||
:event="draft"
|
||||
class="is-one-quarter-desktop column"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="pastParticipations.length > 0">
|
||||
<subtitle>
|
||||
{{ $t("Past events") }}
|
||||
</subtitle>
|
||||
<transition-group name="list" tag="p">
|
||||
<div v-for="month in monthlyPastParticipations" :key="month[0]">
|
||||
<span>{{ month[0] }}</span>
|
||||
<EventListCard
|
||||
v-for="participation in month[1]"
|
||||
:key="participation.id"
|
||||
:participation="participation"
|
||||
:options="{ hideDate: false }"
|
||||
@eventDeleted="eventDeleted"
|
||||
class="participation"
|
||||
/>
|
||||
</div>
|
||||
</transition-group>
|
||||
<div class="columns is-centered">
|
||||
<b-button
|
||||
class="column is-narrow"
|
||||
v-if="hasMorePastParticipations && pastParticipations.length === limit"
|
||||
@click="loadMorePastParticipations"
|
||||
size="is-large"
|
||||
type="is-primary"
|
||||
>{{ $t("Load more") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
<b-message
|
||||
v-if="
|
||||
futureParticipations.length === 0 &&
|
||||
pastParticipations.length === 0 &&
|
||||
$apollo.loading === false
|
||||
"
|
||||
type="is-danger"
|
||||
>
|
||||
{{ $t("No events found") }}
|
||||
</b-message>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { LOGGED_USER_PARTICIPATIONS, LOGGED_USER_DRAFTS } from '@/graphql/actor';
|
||||
import { EventModel, IEvent, IParticipant, Participant, ParticipantRole } from '@/types/event.model';
|
||||
import EventListCard from '@/components/Event/EventListCard.vue';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LOGGED_USER_PARTICIPATIONS, LOGGED_USER_DRAFTS } from "../../graphql/actor";
|
||||
import {
|
||||
EventModel,
|
||||
IEvent,
|
||||
IParticipant,
|
||||
Participant,
|
||||
ParticipantRole,
|
||||
} from "../../types/event.model";
|
||||
import EventListCard from "../../components/Event/EventListCard.vue";
|
||||
import EventCard from "../../components/Event/EventCard.vue";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -85,76 +109,91 @@ import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
apollo: {
|
||||
futureParticipations: {
|
||||
query: LOGGED_USER_PARTICIPATIONS,
|
||||
fetchPolicy: 'network-only',
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
afterDateTime: (new Date()).toISOString(),
|
||||
afterDateTime: new Date().toISOString(),
|
||||
},
|
||||
update: data => data.loggedUser.participations.map(participation => new Participant(participation)),
|
||||
update: (data) =>
|
||||
data.loggedUser.participations.elements.map(
|
||||
(participation: IParticipant) => new Participant(participation)
|
||||
),
|
||||
},
|
||||
drafts: {
|
||||
query: LOGGED_USER_DRAFTS,
|
||||
fetchPolicy: 'network-only',
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
},
|
||||
update: data => data.loggedUser.drafts.map(event => new EventModel(event)),
|
||||
update: (data) => data.loggedUser.drafts.map((event: IEvent) => new EventModel(event)),
|
||||
},
|
||||
pastParticipations: {
|
||||
query: LOGGED_USER_PARTICIPATIONS,
|
||||
fetchPolicy: 'network-only',
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
beforeDateTime: (new Date()).toISOString(),
|
||||
beforeDateTime: new Date().toISOString(),
|
||||
},
|
||||
update: data => data.loggedUser.participations.map(participation => new Participant(participation)),
|
||||
update: (data) =>
|
||||
data.loggedUser.participations.elements.map(
|
||||
(participation: IParticipant) => new Participant(participation)
|
||||
),
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t('My events') as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t("My events") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class MyEvents extends Vue {
|
||||
futurePage: number = 1;
|
||||
pastPage: number = 1;
|
||||
limit: number = 10;
|
||||
futurePage = 1;
|
||||
|
||||
pastPage = 1;
|
||||
|
||||
limit = 10;
|
||||
|
||||
futureParticipations: IParticipant[] = [];
|
||||
hasMoreFutureParticipations: boolean = true;
|
||||
|
||||
hasMoreFutureParticipations = true;
|
||||
|
||||
pastParticipations: IParticipant[] = [];
|
||||
hasMorePastParticipations: boolean = true;
|
||||
|
||||
hasMorePastParticipations = true;
|
||||
|
||||
drafts: IEvent[] = [];
|
||||
|
||||
private monthlyParticipations(participations: IParticipant[]): Map<string, Participant[]> {
|
||||
const res = participations.filter(({ event, role }) => event.beginsOn != null && role !== ParticipantRole.REJECTED);
|
||||
static monthlyParticipations(participations: IParticipant[]): Map<string, Participant[]> {
|
||||
const res = participations.filter(
|
||||
({ event, role }) => event.beginsOn != null && role !== ParticipantRole.REJECTED
|
||||
);
|
||||
res.sort(
|
||||
(a: IParticipant, b: IParticipant) => a.event.beginsOn.getTime() - b.event.beginsOn.getTime(),
|
||||
);
|
||||
(a: IParticipant, b: IParticipant) => a.event.beginsOn.getTime() - b.event.beginsOn.getTime()
|
||||
);
|
||||
return res.reduce((acc: Map<string, IParticipant[]>, participation: IParticipant) => {
|
||||
const month = (new Date(participation.event.beginsOn)).toLocaleDateString(undefined, { year: 'numeric', month: 'long' });
|
||||
const participations: IParticipant[] = acc.get(month) || [];
|
||||
participations.push(participation);
|
||||
acc.set(month, participations);
|
||||
const month = new Date(participation.event.beginsOn).toLocaleDateString(undefined, {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
});
|
||||
const filteredParticipations: IParticipant[] = acc.get(month) || [];
|
||||
filteredParticipations.push(participation);
|
||||
acc.set(month, filteredParticipations);
|
||||
return acc;
|
||||
}, new Map());
|
||||
}, new Map());
|
||||
}
|
||||
|
||||
get monthlyFutureParticipations(): Map<string, Participant[]> {
|
||||
return this.monthlyParticipations(this.futureParticipations);
|
||||
return MyEvents.monthlyParticipations(this.futureParticipations);
|
||||
}
|
||||
|
||||
get monthlyPastParticipations(): Map<string, Participant[]> {
|
||||
return this.monthlyParticipations(this.pastParticipations);
|
||||
return MyEvents.monthlyParticipations(this.pastParticipations);
|
||||
}
|
||||
|
||||
loadMoreFutureParticipations() {
|
||||
@@ -183,12 +222,12 @@ export default class MyEvents extends Vue {
|
||||
loadMorePastParticipations() {
|
||||
this.pastPage += 1;
|
||||
this.$apollo.queries.pastParticipations.fetchMore({
|
||||
// New variables
|
||||
// New variables
|
||||
variables: {
|
||||
page: this.pastPage,
|
||||
limit: this.limit,
|
||||
},
|
||||
// Transform the previous result with new data
|
||||
// Transform the previous result with new data
|
||||
updateQuery: (previousResult, { fetchMoreResult }) => {
|
||||
const newParticipations = fetchMoreResult.loggedUser.participations;
|
||||
this.hasMorePastParticipations = newParticipations.length === this.limit;
|
||||
@@ -203,28 +242,32 @@ export default class MyEvents extends Vue {
|
||||
});
|
||||
}
|
||||
|
||||
eventDeleted(eventid) {
|
||||
this.futureParticipations = this.futureParticipations.filter(participation => participation.event.id !== eventid);
|
||||
this.pastParticipations = this.pastParticipations.filter(participation => participation.event.id !== eventid);
|
||||
eventDeleted(eventid: string) {
|
||||
this.futureParticipations = this.futureParticipations.filter(
|
||||
(participation) => participation.event.id !== eventid
|
||||
);
|
||||
this.pastParticipations = this.pastParticipations.filter(
|
||||
(participation) => participation.event.id !== eventid
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables";
|
||||
@import "../../variables";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.participation {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
.participation {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
section {
|
||||
.upcoming-month {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
section {
|
||||
.upcoming-month {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,80 +1,92 @@
|
||||
import {ParticipantRole} from "@/types/event.model";
|
||||
<template>
|
||||
<main class="container">
|
||||
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
|
||||
<b-tab-item>
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple" />
|
||||
<span>{{ $t('Participants')}} <b-tag rounded> {{ participantStats.going }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="participants && participants.total > 0">
|
||||
<h2 class="title">{{ $t('Participants') }}</h2>
|
||||
<ParticipationTable
|
||||
:data="participants.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:showRole="true"
|
||||
:total="participants.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => participantPage = page"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item :disabled="participantStats.notApproved === 0">
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple-plus" />
|
||||
<span>{{ $t('Requests') }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="queue && queue.total > 0">
|
||||
<h2 class="title">{{ $t('Waiting list') }}</h2>
|
||||
<ParticipationTable
|
||||
:data="queue.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="queue.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => queuePage = page"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item :disabled="participantStats.rejected === 0">
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple-minus"></b-icon>
|
||||
<span>{{ $t('Rejected')}} <b-tag rounded> {{ participantStats.rejected }} </b-tag> </span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="rejected && rejected.total > 0">
|
||||
<h2 class="title">{{ $t('Rejected participations') }}</h2>
|
||||
<ParticipationTable
|
||||
:data="rejected.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="rejected.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => rejectedPage = page"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
</b-tabs>
|
||||
</main>
|
||||
<main class="container">
|
||||
<b-tabs type="is-boxed" v-if="event" v-model="activeTab">
|
||||
<b-tab-item>
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple" />
|
||||
<span
|
||||
>{{ $t("Participants") }} <b-tag rounded> {{ participantStats.going }} </b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="participants && participants.total > 0">
|
||||
<h2 class="title">{{ $t("Participants") }}</h2>
|
||||
<ParticipationTable
|
||||
:data="participants.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:showRole="true"
|
||||
:total="participants.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => (participantPage = page)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item :disabled="participantStats.notApproved === 0">
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple-plus" />
|
||||
<span
|
||||
>{{ $t("Requests") }} <b-tag rounded> {{ participantStats.notApproved }} </b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="queue && queue.total > 0">
|
||||
<h2 class="title">{{ $t("Waiting list") }}</h2>
|
||||
<ParticipationTable
|
||||
:data="queue.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="queue.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => (queuePage = page)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
<b-tab-item :disabled="participantStats.rejected === 0">
|
||||
<template slot="header">
|
||||
<b-icon icon="account-multiple-minus"></b-icon>
|
||||
<span
|
||||
>{{ $t("Rejected") }} <b-tag rounded> {{ participantStats.rejected }} </b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<template>
|
||||
<section v-if="rejected && rejected.total > 0">
|
||||
<h2 class="title">{{ $t("Rejected participations") }}</h2>
|
||||
<ParticipationTable
|
||||
:data="rejected.elements"
|
||||
:accept-participant="acceptParticipant"
|
||||
:refuse-participant="refuseParticipant"
|
||||
:total="rejected.total"
|
||||
:perPage="PARTICIPANTS_PER_PAGE"
|
||||
@page-change="(page) => (rejectedPage = page)"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
</b-tab-item>
|
||||
</b-tabs>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { IEvent, IEventParticipantStats, IParticipant, Participant, ParticipantRole } from '@/types/event.model';
|
||||
import { PARTICIPANTS, UPDATE_PARTICIPANT } from '@/graphql/event';
|
||||
import ParticipantCard from '@/components/Account/ParticipantCard.vue';
|
||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { IPerson } from '@/types/actor';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import ParticipationTable from '@/components/Event/ParticipationTable.vue';
|
||||
import { Paginate } from '@/types/paginate';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import {
|
||||
IEvent,
|
||||
IEventParticipantStats,
|
||||
IParticipant,
|
||||
Participant,
|
||||
ParticipantRole,
|
||||
} from "../../types/event.model";
|
||||
import { PARTICIPANTS, UPDATE_PARTICIPANT } from "../../graphql/event";
|
||||
import ParticipantCard from "../../components/Account/ParticipantCard.vue";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import { IPerson } from "../../types/actor";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import ParticipationTable from "../../components/Event/ParticipationTable.vue";
|
||||
import { Paginate } from "../../types/paginate";
|
||||
|
||||
const PARTICIPANTS_PER_PAGE = 20;
|
||||
const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
@@ -115,7 +127,9 @@ const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update(data) { return this.dataTransform(data); },
|
||||
update(data) {
|
||||
return this.dataTransform(data);
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
@@ -131,7 +145,9 @@ const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update(data) { return this.dataTransform(data); },
|
||||
update(data) {
|
||||
return this.dataTransform(data);
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
@@ -147,45 +163,57 @@ const MESSAGE_ELLIPSIS_LENGTH = 130;
|
||||
actorId: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
update(data) { return this.dataTransform(data); },
|
||||
update(data) {
|
||||
return this.dataTransform(data);
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
},
|
||||
filters: {
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat('…'),
|
||||
ellipsize: (text?: string) => text && text.substr(0, MESSAGE_ELLIPSIS_LENGTH).concat("…"),
|
||||
},
|
||||
})
|
||||
export default class Participants extends Vue {
|
||||
@Prop({ required: true }) eventId!: string;
|
||||
page: number = 1;
|
||||
limit: number = 10;
|
||||
|
||||
page = 1;
|
||||
|
||||
limit = 10;
|
||||
|
||||
participants!: Paginate<IParticipant>;
|
||||
participantPage: number = 1;
|
||||
|
||||
participantPage = 1;
|
||||
|
||||
queue!: Paginate<IParticipant>;
|
||||
queuePage: number = 1;
|
||||
|
||||
queuePage = 1;
|
||||
|
||||
rejected!: Paginate<IParticipant>;
|
||||
rejectedPage: number = 1;
|
||||
|
||||
rejectedPage = 1;
|
||||
|
||||
event!: IEvent;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
ParticipantRole = ParticipantRole;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
hasMoreParticipants: boolean = false;
|
||||
activeTab: number = 0;
|
||||
hasMoreParticipants = false;
|
||||
|
||||
activeTab = 0;
|
||||
|
||||
PARTICIPANTS_PER_PAGE = PARTICIPANTS_PER_PAGE;
|
||||
|
||||
dataTransform(data): Paginate<Participant> {
|
||||
dataTransform(data: { event: IEvent }): Paginate<Participant> {
|
||||
return {
|
||||
total: data.event.participants.total,
|
||||
elements: data.event.participants.elements.map(participation => new Participant(participation)),
|
||||
elements: data.event.participants.elements.map(
|
||||
(participation: IParticipant) => new Participant(participation)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -194,10 +222,13 @@ export default class Participants extends Vue {
|
||||
return this.event.participantStats;
|
||||
}
|
||||
|
||||
@Watch('participantStats', { deep: true })
|
||||
@Watch("participantStats", { deep: true })
|
||||
watchParticipantStats(stats: IEventParticipantStats) {
|
||||
if (!stats) return;
|
||||
if ((stats.notApproved === 0 && this.activeTab === 1) || stats.rejected === 0 && this.activeTab === 2 ) {
|
||||
if (
|
||||
(stats.notApproved === 0 && this.activeTab === 1) ||
|
||||
(stats.rejected === 0 && this.activeTab === 2)
|
||||
) {
|
||||
this.activeTab = 0;
|
||||
}
|
||||
}
|
||||
@@ -236,8 +267,12 @@ export default class Participants extends Vue {
|
||||
},
|
||||
});
|
||||
if (data) {
|
||||
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.rejected.elements = this.rejected.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.queue.elements = this.queue.elements.filter(
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.rejected.elements = this.rejected.elements.filter(
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.event.participantStats.going += 1;
|
||||
if (participant.role === ParticipantRole.NOT_APPROVED) {
|
||||
this.event.participantStats.notApproved -= 1;
|
||||
@@ -265,9 +300,11 @@ export default class Participants extends Vue {
|
||||
});
|
||||
if (data) {
|
||||
this.event.participants.elements = this.event.participants.elements.filter(
|
||||
participant => participant.id !== data.updateParticipation.id,
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.queue.elements = this.queue.elements.filter(
|
||||
(participant) => participant.id !== data.updateParticipation.id
|
||||
);
|
||||
this.queue.elements = this.queue.elements.filter(participant => participant.id !== data.updateParticipation.id);
|
||||
this.event.participantStats.rejected += 1;
|
||||
if (participant.role === ParticipantRole.PARTICIPANT) {
|
||||
this.event.participantStats.participant -= 1;
|
||||
@@ -277,7 +314,9 @@ export default class Participants extends Vue {
|
||||
this.event.participantStats.notApproved -= 1;
|
||||
}
|
||||
participant.role = ParticipantRole.REJECTED;
|
||||
this.rejected.elements = this.rejected.elements.filter(participantIn => participantIn.id !== participant.id);
|
||||
this.rejected.elements = this.rejected.elements.filter(
|
||||
(participantIn) => participantIn.id !== participant.id
|
||||
);
|
||||
this.rejected.elements.push(participant);
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -289,16 +328,16 @@ export default class Participants extends Vue {
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
section {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
section {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
nav.tabs li {
|
||||
margin: 3rem 0 0;
|
||||
}
|
||||
nav.tabs li {
|
||||
margin: 3rem 0 0;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
background: #fff;
|
||||
}
|
||||
.tab-content {
|
||||
background: #fff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,50 +1,41 @@
|
||||
<template>
|
||||
<div class="container root">
|
||||
<h1>{{ $t('Create a new group') }}</h1>
|
||||
<section class="section container">
|
||||
<h1>{{ $t("Create a new group") }}</h1>
|
||||
|
||||
<div>
|
||||
<b-field :label="$t('Group name')">
|
||||
<b-input aria-required="true" required v-model="group.preferred_username"/>
|
||||
<b-input aria-required="true" required v-model="group.preferredUsername" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Group full name')">
|
||||
<b-input aria-required="true" required v-model="group.name"/>
|
||||
<b-input aria-required="true" required v-model="group.name" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Description')">
|
||||
<b-input aria-required="true" required v-model="group.description" type="textarea"/>
|
||||
<b-input aria-required="true" required v-model="group.summary" type="textarea" />
|
||||
</b-field>
|
||||
|
||||
<div>
|
||||
Avatar
|
||||
<picture-upload v-model="avatarFile"></picture-upload>
|
||||
<picture-upload v-model="avatarFile" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
Banner
|
||||
<picture-upload v-model="avatarFile"></picture-upload>
|
||||
<picture-upload v-model="avatarFile" />
|
||||
</div>
|
||||
|
||||
<button class="button is-primary" @click="createGroup()">
|
||||
{{ $t('Create my group') }}
|
||||
</button>
|
||||
<button class="button is-primary" @click="createGroup()">{{ $t("Create my group") }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.root {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Group, IPerson } from '@/types/actor';
|
||||
import { CREATE_GROUP, CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { RouteName } from '@/router';
|
||||
import PictureUpload from '@/components/PictureUpload.vue';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { Group, IPerson } from "@/types/actor";
|
||||
import { CREATE_GROUP, CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||
import PictureUpload from "@/components/PictureUpload.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -62,6 +53,7 @@ export default class CreateGroup extends Vue {
|
||||
group = new Group();
|
||||
|
||||
avatarFile: File | null = null;
|
||||
|
||||
bannerFile: File | null = null;
|
||||
|
||||
async createGroup() {
|
||||
@@ -74,10 +66,15 @@ export default class CreateGroup extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
await this.$router.push({ name: RouteName.GROUP, params: { identityName: this.group.preferredUsername } });
|
||||
await this.$router.push({
|
||||
name: RouteName.GROUP,
|
||||
params: { identityName: this.group.preferredUsername },
|
||||
});
|
||||
|
||||
this.$notifier.success(
|
||||
this.$t('Group {displayName} created', { displayName: this.group.displayName() }) as string,
|
||||
this.$t("Group {displayName} created", {
|
||||
displayName: this.group.displayName(),
|
||||
}) as string
|
||||
);
|
||||
} catch (err) {
|
||||
this.handleError(err);
|
||||
@@ -114,7 +111,12 @@ export default class CreateGroup extends Vue {
|
||||
creatorActorId: this.currentActor.id,
|
||||
};
|
||||
|
||||
return Object.assign({}, this.group, avatarObj, bannerObj, currentActor);
|
||||
return {
|
||||
...this.group,
|
||||
...avatarObj,
|
||||
...bannerObj,
|
||||
...currentActor,
|
||||
};
|
||||
}
|
||||
|
||||
private handleError(err: any) {
|
||||
|
||||
@@ -1,66 +1,194 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<div v-if="group">
|
||||
<div class="card-image" v-if="group.banner.url">
|
||||
<figure class="image">
|
||||
<img :src="group.banner.url">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48">
|
||||
<img :src="group.avatar.url">
|
||||
<div class="container is-widescreen">
|
||||
<div
|
||||
v-if="group && groupMemberships && groupMemberships.includes(group.id)"
|
||||
class="block-container"
|
||||
>
|
||||
<div class="block-column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs" v-if="group">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.MY_GROUPS }">{{ $t("My groups") }}</router-link>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(group.preferredUsername) },
|
||||
}"
|
||||
>{{ group.name }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section class="presentation">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<h1>{{ group.name }}</h1>
|
||||
<small class="has-text-grey">@{{ usernameWithDomain(group) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="members">
|
||||
<figure
|
||||
class="image is-48x48"
|
||||
:title="
|
||||
$t(`@{username} ({role})`, {
|
||||
username: usernameWithDomain(member.actor),
|
||||
role: member.role,
|
||||
})
|
||||
"
|
||||
v-for="member in group.members.elements"
|
||||
:key="member.actor.id"
|
||||
>
|
||||
<img
|
||||
class="is-rounded"
|
||||
:src="member.actor.avatar.url"
|
||||
v-if="member.actor.avatar"
|
||||
alt
|
||||
/>
|
||||
</figure>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div class="organized-events-wrapper" v-if="group.organizedEvents.total > 0">
|
||||
<EventMinimalistCard
|
||||
v-for="event in group.organizedEvents.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
class="organized-event"
|
||||
/>
|
||||
</div>
|
||||
<router-link :to="{}">{{ $t("View all upcoming events") }}</router-link>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Resources") }}</subtitle>
|
||||
<div v-if="group.resources.elements.length > 0">
|
||||
<div v-for="resource in group.resources.elements" :key="resource.id">
|
||||
<resource-item
|
||||
:resource="resource"
|
||||
v-if="resource.type !== 'folder'"
|
||||
:inline="true"
|
||||
/>
|
||||
<folder-item :resource="resource" :group="group" v-else :inline="true" />
|
||||
</div>
|
||||
</div>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.RESOURCE_FOLDER_ROOT,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("View all resources") }}</router-link
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
<div class="block-column">
|
||||
<section>
|
||||
<subtitle>{{ $t("Public page") }}</subtitle>
|
||||
<p>{{ $t("Followed by {count} persons", { count: group.members.total }) }}</p>
|
||||
<b-button type="is-light">{{ $t("Edit biography") }}</b-button>
|
||||
<b-button type="is-primary">{{ $t("Post a public message") }}</b-button>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Ongoing tasks") }}</subtitle>
|
||||
<div
|
||||
v-if="group.todoLists.elements.length > 0"
|
||||
v-for="todoList in group.todoLists.elements"
|
||||
:key="todoList.id"
|
||||
>
|
||||
<router-link :to="{ name: RouteName.TODO_LIST, params: { id: todoList.id } }">
|
||||
<h2 class="is-size-3">
|
||||
{{
|
||||
$tc("{title} ({count} todos)", todoList.todos.total, {
|
||||
count: todoList.todos.total,
|
||||
title: todoList.title,
|
||||
})
|
||||
}}
|
||||
</h2>
|
||||
</router-link>
|
||||
<compact-todo
|
||||
:todo="todo"
|
||||
v-for="todo in todoList.todos.elements.slice(0, 3)"
|
||||
:key="todo.id"
|
||||
/>
|
||||
</div>
|
||||
<router-link :to="{ name: RouteName.TODO_LISTS }">{{ $t("View all todos") }}</router-link>
|
||||
</section>
|
||||
<section>
|
||||
<subtitle>{{ $t("Discussions") }}</subtitle>
|
||||
<conversation-list-item
|
||||
v-if="group.conversations.total > 0"
|
||||
v-for="conversation in group.conversations.elements"
|
||||
:key="conversation.id"
|
||||
:conversation="conversation"
|
||||
/>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.CONVERSATION_LIST,
|
||||
params: { preferredUsername: usernameWithDomain(group) },
|
||||
}"
|
||||
>{{ $t("View all conversations") }}</router-link
|
||||
>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="group">
|
||||
<section class="presentation">
|
||||
<div class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-128x128" v-if="group.avatar">
|
||||
<img :src="group.avatar.url" alt />
|
||||
</figure>
|
||||
<b-icon v-else size="is-large" icon="account-group" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<p class="title">{{ group.name }}</p>
|
||||
<p class="subtitle">@{{ group.preferredUsername }}</p>
|
||||
<h2>{{ group.name }}</h2>
|
||||
<small class="has-text-grey">@{{ usernameWithDomain(group) }}</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p v-html="group.summary"></p>
|
||||
</div>
|
||||
</div>
|
||||
<section class="box" v-if="group.organizedEvents.length > 0">
|
||||
<subtitle>
|
||||
{{ $t('Organized') }}
|
||||
</subtitle>
|
||||
<div class="columns">
|
||||
<EventCard
|
||||
v-for="event in group.organizedEvents"
|
||||
:event="event"
|
||||
:options="{ hideDetails: true }"
|
||||
:key="event.uuid"
|
||||
class="column is-one-third"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section v-if="group.members.length > 0">
|
||||
<subtitle>
|
||||
{{ $t('Members') }}
|
||||
</subtitle>
|
||||
<div class="columns">
|
||||
<span
|
||||
v-for="member in group.members"
|
||||
:key="member.actor.preferredUsername"
|
||||
>{{ member.actor.preferredUsername }}</span>
|
||||
<section>
|
||||
<subtitle>{{ $t("Upcoming events") }}</subtitle>
|
||||
<div class="organized-events-wrapper" v-if="group.organizedEvents.total > 0">
|
||||
<EventMinimalistCard
|
||||
v-for="event in group.organizedEvents.elements"
|
||||
:event="event"
|
||||
:key="event.uuid"
|
||||
class="organized-event"
|
||||
/>
|
||||
<router-link :to="{}">{{ $t("View all upcoming events") }}</router-link>
|
||||
</div>
|
||||
<span v-else>{{ $t("No public upcoming events") }}</span>
|
||||
</section>
|
||||
<!-- {{ group }}-->
|
||||
<section>
|
||||
<subtitle>{{ $t("Latest posts") }}</subtitle>
|
||||
</section>
|
||||
</div>
|
||||
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
|
||||
{{ $t('No group found') }}
|
||||
{{ $t("No group found") }}
|
||||
</b-message>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import { FETCH_GROUP, CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { IGroup } from '@/types/actor';
|
||||
import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import EventCard from "@/components/Event/EventCard.vue";
|
||||
import { FETCH_GROUP, CURRENT_ACTOR_CLIENT, PERSON_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import { IActor, IGroup, IPerson, usernameWithDomain } from "@/types/actor";
|
||||
import Subtitle from "@/components/Utils/Subtitle.vue";
|
||||
import CompactTodo from "@/components/Todo/CompactTodo.vue";
|
||||
import EventMinimalistCard from "@/components/Event/EventMinimalistCard.vue";
|
||||
import ConversationListItem from "@/components/Conversation/ConversationListItem.vue";
|
||||
import ResourceItem from "@/components/Resource/ResourceItem.vue";
|
||||
import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -68,49 +196,141 @@ import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
name: this.preferredUsername,
|
||||
};
|
||||
},
|
||||
},
|
||||
currentActor: {
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
person: {
|
||||
query: PERSON_MEMBERSHIPS,
|
||||
variables() {
|
||||
return {
|
||||
id: this.currentActor.id,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.currentActor || !this.currentActor.id;
|
||||
},
|
||||
},
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
components: {
|
||||
ConversationListItem,
|
||||
EventMinimalistCard,
|
||||
CompactTodo,
|
||||
Subtitle,
|
||||
EventCard,
|
||||
FolderItem,
|
||||
ResourceItem,
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
// @ts-ignore
|
||||
title: this.groupTitle,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
meta: [
|
||||
// @ts-ignore
|
||||
{ name: "description", content: this.groupSummary },
|
||||
],
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Group extends Vue {
|
||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||
|
||||
currentActor!: IActor;
|
||||
|
||||
person!: IPerson;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
loading = true;
|
||||
|
||||
created() {
|
||||
this.fetchData();
|
||||
RouteName = RouteName;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
@Watch("currentActor")
|
||||
watchCurrentActor(currentActor: IActor, oldActor: IActor) {
|
||||
if (currentActor.id && oldActor && currentActor.id !== oldActor.id) {
|
||||
this.$apollo.queries.group.refetch();
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('$route')
|
||||
onRouteChanged() {
|
||||
// call again the method if the route changes
|
||||
this.fetchData();
|
||||
get groupTitle() {
|
||||
if (!this.group) return undefined;
|
||||
return this.group.preferredUsername;
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(`/actors/${this.name}`, this.$store)
|
||||
// .then(response => response.json())
|
||||
// .then((response) => {
|
||||
// this.group = response.data;
|
||||
// this.loading = false;
|
||||
// console.log(this.group);
|
||||
// });
|
||||
get groupSummary() {
|
||||
if (!this.group) return undefined;
|
||||
return this.group.summary;
|
||||
}
|
||||
|
||||
get groupMemberships() {
|
||||
if (!this.person || !this.person.id) return undefined;
|
||||
return this.person.memberships.elements.map(({ parent: { id } }) => id);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
section.container {
|
||||
min-height: 30em;
|
||||
div.container {
|
||||
background: white;
|
||||
margin-bottom: 3rem;
|
||||
padding: 2rem 0;
|
||||
|
||||
.block-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.block-column {
|
||||
flex: 1;
|
||||
margin: 0 2rem;
|
||||
|
||||
section {
|
||||
/deep/ h2 span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&.presentation {
|
||||
.members {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.organized-events-wrapper {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.organized-event {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.presentation {
|
||||
.media-left {
|
||||
span.icon.is-large {
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
|
||||
/deep/ i.mdi.mdi-account-group.mdi-48px:before {
|
||||
font-size: 100px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media-content {
|
||||
h2 {
|
||||
color: #3c376e;
|
||||
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,78 +1,62 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<h1>
|
||||
{{ $t('Group List') }}
|
||||
</h1>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<section class="container section">
|
||||
<h1>{{ $t("Group List") }} ({{ groups.total }})</h1>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<div class="columns">
|
||||
<GroupCard
|
||||
v-for="group in groups"
|
||||
v-for="group in groups.elements"
|
||||
:key="group.uuid"
|
||||
:group="group"
|
||||
class="column is-one-quarter-desktop is-half-mobile"
|
||||
/>
|
||||
</div>
|
||||
<router-link class="button" :to="{ name: RouteName.CREATE_GROUP }">
|
||||
{{ $t('Create group') }}
|
||||
</router-link>
|
||||
<router-link class="button" :to="{ name: RouteName.CREATE_GROUP }">{{
|
||||
$t("Create group")
|
||||
}}</router-link>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LIST_GROUPS } from "@/graphql/actor";
|
||||
import { Group, IGroup } from "@/types/actor";
|
||||
import GroupCard from "@/components/Group/GroupCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
apollo: {
|
||||
groups: {
|
||||
query: LIST_GROUPS,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
GroupCard,
|
||||
},
|
||||
})
|
||||
export default class GroupList extends Vue {
|
||||
groups = [];
|
||||
groups: { elements: IGroup[]; total: number } = { elements: [], total: 0 };
|
||||
|
||||
loading = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
//
|
||||
// usernameWithDomain(actor) {
|
||||
// return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
|
||||
// }
|
||||
|
||||
created() {
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
usernameWithDomain(actor) {
|
||||
return actor.username + (actor.domain === null ? '' : `@${actor.domain}`);
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch('/groups', this.$store)
|
||||
// .then(response => response.json())
|
||||
// .then((data) => {
|
||||
// console.log(data);
|
||||
// this.loading = false;
|
||||
// this.groups = data.data;
|
||||
// });
|
||||
}
|
||||
|
||||
deleteGroup(group) {
|
||||
const router = this.$router;
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(`/groups/${this.usernameWithDomain(group)}`, this.$store, { method: 'DELETE' })
|
||||
// .then(response => response.json())
|
||||
// .then(() => router.push('/groups'));
|
||||
}
|
||||
|
||||
viewActor(actor) {
|
||||
this.$router.push({
|
||||
name: RouteName.GROUP,
|
||||
params: { name: this.usernameWithDomain(actor) },
|
||||
});
|
||||
}
|
||||
|
||||
joinGroup(group) {
|
||||
const router = this.$router;
|
||||
// FIXME: remove eventFetch
|
||||
// eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' })
|
||||
// .then(response => response.json())
|
||||
// .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } }));
|
||||
}
|
||||
// viewActor(actor) {
|
||||
// this.$router.push({
|
||||
// name: RouteName.GROUP,
|
||||
// params: { name: this.usernameWithDomain(actor) },
|
||||
// });
|
||||
// }
|
||||
//
|
||||
// joinGroup(group) {
|
||||
// const router = this.$router;
|
||||
// // FIXME: remove eventFetch
|
||||
// // eventFetch(`/groups/${this.usernameWithDomain(group)}/join`, this.$store, { method: 'POST' })
|
||||
// // .then(response => response.json())
|
||||
// // .then(() => router.push({ name: 'Group', params: { name: this.usernameWithDomain(group) } }));
|
||||
// }
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
</style>
|
||||
|
||||
62
js/src/views/Group/GroupMembers.vue
Normal file
62
js/src/views/Group/GroupMembers.vue
Normal file
@@ -0,0 +1,62 @@
|
||||
<template>
|
||||
<section class="container section" v-if="group">
|
||||
<form @submit.prevent="inviteMember">
|
||||
<b-field :label="$t('Invite a new member')" custom-class="add-relay" horizontal>
|
||||
<b-field grouped expanded size="is-large">
|
||||
<p class="control">
|
||||
<b-input v-model="newMemberUsername" :placeholder="$t('Ex: someone@mobilizon.org')" />
|
||||
</p>
|
||||
<p class="control">
|
||||
<b-button type="is-primary" native-type="submit">{{ $t("Invite member") }}</b-button>
|
||||
</p>
|
||||
</b-field>
|
||||
</b-field>
|
||||
</form>
|
||||
<h1>{{ $t("Group Members") }} ({{ group.members.total }})</h1>
|
||||
<pre>{{ group.members }}</pre>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import RouteName from "../../router/name";
|
||||
import { FETCH_GROUP } from "../../graphql/actor";
|
||||
import { INVITE_MEMBER } from "../../graphql/member";
|
||||
import { IGroup } from "../../types/actor";
|
||||
import { IMember } from "../../types/actor/group.model";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
};
|
||||
},
|
||||
skip() {
|
||||
return !this.$route.params.preferredUsername;
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class GroupMembers extends Vue {
|
||||
group!: IGroup;
|
||||
|
||||
loading = true;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
newMemberUsername = "";
|
||||
|
||||
async inviteMember() {
|
||||
await this.$apollo.mutate<{ inviteMember: IMember }>({
|
||||
mutation: INVITE_MEMBER,
|
||||
variables: {
|
||||
groupId: this.group.id,
|
||||
targetActorUsername: this.newMemberUsername,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
102
js/src/views/Group/MyGroups.vue
Normal file
102
js/src/views/Group/MyGroups.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1 class="title">{{ $t("My groups") }}</h1>
|
||||
<router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t("Create group") }}</router-link>
|
||||
<b-loading :active.sync="$apollo.loading"></b-loading>
|
||||
<section v-if="invitations && invitations.length > 0">
|
||||
<InvitationCard
|
||||
v-for="member in invitations"
|
||||
:key="member.id"
|
||||
:member="member"
|
||||
@accept="acceptInvitation"
|
||||
/>
|
||||
</section>
|
||||
<section v-if="memberships && memberships.length > 0">
|
||||
<GroupCard v-for="member in memberships" :key="member.id" :member="member" />
|
||||
</section>
|
||||
<b-message v-if="$apollo.loading === false && memberships.length === 0" type="is-danger">
|
||||
{{ $t("No groups found") }}
|
||||
</b-message>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
|
||||
import GroupCard from "@/components/Group/GroupCard.vue";
|
||||
import InvitationCard from "@/components/Group/InvitationCard.vue";
|
||||
import { Paginate } from "@/types/paginate";
|
||||
import { IGroup, IMember, MemberRole } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import { ACCEPT_INVITATION } from "../../graphql/member";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
GroupCard,
|
||||
InvitationCard,
|
||||
},
|
||||
apollo: {
|
||||
paginatedGroups: {
|
||||
query: LOGGED_USER_MEMBERSHIPS,
|
||||
fetchPolicy: "network-only",
|
||||
variables: {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
beforeDateTime: new Date().toISOString(),
|
||||
},
|
||||
update: (data) => data.loggedUser.memberships,
|
||||
},
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t("My groups") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class MyEvents extends Vue {
|
||||
paginatedGroups!: Paginate<IMember>;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get invitations() {
|
||||
if (!this.paginatedGroups) return [];
|
||||
return this.paginatedGroups.elements.filter((member) => member.role === MemberRole.INVITED);
|
||||
}
|
||||
|
||||
get memberships() {
|
||||
if (!this.paginatedGroups) return [];
|
||||
return this.paginatedGroups.elements.filter((member) => member.role !== MemberRole.INVITED);
|
||||
}
|
||||
|
||||
async acceptInvitation(id: string) {
|
||||
await this.$apollo.mutate<{ acceptInvitation: IMember }>({
|
||||
mutation: ACCEPT_INVITATION,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.participation {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
section {
|
||||
.upcoming-month {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
96
js/src/views/Group/Settings.vue
Normal file
96
js/src/views/Group/Settings.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<aside class="section container">
|
||||
<h1 class="title">{{ $t("Settings") }}</h1>
|
||||
<div class="columns">
|
||||
<SettingsMenu class="column is-one-quarter-desktop" :menu="menu" />
|
||||
<div class="column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li
|
||||
v-for="route in routes.get($route.name)"
|
||||
:class="{ 'is-active': route.to.name === $route.name }"
|
||||
:key="route.title"
|
||||
>
|
||||
<router-link :to="{ name: route.to.name }">{{ route.title }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import SettingsMenu from "@/components/Settings/SettingsMenu.vue";
|
||||
import { ISettingMenuSection } from "@/types/setting-menu.model";
|
||||
import { Route } from "vue-router";
|
||||
import { IGroup, IPerson } from "@/types/actor";
|
||||
import { FETCH_GROUP } from "@/graphql/actor";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: { SettingsMenu },
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Settings extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
menu: ISettingMenuSection[] = [];
|
||||
|
||||
group!: IGroup[];
|
||||
|
||||
mounted() {
|
||||
this.menu = [
|
||||
{
|
||||
title: this.$t("Settings") as string,
|
||||
to: { name: RouteName.GROUP_SETTINGS } as Route,
|
||||
items: [
|
||||
{
|
||||
title: this.$t("Public") as string,
|
||||
to: { name: RouteName.GROUP_PUBLIC_SETTINGS } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t("Members") as string,
|
||||
to: { name: RouteName.GROUP_MEMBERS_SETTINGS } as Route,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get routes(): Map<string, Route[]> {
|
||||
return this.getPath(this.menu);
|
||||
}
|
||||
|
||||
getPath(object: ISettingMenuSection[]) {
|
||||
function iter(menu: ISettingMenuSection[] | ISettingMenuSection, acc: ISettingMenuSection[]) {
|
||||
if (Array.isArray(menu)) {
|
||||
return menu.forEach((item: ISettingMenuSection) => {
|
||||
iter(item, acc.concat(item));
|
||||
});
|
||||
}
|
||||
if (menu.items && menu.items.length > 0) {
|
||||
return menu.items.forEach((item: ISettingMenuSection) => {
|
||||
iter(item, acc.concat(item));
|
||||
});
|
||||
}
|
||||
result.set(menu.to.name, acc);
|
||||
}
|
||||
|
||||
const result = new Map();
|
||||
iter(object, []);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
aside.section {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,130 +1,142 @@
|
||||
<template>
|
||||
<div>
|
||||
<section class="hero is-light is-bold" v-if="config && (!currentUser.id || !currentActor.id)">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">{{ $t('Gather ⋅ Organize ⋅ Mobilize') }}</h1>
|
||||
<p>{{ $t('Join {instance}, a Mobilizon instance', { instance: config.name }) }}</p>
|
||||
<p class="instance-description">{{ config.description }}</p>
|
||||
<!-- We don't invite to find other instances yet -->
|
||||
<!-- <p v-if="!config.registrationsOpen">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h1 class="title">{{ $t("Gather ⋅ Organize ⋅ Mobilize") }}</h1>
|
||||
<p>{{ $t("Join {instance}, a Mobilizon instance", { instance: config.name }) }}</p>
|
||||
<p class="instance-description">{{ config.description }}</p>
|
||||
<!-- We don't invite to find other instances yet -->
|
||||
<!-- <p v-if="!config.registrationsOpen">
|
||||
{{ $t("This instance isn't opened to registrations, but you can register on other instances.") }}
|
||||
</p> -->
|
||||
<b-message type="is-danger" v-if="!config.registrationsOpen">
|
||||
{{ $t("Unfortunately, this instance isn't opened to registrations") }}
|
||||
</b-message>
|
||||
<div class="buttons">
|
||||
<b-button type="is-primary" tag="router-link" :to="{ name: RouteName.REGISTER }" v-if="config.registrationsOpen">
|
||||
{{ $t('Sign up') }}
|
||||
</b-button>
|
||||
<!-- We don't invite to find other instances yet -->
|
||||
<!-- <b-button v-else type="is-link" tag="a" href="https://joinmastodon.org">{{ $t('Find an instance') }}</b-button> -->
|
||||
<b-button type="is-text" tag="router-link" :to="{ name: RouteName.ABOUT }">{{ $t('Learn more about Mobilizon')}}</b-button>
|
||||
</div>
|
||||
</p>-->
|
||||
<b-message type="is-danger" v-if="!config.registrationsOpen">{{
|
||||
$t("Unfortunately, this instance isn't opened to registrations")
|
||||
}}</b-message>
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
type="is-primary"
|
||||
tag="router-link"
|
||||
:to="{ name: RouteName.REGISTER }"
|
||||
v-if="config.registrationsOpen"
|
||||
>{{ $t("Sign up") }}</b-button
|
||||
>
|
||||
<!-- We don't invite to find other instances yet -->
|
||||
<!-- <b-button v-else type="is-link" tag="a" href="https://joinmastodon.org">{{ $t('Find an instance') }}</b-button> -->
|
||||
<b-button type="is-text" tag="router-link" :to="{ name: RouteName.ABOUT }">
|
||||
{{ $t("Learn more about Mobilizon") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
<div class="container section" v-if="config">
|
||||
<section v-if="currentActor.id">
|
||||
<b-message type="is-info" v-if="welcomeBack">
|
||||
{{ $t('Welcome back {username}!', { username: currentActor.displayName() }) }}
|
||||
</b-message>
|
||||
<b-message type="is-info" v-if="newRegisteredUser">
|
||||
{{ $t('Welcome to Mobilizon, {username}!', { username: currentActor.displayName() }) }}
|
||||
</b-message>
|
||||
<b-message type="is-info" v-if="welcomeBack">{{
|
||||
$t("Welcome back {username}!", { username: currentActor.displayName() })
|
||||
}}</b-message>
|
||||
<b-message type="is-info" v-if="newRegisteredUser">{{
|
||||
$t("Welcome to Mobilizon, {username}!", { username: currentActor.displayName() })
|
||||
}}</b-message>
|
||||
</section>
|
||||
<section v-if="currentActor.id && goingToEvents.size > 0" class="container">
|
||||
<h3 class="title">
|
||||
{{ $t("Upcoming") }}
|
||||
</h3>
|
||||
<h3 class="title">{{ $t("Upcoming") }}</h3>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<div v-for="row of goingToEvents" class="upcoming-events" :key="row[0]">
|
||||
<span class="date-component-container" v-if="isInLessThanSevenDays(row[0])">
|
||||
<date-component :date="row[0]" />
|
||||
<subtitle
|
||||
v-if="isToday(row[0])">
|
||||
{{ $tc('You have one event today.', row[1].length, {count: row[1].length}) }}
|
||||
</subtitle>
|
||||
<subtitle
|
||||
v-else-if="isTomorrow(row[0])">
|
||||
{{ $tc('You have one event tomorrow.', row[1].length, {count: row[1].length}) }}
|
||||
</subtitle>
|
||||
<subtitle
|
||||
v-else-if="isInLessThanSevenDays(row[0])">
|
||||
{{ $tc('You have one event in {days} days.', row[1].length, {count: row[1].length, days: calculateDiffDays(row[0])}) }}
|
||||
<subtitle v-if="isToday(row[0])">{{
|
||||
$tc("You have one event today.", row[1].length, { count: row[1].length })
|
||||
}}</subtitle>
|
||||
<subtitle v-else-if="isTomorrow(row[0])">{{
|
||||
$tc("You have one event tomorrow.", row[1].length, { count: row[1].length })
|
||||
}}</subtitle>
|
||||
<subtitle v-else-if="isInLessThanSevenDays(row[0])">
|
||||
{{
|
||||
$tc("You have one event in {days} days.", row[1].length, {
|
||||
count: row[1].length,
|
||||
days: calculateDiffDays(row[0]),
|
||||
})
|
||||
}}
|
||||
</subtitle>
|
||||
</span>
|
||||
<div>
|
||||
<EventListCard
|
||||
v-for="participation in row[1]"
|
||||
v-if="isInLessThanSevenDays(row[0])"
|
||||
:key="participation[1].id"
|
||||
:participation="participation[1]"
|
||||
v-for="participation in row[1]"
|
||||
v-if="isInLessThanSevenDays(row[0])"
|
||||
:key="participation[1].id"
|
||||
:participation="participation[1]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<span class="view-all">
|
||||
<router-link :to=" { name: RouteName.MY_EVENTS }">{{ $t('View everything')}} >></router-link>
|
||||
<router-link :to="{ name: RouteName.MY_EVENTS }"
|
||||
>{{ $t("View everything") }} >></router-link
|
||||
>
|
||||
</span>
|
||||
</section>
|
||||
<section v-if="currentActor && lastWeekEvents.length > 0">
|
||||
<h3 class="title">
|
||||
{{ $t("Last week") }}
|
||||
</h3>
|
||||
<h3 class="title">{{ $t("Last week") }}</h3>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<div>
|
||||
<EventListCard
|
||||
v-for="participation in lastWeekEvents"
|
||||
:key="participation.id"
|
||||
:participation="participation"
|
||||
:options="{ hideDate: false }"
|
||||
/>
|
||||
<EventListCard
|
||||
v-for="participation in lastWeekEvents"
|
||||
:key="participation.id"
|
||||
:participation="participation"
|
||||
:options="{ hideDate: false }"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<section class="events-featured">
|
||||
<h3 class="title">{{ $t('Featured events') }}</h3>
|
||||
<h3 class="title">{{ $t("Featured events") }}</h3>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<div v-if="filteredFeaturedEvents.length > 0" class="columns is-multiline">
|
||||
<div class="column is-one-third-desktop" v-for="event in filteredFeaturedEvents.slice(0, 6)" :key="event.uuid">
|
||||
<EventCard
|
||||
:event="event"
|
||||
/>
|
||||
<div
|
||||
class="column is-one-third-desktop"
|
||||
v-for="event in filteredFeaturedEvents.slice(0, 6)"
|
||||
:key="event.uuid"
|
||||
>
|
||||
<EventCard :event="event" />
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-else type="is-danger">
|
||||
{{ $t('No events found') }}
|
||||
</b-message>
|
||||
<b-message v-else type="is-danger">{{ $t("No events found") }}</b-message>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import ngeohash from 'ngeohash';
|
||||
import { FETCH_EVENTS } from '@/graphql/event';
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import EventListCard from '@/components/Event/EventListCard.vue';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import { CURRENT_ACTOR_CLIENT, LOGGED_USER_PARTICIPATIONS } from '@/graphql/actor';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
import { CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { RouteName } from '@/router';
|
||||
import { EventModel, IEvent, IParticipant, Participant, ParticipantRole } from '@/types/event.model';
|
||||
import DateComponent from '@/components/Event/DateCalendarIcon.vue';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
import ngeohash from "ngeohash";
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { FETCH_EVENTS } from "../graphql/event";
|
||||
import EventListCard from "../components/Event/EventListCard.vue";
|
||||
import EventCard from "../components/Event/EventCard.vue";
|
||||
import { CURRENT_ACTOR_CLIENT, LOGGED_USER_PARTICIPATIONS } from "../graphql/actor";
|
||||
import { IPerson, Person } from "../types/actor";
|
||||
import { ICurrentUser } from "../types/current-user.model";
|
||||
import { CURRENT_USER_CLIENT } from "../graphql/user";
|
||||
import RouteName from "../router/name";
|
||||
import {
|
||||
EventModel,
|
||||
IEvent,
|
||||
IParticipant,
|
||||
Participant,
|
||||
ParticipantRole,
|
||||
} from "../types/event.model";
|
||||
import DateComponent from "../components/Event/DateCalendarIcon.vue";
|
||||
import { CONFIG } from "../graphql/config";
|
||||
import { IConfig } from "../types/config.model";
|
||||
import Subtitle from "../components/Utils/Subtitle.vue";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
events: {
|
||||
query: FETCH_EVENTS,
|
||||
fetchPolicy: 'no-cache', // Debug me: https://github.com/apollographql/apollo-client/issues/3030
|
||||
fetchPolicy: "no-cache", // Debug me: https://github.com/apollographql/apollo-client/issues/3030
|
||||
},
|
||||
currentActor: {
|
||||
query: CURRENT_ACTOR_CLIENT,
|
||||
update: data => new Person(data.currentActor),
|
||||
update: (data) => new Person(data.currentActor),
|
||||
},
|
||||
currentUser: {
|
||||
query: CURRENT_USER_CLIENT,
|
||||
@@ -141,9 +153,10 @@ import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
afterDateTime: lastWeek.toISOString(),
|
||||
};
|
||||
},
|
||||
update: (data) => {
|
||||
return data.loggedUser.participations.map(participation => new Participant(participation));
|
||||
},
|
||||
update: (data) =>
|
||||
data.loggedUser.participations.map(
|
||||
(participation: IParticipant) => new Participant(participation)
|
||||
),
|
||||
skip() {
|
||||
return this.currentUser.isLoggedIn === false;
|
||||
},
|
||||
@@ -161,18 +174,25 @@ import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
// @ts-ignore
|
||||
title: this.instanceName,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Home extends Vue {
|
||||
events: IEvent[] = [];
|
||||
|
||||
locations = [];
|
||||
|
||||
city = { name: null };
|
||||
|
||||
country = { name: null };
|
||||
|
||||
currentUser!: ICurrentUser;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
currentUserParticipations: IParticipant[] = [];
|
||||
@@ -189,39 +209,39 @@ export default class Home extends Vue {
|
||||
}
|
||||
|
||||
get welcomeBack() {
|
||||
return window.localStorage.getItem('welcome-back') === 'yes';
|
||||
return window.localStorage.getItem("welcome-back") === "yes";
|
||||
}
|
||||
|
||||
get newRegisteredUser() {
|
||||
return window.localStorage.getItem('new-registered-user') === 'yes';
|
||||
return window.localStorage.getItem("new-registered-user") === "yes";
|
||||
}
|
||||
|
||||
mounted() {
|
||||
if (window.localStorage.getItem('welcome-back')) {
|
||||
window.localStorage.removeItem('welcome-back');
|
||||
if (window.localStorage.getItem("welcome-back")) {
|
||||
window.localStorage.removeItem("welcome-back");
|
||||
}
|
||||
if (window.localStorage.getItem('new-registered-user')) {
|
||||
window.localStorage.removeItem('new-registered-user');
|
||||
if (window.localStorage.getItem("new-registered-user")) {
|
||||
window.localStorage.removeItem("new-registered-user");
|
||||
}
|
||||
}
|
||||
|
||||
isToday(date: Date) {
|
||||
return (new Date(date)).toDateString() === (new Date()).toDateString();
|
||||
return new Date(date).toDateString() === new Date().toDateString();
|
||||
}
|
||||
|
||||
isTomorrow(date: string) :boolean {
|
||||
isTomorrow(date: string): boolean {
|
||||
return this.isInDays(date, 1);
|
||||
}
|
||||
|
||||
isInDays(date: string, nbDays: number) :boolean {
|
||||
isInDays(date: string, nbDays: number): boolean {
|
||||
return this.calculateDiffDays(date) === nbDays;
|
||||
}
|
||||
|
||||
isBefore(date: string, nbDays: number) :boolean {
|
||||
isBefore(date: string, nbDays: number): boolean {
|
||||
return this.calculateDiffDays(date) < nbDays;
|
||||
}
|
||||
|
||||
isAfter(date: string, nbDays: number) :boolean {
|
||||
isAfter(date: string, nbDays: number): boolean {
|
||||
return this.calculateDiffDays(date) >= nbDays;
|
||||
}
|
||||
|
||||
@@ -230,36 +250,43 @@ export default class Home extends Vue {
|
||||
}
|
||||
|
||||
calculateDiffDays(date: string): number {
|
||||
return Math.ceil(((new Date(date)).getTime() - (new Date()).getTime()) / 1000 / 60 / 60 / 24);
|
||||
return Math.ceil((new Date(date).getTime() - new Date().getTime()) / 1000 / 60 / 60 / 24);
|
||||
}
|
||||
|
||||
get goingToEvents(): Map<string, Map<string, IParticipant>> {
|
||||
const res = this.currentUserParticipations.filter(({ event, role }) => {
|
||||
return event.beginsOn != null &&
|
||||
this.isAfter(event.beginsOn.toDateString(), 0) &&
|
||||
this.isBefore(event.beginsOn.toDateString(), 7) &&
|
||||
role !== ParticipantRole.REJECTED;
|
||||
});
|
||||
const res = this.currentUserParticipations.filter(
|
||||
({ event, role }) =>
|
||||
event.beginsOn != null &&
|
||||
this.isAfter(event.beginsOn.toDateString(), 0) &&
|
||||
this.isBefore(event.beginsOn.toDateString(), 7) &&
|
||||
role !== ParticipantRole.REJECTED
|
||||
);
|
||||
res.sort(
|
||||
(a: IParticipant, b: IParticipant) => a.event.beginsOn.getTime() - b.event.beginsOn.getTime(),
|
||||
(a: IParticipant, b: IParticipant) => a.event.beginsOn.getTime() - b.event.beginsOn.getTime()
|
||||
);
|
||||
|
||||
return res.reduce((acc: Map<string, Map<string, IParticipant>>, participation: IParticipant) => {
|
||||
const day = (new Date(participation.event.beginsOn)).toDateString();
|
||||
const participations: Map<string, IParticipant> = acc.get(day) || new Map();
|
||||
participations.set(`${participation.event.uuid}${participation.actor.id}`, participation);
|
||||
acc.set(day, participations);
|
||||
return acc;
|
||||
}, new Map());
|
||||
return res.reduce(
|
||||
(acc: Map<string, Map<string, IParticipant>>, participation: IParticipant) => {
|
||||
const day = new Date(participation.event.beginsOn).toDateString();
|
||||
const participations: Map<string, IParticipant> = acc.get(day) || new Map();
|
||||
participations.set(`${participation.event.uuid}${participation.actor.id}`, participation);
|
||||
acc.set(day, participations);
|
||||
return acc;
|
||||
},
|
||||
new Map()
|
||||
);
|
||||
}
|
||||
|
||||
get lastWeekEvents() {
|
||||
const res = this.currentUserParticipations.filter(({ event, role }) => {
|
||||
return event.beginsOn != null && this.isBefore(event.beginsOn.toDateString(), 0) && role !== ParticipantRole.REJECTED;
|
||||
});
|
||||
const res = this.currentUserParticipations.filter(
|
||||
({ event, role }) =>
|
||||
event.beginsOn != null &&
|
||||
this.isBefore(event.beginsOn.toDateString(), 0) &&
|
||||
role !== ParticipantRole.REJECTED
|
||||
);
|
||||
res.sort(
|
||||
(a: IParticipant, b: IParticipant) => a.event.beginsOn.getTime() - b.event.beginsOn.getTime(),
|
||||
);
|
||||
(a: IParticipant, b: IParticipant) => a.event.beginsOn.getTime() - b.event.beginsOn.getTime()
|
||||
);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -267,48 +294,56 @@ export default class Home extends Vue {
|
||||
* Return all events from server excluding the ones shown as participating
|
||||
*/
|
||||
get filteredFeaturedEvents() {
|
||||
return this.events.filter(({ id }) => !this.currentUserParticipations.map(({ event: { id } }) => id).includes(id));
|
||||
return this.events.filter(
|
||||
({ id }) => !this.currentUserParticipations.map(({ event: { id } }) => id).includes(id)
|
||||
);
|
||||
}
|
||||
|
||||
geoLocalize() {
|
||||
const router = this.$router;
|
||||
const sessionCity = sessionStorage.getItem('City');
|
||||
const sessionCity = sessionStorage.getItem("City");
|
||||
|
||||
if (sessionCity) {
|
||||
return router.push({ name: 'EventList', params: { location: sessionCity } });
|
||||
return router.push({
|
||||
name: "EventList",
|
||||
params: { location: sessionCity },
|
||||
});
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
pos => {
|
||||
(pos) => {
|
||||
const crd = pos.coords;
|
||||
|
||||
const geoHash = ngeohash.encode(crd.latitude, crd.longitude, 11);
|
||||
sessionStorage.setItem('City', geoHash);
|
||||
router.push({ name: RouteName.EVENT_LIST, params: { location: geoHash } });
|
||||
sessionStorage.setItem("City", geoHash);
|
||||
router.push({
|
||||
name: RouteName.EVENT_LIST,
|
||||
params: { location: geoHash },
|
||||
});
|
||||
},
|
||||
|
||||
err => console.warn(`ERROR(${err.code}): ${err.message}`),
|
||||
(err) => console.warn(`ERROR(${err.code}): ${err.message}`),
|
||||
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 5000,
|
||||
maximumAge: 0,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getAddressData(addressData) {
|
||||
const geoHash = ngeohash.encode(
|
||||
addressData.latitude,
|
||||
addressData.longitude,
|
||||
11,
|
||||
);
|
||||
sessionStorage.setItem('City', geoHash);
|
||||
// getAddressData(addressData) {
|
||||
// const geoHash = ngeohash.encode(
|
||||
// addressData.latitude,
|
||||
// addressData.longitude,
|
||||
// 11,
|
||||
// );
|
||||
// sessionStorage.setItem('City', geoHash);
|
||||
|
||||
this.$router.push({ name: RouteName.EVENT_LIST, params: { location: geoHash } });
|
||||
}
|
||||
// this.$router.push({ name: RouteName.EVENT_LIST, params: { location: geoHash } });
|
||||
// }
|
||||
|
||||
viewEvent(event) {
|
||||
viewEvent(event: IEvent) {
|
||||
this.$router.push({ name: RouteName.EVENT, params: { uuid: event.uuid } });
|
||||
}
|
||||
|
||||
@@ -320,15 +355,15 @@ export default class Home extends Vue {
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style lang="scss" scoped>
|
||||
@import "@/variables.scss";
|
||||
@import "@/variables.scss";
|
||||
|
||||
main > div > .container {
|
||||
background: $white;
|
||||
}
|
||||
main > div > .container {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
.section {
|
||||
padding: 1rem 1.5rem;
|
||||
}
|
||||
|
||||
.search-autocomplete {
|
||||
border: 1px solid #dbdbdb;
|
||||
@@ -351,34 +386,34 @@ export default class Home extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
section.container {
|
||||
margin: auto auto 3rem;
|
||||
}
|
||||
section.container {
|
||||
margin: auto auto 3rem;
|
||||
}
|
||||
|
||||
span.view-all {
|
||||
display: block;
|
||||
margin-top: 2rem;
|
||||
text-align: right;
|
||||
span.view-all {
|
||||
display: block;
|
||||
margin-top: 2rem;
|
||||
text-align: right;
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
section.hero {
|
||||
margin-top: -3px;
|
||||
background: lighten($secondary, 20%);
|
||||
|
||||
.title {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
section.hero {
|
||||
margin-top: -3px;
|
||||
background: lighten($secondary, 20%);
|
||||
|
||||
.title {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
.column figure.image img {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.instance-description {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.column figure.image img {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.instance-description {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
<template>
|
||||
<div class="container section">
|
||||
<b-notification v-if="$apollo.queries.searchEvents.loading">{{ $t('Redirecting to event…') }}</b-notification>
|
||||
<b-notification v-if="$apollo.queries.searchEvents.skip" type="is-danger">{{ $t('Resource provided is not an URL') }}</b-notification>
|
||||
</div>
|
||||
<div class="container section">
|
||||
<b-notification v-if="$apollo.queries.searchEvents.loading">
|
||||
{{ $t("Redirecting to event…") }}
|
||||
</b-notification>
|
||||
<b-notification v-if="$apollo.queries.searchEvents.skip" type="is-danger">
|
||||
{{ $t("Resource provided is not an URL") }}
|
||||
</b-notification>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Component,
|
||||
Vue,
|
||||
} from 'vue-property-decorator';
|
||||
import { RouteName } from '@/router';
|
||||
import { SEARCH_EVENTS } from '@/graphql/search';
|
||||
import { IEvent } from '@/types/event.model';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { SEARCH_EVENTS } from "@/graphql/search";
|
||||
import { IEvent } from "@/types/event.model";
|
||||
import RouteName from "../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -35,9 +36,16 @@ import { IEvent } from '@/types/event.model';
|
||||
}
|
||||
},
|
||||
async result({ data }) {
|
||||
if (data.searchEvents && data.searchEvents.total > 0 && data.searchEvents.elements.length > 0) {
|
||||
if (
|
||||
data.searchEvents &&
|
||||
data.searchEvents.total > 0 &&
|
||||
data.searchEvents.elements.length > 0
|
||||
) {
|
||||
const event = data.searchEvents.elements[0];
|
||||
return await this.$router.replace({ name: RouteName.EVENT, params: { uuid: event.uuid } });
|
||||
return await this.$router.replace({
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: event.uuid },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -45,13 +53,14 @@ import { IEvent } from '@/types/event.model';
|
||||
})
|
||||
export default class Interact extends Vue {
|
||||
searchEvents!: IEvent[];
|
||||
|
||||
RouteName = RouteName;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@/variables.scss";
|
||||
@import "@/variables.scss";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
</style>
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,17 +3,19 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
|
||||
@Component
|
||||
export default class Location extends Vue {
|
||||
@Prop(String) address!: string;
|
||||
|
||||
description = 'Paris, France';
|
||||
description = "Paris, France";
|
||||
|
||||
center = { lat: 48.85, lng: 2.35 };
|
||||
|
||||
markers: any[] = [];
|
||||
|
||||
setPlace(place) {
|
||||
setPlace(place: any) {
|
||||
this.center = {
|
||||
lat: place.geometry.location.lat(),
|
||||
lng: place.geometry.location.lng(),
|
||||
@@ -24,7 +26,7 @@ export default class Location extends Vue {
|
||||
},
|
||||
];
|
||||
|
||||
this.$emit('input', place.formatted_address);
|
||||
this.$emit("input", place.formatted_address);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,44 +1,57 @@
|
||||
<template>
|
||||
<section>
|
||||
<ul v-if="actionLogs.length > 0">
|
||||
<li v-for="log in actionLogs">
|
||||
<div class="box">
|
||||
<img class="image" :src="log.actor.avatar.url" v-if="log.actor.avatar" />
|
||||
<span>@{{ log.actor.preferredUsername }}</span>
|
||||
<span v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED">
|
||||
closed <router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.REPORT_UPDATE_OPENED">
|
||||
reopened <router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED">
|
||||
marked <router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }">report #{{ log.object.id }}</router-link> as resolved
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.NOTE_CREATION">
|
||||
added a note on
|
||||
<router-link v-if="log.object.report" :to="{ name: RouteName.REPORT, params: { reportId: log.object.report.id } }">report #{{ log.object.report.id }}</router-link>
|
||||
<span v-else>a non-existent report</span>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.EVENT_DELETION">
|
||||
deleted an event named « {{ log.object.title }} »
|
||||
</span>
|
||||
<br />
|
||||
<small>{{ log.insertedAt | formatDateTimeString }}</small>
|
||||
</div>
|
||||
<!-- <pre>{{ log }}</pre>-->
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
<b-message type="is-info">No moderation logs yet</b-message>
|
||||
<section>
|
||||
<ul v-if="actionLogs.length > 0">
|
||||
<li v-for="log in actionLogs">
|
||||
<div class="box">
|
||||
<img class="image" :src="log.actor.avatar.url" v-if="log.actor.avatar" />
|
||||
<span>@{{ log.actor.preferredUsername }}</span>
|
||||
<span v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED">
|
||||
closed
|
||||
<router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||
>report #{{ log.object.id }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.REPORT_UPDATE_OPENED">
|
||||
reopened
|
||||
<router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||
>report #{{ log.object.id }}</router-link
|
||||
>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.REPORT_UPDATE_RESOLVED">
|
||||
marked
|
||||
<router-link :to="{ name: RouteName.REPORT, params: { reportId: log.object.id } }"
|
||||
>report #{{ log.object.id }}</router-link
|
||||
>as resolved
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.NOTE_CREATION">
|
||||
added a note on
|
||||
<router-link
|
||||
v-if="log.object.report"
|
||||
:to="{ name: RouteName.REPORT, params: { reportId: log.object.report.id } }"
|
||||
>report #{{ log.object.report.id }}</router-link
|
||||
>
|
||||
<span v-else>a non-existent report</span>
|
||||
</span>
|
||||
<span v-else-if="log.action === ActionLogAction.EVENT_DELETION"
|
||||
>deleted an event named « {{ log.object.title }} »</span
|
||||
>
|
||||
<br />
|
||||
<small>{{ log.insertedAt | formatDateTimeString }}</small>
|
||||
</div>
|
||||
</section>
|
||||
<!-- <pre>{{ log }}</pre>-->
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
<b-message type="is-info">No moderation logs yet</b-message>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { IActionLog, ActionLogAction } from '@/types/report.model';
|
||||
import { LOGS } from '@/graphql/report';
|
||||
import ReportCard from '@/components/Report/ReportCard.vue';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import { IActionLog, ActionLogAction } from "@/types/report.model";
|
||||
import { LOGS } from "@/graphql/report";
|
||||
import ReportCard from "@/components/Report/ReportCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -51,17 +64,17 @@ import { RouteName } from '@/router';
|
||||
},
|
||||
})
|
||||
export default class ReportList extends Vue {
|
||||
|
||||
actionLogs?: IActionLog[] = [];
|
||||
|
||||
ActionLogAction = ActionLogAction;
|
||||
|
||||
RouteName = RouteName;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
||||
img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,158 +1,215 @@
|
||||
<template>
|
||||
<section>
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
|
||||
<div class="container" v-if="report">
|
||||
<div class="buttons">
|
||||
<b-button v-if="report.status !== ReportStatusEnum.RESOLVED" @click="updateReport(ReportStatusEnum.RESOLVED)" type="is-primary">{{ $t('Mark as resolved') }}</b-button>
|
||||
<b-button v-if="report.status !== ReportStatusEnum.OPEN" @click="updateReport(ReportStatusEnum.OPEN)" type="is-success">{{ $t('Reopen') }}</b-button>
|
||||
<b-button v-if="report.status !== ReportStatusEnum.CLOSED" @click="updateReport(ReportStatusEnum.CLOSED)" type="is-danger">{{ $t('Close') }}</b-button>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t('Reported identity') }}</td>
|
||||
<td>
|
||||
<router-link :to="{ name: RouteName.PROFILE, params: { name: report.reported.preferredUsername } }">
|
||||
<img v-if="report.reported.avatar" class="image" :src="report.reported.avatar.url" /> @{{ report.reported.preferredUsername }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('Reported by') }}</td>
|
||||
<td v-if="report.reporter.type === ActorType.APPLICATION">
|
||||
{{ report.reporter.domain }}
|
||||
</td>
|
||||
<td v-else>
|
||||
<router-link :to="{ name: RouteName.PROFILE, params: { name: report.reporter.preferredUsername } }">
|
||||
<img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('Reported')}}</td>
|
||||
<td>{{ report.insertedAt | formatDateTimeString }}</td>
|
||||
</tr>
|
||||
<tr v-if="report.updatedAt !== report.insertedAt">
|
||||
<td>{{ $t('Updated') }}</td>
|
||||
<td>{{ report.updatedAt | formatDateTimeString }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t('Status') }}</td>
|
||||
<td>
|
||||
<span v-if="report.status === ReportStatusEnum.OPEN">{{ $t('Open') }}</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.CLOSED">{{ $t('Closed') }}</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">{{ $t('Resolved') }}</span>
|
||||
<span v-else>{{ $t('Unknown') }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="report.event && report.comments.length > 0">
|
||||
<td>{{ $t('Event') }}</td>
|
||||
<td>
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid }}">{{ report.event.title }}</router-link>
|
||||
<span class="is-pulled-right">
|
||||
<!-- <b-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- type="is-primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="is-small">{{ $t('Delete') }}</b-button>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box report-content">
|
||||
<p v-if="report.content" v-html="nl2br(report.content)" />
|
||||
<p v-else>{{ $t('No comment') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="box" v-if="report.event && report.comments.length === 0">
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid }}">
|
||||
<h3 class="title">{{ report.event.title }}</h3>
|
||||
<p v-html="report.event.description" />
|
||||
<section>
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
|
||||
{{ error }}
|
||||
</b-message>
|
||||
<div class="container" v-if="report">
|
||||
<div class="buttons">
|
||||
<b-button
|
||||
v-if="report.status !== ReportStatusEnum.RESOLVED"
|
||||
@click="updateReport(ReportStatusEnum.RESOLVED)"
|
||||
type="is-primary"
|
||||
>{{ $t("Mark as resolved") }}</b-button
|
||||
>
|
||||
<b-button
|
||||
v-if="report.status !== ReportStatusEnum.OPEN"
|
||||
@click="updateReport(ReportStatusEnum.OPEN)"
|
||||
type="is-success"
|
||||
>{{ $t("Reopen") }}</b-button
|
||||
>
|
||||
<b-button
|
||||
v-if="report.status !== ReportStatusEnum.CLOSED"
|
||||
@click="updateReport(ReportStatusEnum.CLOSED)"
|
||||
type="is-danger"
|
||||
>{{ $t("Close") }}</b-button
|
||||
>
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="table is-striped is-fullwidth">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ $t("Reported identity") }}</td>
|
||||
<td>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILE,
|
||||
params: { name: report.reported.preferredUsername },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reported.avatar"
|
||||
class="image"
|
||||
:src="report.reported.avatar.url"
|
||||
/>
|
||||
@{{ report.reported.preferredUsername }}
|
||||
</router-link>
|
||||
<!-- <b-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- type="is-primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t("Reported by") }}</td>
|
||||
<td v-if="report.reporter.type === ActorType.APPLICATION">
|
||||
{{ report.reporter.domain }}
|
||||
</td>
|
||||
<td v-else>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.PROFILE,
|
||||
params: { name: report.reporter.preferredUsername },
|
||||
}"
|
||||
>
|
||||
<img
|
||||
v-if="report.reporter.avatar"
|
||||
class="image"
|
||||
:src="report.reporter.avatar.url"
|
||||
/>
|
||||
@{{ report.reporter.preferredUsername }}
|
||||
</router-link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t("Reported") }}</td>
|
||||
<td>{{ report.insertedAt | formatDateTimeString }}</td>
|
||||
</tr>
|
||||
<tr v-if="report.updatedAt !== report.insertedAt">
|
||||
<td>{{ $t("Updated") }}</td>
|
||||
<td>{{ report.updatedAt | formatDateTimeString }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ $t("Status") }}</td>
|
||||
<td>
|
||||
<span v-if="report.status === ReportStatusEnum.OPEN">{{ $t("Open") }}</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.CLOSED">
|
||||
{{ $t("Closed") }}
|
||||
</span>
|
||||
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">
|
||||
{{ $t("Resolved") }}
|
||||
</span>
|
||||
<span v-else>{{ $t("Unknown") }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="report.event && report.comments.length > 0">
|
||||
<td>{{ $t("Event") }}</td>
|
||||
<td>
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid } }">
|
||||
{{ report.event.title }}
|
||||
</router-link>
|
||||
<span class="is-pulled-right">
|
||||
<!-- <b-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- type="is-primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="is-small"
|
||||
>{{ $t("Delete") }}</b-button
|
||||
>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="box report-content">
|
||||
<p v-if="report.content" v-html="nl2br(report.content)" />
|
||||
<p v-else>{{ $t("No comment") }}</p>
|
||||
</div>
|
||||
|
||||
<div class="box" v-if="report.event && report.comments.length === 0">
|
||||
<router-link :to="{ name: RouteName.EVENT, params: { uuid: report.event.uuid } }">
|
||||
<h3 class="title">{{ report.event.title }}</h3>
|
||||
<p v-html="report.event.description" />
|
||||
</router-link>
|
||||
<!-- <b-button-->
|
||||
<!-- tag="router-link"-->
|
||||
<!-- type="is-primary"-->
|
||||
<!-- :to="{ name: RouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"-->
|
||||
<!-- icon-left="pencil"-->
|
||||
<!-- size="is-small">{{ $t('Edit') }}</b-button>-->
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="is-small"
|
||||
>{{ $t("Delete") }}</b-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<ul v-for="comment in report.comments" v-if="report.comments.length > 0">
|
||||
<li>
|
||||
<div class="box" v-if="comment">
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
|
||||
<img :src="comment.actor.avatar.url" alt="Image" />
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="comment.actor">
|
||||
<strong>{{ comment.actor.name }}</strong>
|
||||
<small>@{{ comment.actor.preferredUsername }}</small>
|
||||
</span>
|
||||
<span v-else>{{ $t("Unknown actor") }}</span>
|
||||
<br />
|
||||
<p v-html="comment.text" />
|
||||
</div>
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmEventDelete()"
|
||||
icon-left="delete"
|
||||
size="is-small">{{ $t('Delete') }}</b-button>
|
||||
</div>
|
||||
type="is-danger"
|
||||
@click="confirmCommentDelete(comment)"
|
||||
icon-left="delete"
|
||||
size="is-small"
|
||||
>{{ $t("Delete") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul v-for="comment in report.comments" v-if="report.comments.length > 0">
|
||||
<li>
|
||||
<div class="box" v-if="comment">
|
||||
<article class="media">
|
||||
<div class="media-left">
|
||||
<figure class="image is-48x48" v-if="comment.actor && comment.actor.avatar">
|
||||
<img :src="comment.actor.avatar.url" alt="Image">
|
||||
</figure>
|
||||
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
|
||||
</div>
|
||||
<div class="media-content">
|
||||
<div class="content">
|
||||
<span v-if="comment.actor">
|
||||
<strong>{{ comment.actor.name }}</strong> <small>@{{ comment.actor.preferredUsername }}</small>
|
||||
</span>
|
||||
<span v-else>{{ $t('Unknown actor') }}</span>
|
||||
<br>
|
||||
<p v-html="comment.text" />
|
||||
</div>
|
||||
<b-button
|
||||
type="is-danger"
|
||||
@click="confirmCommentDelete(comment)"
|
||||
icon-left="delete"
|
||||
size="is-small">{{ $t('Delete') }}</b-button>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<h2 class="title" v-if="report.notes.length > 0">{{ $t("Notes") }}</h2>
|
||||
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
|
||||
<p>{{ note.content }}</p>
|
||||
<router-link
|
||||
:to="{ name: RouteName.PROFILE, params: { name: note.moderator.preferredUsername } }"
|
||||
>
|
||||
<img alt class="image" :src="note.moderator.avatar.url" v-if="note.moderator.avatar" />
|
||||
@{{ note.moderator.preferredUsername }}
|
||||
</router-link>
|
||||
<br />
|
||||
<small>
|
||||
<a :href="`#note-${note.id}`" v-if="note.insertedAt">
|
||||
{{ note.insertedAt | formatDateTimeString }}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h2 class="title" v-if="report.notes.length > 0">{{ $t('Notes') }}</h2>
|
||||
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
|
||||
<p>{{ note.content }}</p>
|
||||
<router-link :to="{ name: RouteName.PROFILE, params: { name: note.moderator.preferredUsername } }">
|
||||
<img alt="" class="image" :src="note.moderator.avatar.url" v-if="note.moderator.avatar" />
|
||||
@{{ note.moderator.preferredUsername }}
|
||||
</router-link><br />
|
||||
<small><a :href="`#note-${note.id}`" v-if="note.insertedAt">{{ note.insertedAt | formatDateTimeString }}</a></small>
|
||||
</div>
|
||||
|
||||
<form @submit="addNote()">
|
||||
<b-field :label="$t('New note')">
|
||||
<b-input type="textarea" v-model="noteContent"></b-input>
|
||||
</b-field>
|
||||
<b-button type="submit" @click="addNote">{{ $t('Add a note') }}</b-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<form @submit="addNote()">
|
||||
<b-field :label="$t('New note')">
|
||||
<b-input type="textarea" v-model="noteContent"></b-input>
|
||||
</b-field>
|
||||
<b-button type="submit" @click="addNote">{{ $t("Add a note") }}</b-button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from '@/graphql/report';
|
||||
import { IReport, IReportNote, ReportStatusEnum } from '@/types/report.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
|
||||
import { IPerson, ActorType } from '@/types/actor';
|
||||
import { DELETE_EVENT } from '@/graphql/event';
|
||||
import { uniq } from 'lodash';
|
||||
import { nl2br } from '@/utils/html';
|
||||
import { DELETE_COMMENT } from '@/graphql/comment';
|
||||
import { IComment } from '@/types/comment.model';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { CREATE_REPORT_NOTE, REPORT, UPDATE_REPORT } from "@/graphql/report";
|
||||
import { IReport, IReportNote, ReportStatusEnum } from "@/types/report.model";
|
||||
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||
import { IPerson, ActorType } from "@/types/actor";
|
||||
import { DELETE_EVENT } from "@/graphql/event";
|
||||
import { uniq } from "lodash";
|
||||
import { nl2br } from "@/utils/html";
|
||||
import { DELETE_COMMENT } from "@/graphql/comment";
|
||||
import { IComment } from "@/types/comment.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -173,23 +230,29 @@ import { IComment } from '@/types/comment.model';
|
||||
},
|
||||
metaInfo() {
|
||||
return {
|
||||
title: this.$t('Report') as string,
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
title: this.$t("Report") as string,
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Report extends Vue {
|
||||
@Prop({ required: true }) reportId!: number;
|
||||
|
||||
report!: IReport;
|
||||
|
||||
currentActor!: IPerson;
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
ReportStatusEnum = ReportStatusEnum;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
ActorType = ActorType;
|
||||
|
||||
nl2br = nl2br;
|
||||
|
||||
noteContent: string = '';
|
||||
noteContent = "";
|
||||
|
||||
addNote() {
|
||||
try {
|
||||
@@ -202,11 +265,14 @@ export default class Report extends Vue {
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
const cachedData = store.readQuery<{ report: IReport }>({ query: REPORT, variables: { id: this.report.id } });
|
||||
const cachedData = store.readQuery<{ report: IReport }>({
|
||||
query: REPORT,
|
||||
variables: { id: this.report.id },
|
||||
});
|
||||
if (cachedData == null) return;
|
||||
const { report } = cachedData;
|
||||
if (report === null) {
|
||||
console.error('Cannot update event notes cache, because of null value.');
|
||||
console.error("Cannot update event notes cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const note = data.createReportNote;
|
||||
@@ -214,11 +280,15 @@ export default class Report extends Vue {
|
||||
|
||||
report.notes = report.notes.concat([note]);
|
||||
|
||||
store.writeQuery({ query: REPORT, variables: { id: this.report.id }, data: { report } });
|
||||
store.writeQuery({
|
||||
query: REPORT,
|
||||
variables: { id: this.report.id },
|
||||
data: { report },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
this.noteContent = '';
|
||||
this.noteContent = "";
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -226,10 +296,12 @@ export default class Report extends Vue {
|
||||
|
||||
confirmEventDelete() {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t('Deleting event') as string,
|
||||
message: this.$t('Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead.') as string,
|
||||
confirmText: this.$t('Delete Event') as string,
|
||||
type: 'is-danger',
|
||||
title: this.$t("Deleting event") as string,
|
||||
message: this.$t(
|
||||
"Are you sure you want to <b>delete</b> this event? This action cannot be undone. You may want to engage the conversation with the event creator or edit its event instead."
|
||||
) as string,
|
||||
confirmText: this.$t("Delete Event") as string,
|
||||
type: "is-danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () => this.deleteEvent(),
|
||||
});
|
||||
@@ -237,10 +309,12 @@ export default class Report extends Vue {
|
||||
|
||||
confirmCommentDelete(comment: IComment) {
|
||||
this.$buefy.dialog.confirm({
|
||||
title: this.$t('Deleting comment') as string,
|
||||
message: this.$t('Are you sure you want to <b>delete</b> this comment? This action cannot be undone.') as string,
|
||||
confirmText: this.$t('Delete Comment') as string,
|
||||
type: 'is-danger',
|
||||
title: this.$t("Deleting comment") as string,
|
||||
message: this.$t(
|
||||
"Are you sure you want to <b>delete</b> this comment? This action cannot be undone."
|
||||
) as string,
|
||||
confirmText: this.$t("Delete Comment") as string,
|
||||
type: "is-danger",
|
||||
hasIcon: true,
|
||||
onConfirm: () => this.deleteComment(comment),
|
||||
});
|
||||
@@ -260,9 +334,11 @@ export default class Report extends Vue {
|
||||
});
|
||||
|
||||
this.$buefy.notification.open({
|
||||
message: this.$t('Event {eventTitle} deleted', { eventTitle }) as string,
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
message: this.$t("Event {eventTitle} deleted", {
|
||||
eventTitle,
|
||||
}) as string,
|
||||
type: "is-success",
|
||||
position: "is-bottom-right",
|
||||
duration: 5000,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -279,7 +355,7 @@ export default class Report extends Vue {
|
||||
actorId: this.currentActor.id,
|
||||
},
|
||||
});
|
||||
this.$notifier.success(this.$t('Comment deleted') as string);
|
||||
this.$notifier.success(this.$t("Comment deleted") as string);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -296,17 +372,24 @@ export default class Report extends Vue {
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
const reportCachedData = store.readQuery<{ report: IReport }>({ query: REPORT, variables: { id: this.report.id } });
|
||||
const reportCachedData = store.readQuery<{ report: IReport }>({
|
||||
query: REPORT,
|
||||
variables: { id: this.report.id },
|
||||
});
|
||||
if (reportCachedData == null) return;
|
||||
const { report } = reportCachedData;
|
||||
if (report === null) {
|
||||
console.error('Cannot update event notes cache, because of null value.');
|
||||
console.error("Cannot update event notes cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const updatedReport = data.updateReportStatus;
|
||||
report.status = updatedReport.status;
|
||||
|
||||
store.writeQuery({ query: REPORT, variables: { id: this.report.id }, data: { report } });
|
||||
store.writeQuery({
|
||||
query: REPORT,
|
||||
variables: { id: this.report.id },
|
||||
data: { report },
|
||||
});
|
||||
},
|
||||
});
|
||||
await this.$router.push({ name: RouteName.REPORTS });
|
||||
@@ -315,31 +398,43 @@ export default class Report extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO make me a global function
|
||||
formatDate(value) {
|
||||
return value ? new Date(value).toLocaleString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) : null;
|
||||
// TODO make me a global function
|
||||
formatDate(value: string) {
|
||||
return value
|
||||
? new Date(value).toLocaleString(undefined, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
formatTime(value) {
|
||||
return value ? new Date(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }) : null;
|
||||
formatTime(value: string) {
|
||||
return value
|
||||
? new Date(value).toLocaleTimeString(undefined, {
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})
|
||||
: null;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/variables.scss";
|
||||
@import "@/variables.scss";
|
||||
|
||||
tbody td img.image, .note img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
tbody td img.image,
|
||||
.note img.image {
|
||||
display: inline;
|
||||
height: 1.5em;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
|
||||
.dialog .modal-card-foot {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.dialog .modal-card-foot {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.report-content {
|
||||
border-left: 4px solid $primary;
|
||||
}
|
||||
</style>
|
||||
.report-content {
|
||||
border-left: 4px solid $primary;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,39 +1,42 @@
|
||||
<template>
|
||||
<section>
|
||||
<b-field>
|
||||
<b-radio-button v-model="filterReports"
|
||||
:native-value="ReportStatusEnum.OPEN">
|
||||
{{ $t('Open') }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="filterReports"
|
||||
:native-value="ReportStatusEnum.RESOLVED">
|
||||
{{ $t('Resolved') }}
|
||||
</b-radio-button>
|
||||
<b-radio-button v-model="filterReports"
|
||||
:native-value="ReportStatusEnum.CLOSED">
|
||||
{{ $t('Closed') }}
|
||||
</b-radio-button>
|
||||
</b-field>
|
||||
<ul v-if="reports.length > 0">
|
||||
<li v-for="report in reports">
|
||||
<router-link :to="{ name: RouteName.REPORT, params: { reportId: report.id } }">
|
||||
<report-card :report="report" />
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">{{ $t('No open reports yet') }}</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">{{ $t('No resolved reports yet') }}</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">{{ $t('No closed reports yet') }}</b-message>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<b-field>
|
||||
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.OPEN">{{
|
||||
$t("Open")
|
||||
}}</b-radio-button>
|
||||
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.RESOLVED">{{
|
||||
$t("Resolved")
|
||||
}}</b-radio-button>
|
||||
<b-radio-button v-model="filterReports" :native-value="ReportStatusEnum.CLOSED">{{
|
||||
$t("Closed")
|
||||
}}</b-radio-button>
|
||||
</b-field>
|
||||
<ul v-if="reports.length > 0">
|
||||
<li v-for="report in reports">
|
||||
<router-link :to="{ name: RouteName.REPORT, params: { reportId: report.id } }">
|
||||
<report-card :report="report" />
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.OPEN" type="is-info">
|
||||
{{ $t("No open reports yet") }}
|
||||
</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">
|
||||
{{ $t("No resolved reports yet") }}
|
||||
</b-message>
|
||||
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">
|
||||
{{ $t("No closed reports yet") }}
|
||||
</b-message>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { IReport, ReportStatusEnum } from '@/types/report.model';
|
||||
import { REPORTS } from '@/graphql/report';
|
||||
import ReportCard from '@/components/Report/ReportCard.vue';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { IReport, ReportStatusEnum } from "@/types/report.model";
|
||||
import { REPORTS } from "@/graphql/report";
|
||||
import ReportCard from "@/components/Report/ReportCard.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -42,7 +45,7 @@ import { RouteName } from '@/router';
|
||||
apollo: {
|
||||
reports: {
|
||||
query: REPORTS,
|
||||
fetchPolicy: 'no-cache',
|
||||
fetchPolicy: "no-cache",
|
||||
variables() {
|
||||
return {
|
||||
status: this.filterReports,
|
||||
@@ -53,14 +56,16 @@ import { RouteName } from '@/router';
|
||||
},
|
||||
})
|
||||
export default class ReportList extends Vue {
|
||||
|
||||
reports?: IReport[] = [];
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
ReportStatusEnum = ReportStatusEnum;
|
||||
|
||||
filterReports: ReportStatusEnum = ReportStatusEnum.OPEN;
|
||||
|
||||
@Watch('$route.params.filter', { immediate: true })
|
||||
onRouteFilterChanged (val: string) {
|
||||
@Watch("$route.params.filter", { immediate: true })
|
||||
onRouteFilterChanged(val: string) {
|
||||
if (!val) return;
|
||||
const filter = val.toUpperCase();
|
||||
if (filter in ReportStatusEnum) {
|
||||
@@ -68,9 +73,12 @@ export default class ReportList extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('filterReports', { immediate: true })
|
||||
async onFilterChanged (val: string) {
|
||||
await this.$router.push({ name: RouteName.REPORTS, params: { filter: val.toLowerCase() } });
|
||||
@Watch("filterReports", { immediate: true })
|
||||
async onFilterChanged(val: string) {
|
||||
await this.$router.push({
|
||||
name: RouteName.REPORTS,
|
||||
params: { filter: val.toLowerCase() },
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
<section class="section container has-text-centered not-found">
|
||||
<div class="columns is-vertical">
|
||||
<div class="column is-centered">
|
||||
<img src="../assets/oh_no.jpg" alt="Not found 'oh no' picture">
|
||||
<h1 class="title">
|
||||
{{ $t("The page you're looking for doesn't exist.") }}
|
||||
</h1>
|
||||
<img src="../assets/oh_no.jpg" alt="Not found 'oh no' picture" />
|
||||
<h1 class="title">{{ $t("The page you're looking for doesn't exist.") }}</h1>
|
||||
<p>
|
||||
{{ $t("Please make sure the address is correct and that the page hasn't been moved.") }}
|
||||
</p>
|
||||
@@ -15,9 +13,15 @@
|
||||
<!-- The following should just be replaced with the SearchField component but it fails for some reason -->
|
||||
<form @submit="enter">
|
||||
<b-field class="search">
|
||||
<b-input expanded icon="magnify" type="search" :placeholder="searchPlaceHolder" v-model="searchText" />
|
||||
<b-input
|
||||
expanded
|
||||
icon="magnify"
|
||||
type="search"
|
||||
:placeholder="searchPlaceHolder"
|
||||
v-model="searchText"
|
||||
/>
|
||||
<p class="control">
|
||||
<button type="submit" class="button is-primary">{{ $t('Search') }}</button>
|
||||
<button type="submit" class="button is-primary">{{ $t("Search") }}</button>
|
||||
</p>
|
||||
</b-field>
|
||||
</form>
|
||||
@@ -26,9 +30,9 @@
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { RouteName } from '@/router';
|
||||
import BField from 'buefy/src/components/field/Field.vue';
|
||||
import { Component, Vue } from "vue-property-decorator";
|
||||
import BField from "buefy/src/components/field/Field.vue";
|
||||
import RouteName from "../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
@@ -36,28 +40,31 @@ import BField from 'buefy/src/components/field/Field.vue';
|
||||
},
|
||||
})
|
||||
export default class PageNotFound extends Vue {
|
||||
searchText: string = '';
|
||||
searchText = "";
|
||||
|
||||
get searchPlaceHolder(): string {
|
||||
return this.$t('Search events, groups, etc.') as string;
|
||||
return this.$t("Search events, groups, etc.") as string;
|
||||
}
|
||||
|
||||
enter() {
|
||||
this.$router.push({ name: RouteName.SEARCH, params: { searchTerm: this.searchText } });
|
||||
this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
params: { searchTerm: this.searchText },
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.container.not-found {
|
||||
margin: auto;
|
||||
max-width: 600px;
|
||||
.container.not-found {
|
||||
margin: auto;
|
||||
max-width: 600px;
|
||||
|
||||
img {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
img {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
526
js/src/views/Resources/ResourceFolder.vue
Normal file
526
js/src/views/Resources/ResourceFolder.vue
Normal file
@@ -0,0 +1,526 @@
|
||||
<template>
|
||||
<div class="container section" v-if="resource">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: usernameWithDomain(resource.actor) },
|
||||
}"
|
||||
>{{ resource.actor.preferredUsername }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.RESOURCE_FOLDER_ROOT,
|
||||
params: { preferredUsername: usernameWithDomain(resource.actor) },
|
||||
}"
|
||||
>{{ $t("Resources") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li
|
||||
v-if="resource.path !== '/'"
|
||||
:class="{ 'is-active': index + 1 === ResourceMixin.resourcePathArray(resource).length }"
|
||||
v-for="(pathFragment, index) in ResourceMixin.resourcePathArray(resource)"
|
||||
:key="pathFragment"
|
||||
>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.RESOURCE_FOLDER,
|
||||
params: {
|
||||
path: ResourceMixin.resourcePathArray(resource).slice(0, index + 1),
|
||||
preferredUsername: resource.actor.preferredUsername,
|
||||
},
|
||||
}"
|
||||
>{{ pathFragment }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<b-dropdown aria-role="list">
|
||||
<b-button class="button is-primary" slot="trigger">+</b-button>
|
||||
|
||||
<b-dropdown-item aria-role="listitem" @click="createFolderModal">
|
||||
<b-icon icon="folder" />
|
||||
{{ $t("New folder") }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item aria-role="listitem" @click="createLinkResourceModal = true">
|
||||
<b-icon icon="link" />
|
||||
{{ $t("New link") }}
|
||||
</b-dropdown-item>
|
||||
<hr class="dropdown-divider" v-if="config.resourceProviders.length" />
|
||||
<b-dropdown-item
|
||||
aria-role="listitem"
|
||||
v-for="resourceProvider in config.resourceProviders"
|
||||
:key="resourceProvider.software"
|
||||
@click="createResourceFromProvider(resourceProvider)"
|
||||
>
|
||||
<b-icon :icon="mapServiceTypeToIcon[resourceProvider.software]" />
|
||||
{{ createSentenceForType(resourceProvider.software) }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<div class="list-header">
|
||||
<div class="list-header-right">
|
||||
<b-checkbox v-model="checkedAll" />
|
||||
<div class="actions" v-if="validCheckedResources.length > 0">
|
||||
<small>
|
||||
{{
|
||||
$tc("No resources selected", validCheckedResources.length, {
|
||||
count: validCheckedResources.length,
|
||||
})
|
||||
}}
|
||||
</small>
|
||||
<b-button
|
||||
type="is-danger"
|
||||
icon-right="delete"
|
||||
size="is-small"
|
||||
@click="deleteMultipleResources"
|
||||
>{{ $t("Delete") }}</b-button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<draggable v-model="resource.children.elements" :sort="false" :group="groupObject">
|
||||
<transition-group>
|
||||
<div v-for="localResource in resource.children.elements" :key="localResource.id">
|
||||
<div class="resource-item">
|
||||
<div
|
||||
class="resource-checkbox"
|
||||
:class="{ checked: checkedResources[localResource.id] }"
|
||||
>
|
||||
<b-checkbox v-model="checkedResources[localResource.id]" />
|
||||
</div>
|
||||
<resource-item
|
||||
:resource="localResource"
|
||||
v-if="localResource.type !== 'folder'"
|
||||
@delete="deleteResource"
|
||||
@rename="handleRename"
|
||||
/>
|
||||
<folder-item
|
||||
:resource="localResource"
|
||||
:group="resource.actor"
|
||||
@delete="deleteResource"
|
||||
@rename="handleRename"
|
||||
v-else
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</transition-group>
|
||||
</draggable>
|
||||
</section>
|
||||
<b-modal :active.sync="renameModal" has-modal-card>
|
||||
<div class="modal-card">
|
||||
<section class="modal-card-body">
|
||||
<form @submit.prevent="renameResource">
|
||||
<b-field :label="$t('Title')">
|
||||
<b-input aria-required="true" v-model="updatedResource.title" />
|
||||
</b-field>
|
||||
|
||||
<b-button native-type="submit">{{ $t("Rename resource") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal :active.sync="createResourceModal" has-modal-card>
|
||||
<div class="modal-card">
|
||||
<section class="modal-card-body">
|
||||
<form @submit.prevent="createResource">
|
||||
<b-field :label="$t('Title')">
|
||||
<b-input aria-required="true" v-model="newResource.title" />
|
||||
</b-field>
|
||||
|
||||
<b-button native-type="submit">{{ createResourceButtonLabel }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</b-modal>
|
||||
<b-modal :active.sync="createLinkResourceModal" has-modal-card>
|
||||
<div class="modal-card">
|
||||
<section class="modal-card-body">
|
||||
<form @submit.prevent="createResource">
|
||||
<b-field :label="$t('URL')">
|
||||
<b-input
|
||||
type="url"
|
||||
required
|
||||
v-model="newResource.resourceUrl"
|
||||
@blur="previewResource"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<div class="new-resource-preview" v-if="newResource.title">
|
||||
<resource-item :resource="newResource" />
|
||||
</div>
|
||||
|
||||
<b-field :label="$t('Title')">
|
||||
<b-input aria-required="true" v-model="newResource.title" />
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Text')">
|
||||
<b-input type="textarea" v-model="newResource.summary" />
|
||||
</b-field>
|
||||
|
||||
<b-button native-type="submit">{{ $t("Create resource") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Mixins, Prop, Watch } from "vue-property-decorator";
|
||||
import ResourceItem from "@/components/Resource/ResourceItem.vue";
|
||||
import FolderItem from "@/components/Resource/FolderItem.vue";
|
||||
import Draggable from "vuedraggable";
|
||||
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
|
||||
import { IActor, usernameWithDomain } from "../../types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
import { IResource, mapServiceTypeToIcon, IProvider } from "../../types/resource";
|
||||
import {
|
||||
CREATE_RESOURCE,
|
||||
DELETE_RESOURCE,
|
||||
PREVIEW_RESOURCE_LINK,
|
||||
GET_RESOURCE,
|
||||
UPDATE_RESOURCE,
|
||||
} from "../../graphql/resources";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import ResourceMixin from "../../mixins/resource";
|
||||
|
||||
@Component({
|
||||
components: { FolderItem, ResourceItem, Draggable },
|
||||
apollo: {
|
||||
resource: {
|
||||
query: GET_RESOURCE,
|
||||
variables() {
|
||||
let path = Array.isArray(this.$route.params.path)
|
||||
? this.$route.params.path.join("/")
|
||||
: this.$route.params.path || this.path;
|
||||
path = path[0] !== "/" ? `/${path}` : path;
|
||||
return {
|
||||
path,
|
||||
username: this.$route.params.preferredUsername,
|
||||
};
|
||||
},
|
||||
},
|
||||
config: CONFIG,
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
})
|
||||
export default class Resources extends Mixins(ResourceMixin) {
|
||||
@Prop({ required: true }) path!: string;
|
||||
|
||||
resource!: IResource;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
currentActor!: IActor;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
ResourceMixin = ResourceMixin;
|
||||
|
||||
usernameWithDomain = usernameWithDomain;
|
||||
|
||||
newResource: IResource = {
|
||||
title: "",
|
||||
summary: "",
|
||||
resourceUrl: "",
|
||||
children: { elements: [], total: 0 },
|
||||
metadata: {},
|
||||
type: "link",
|
||||
};
|
||||
|
||||
updatedResource: IResource = {
|
||||
title: "",
|
||||
resourceUrl: "",
|
||||
metadata: {},
|
||||
children: { elements: [], total: 0 },
|
||||
path: undefined,
|
||||
};
|
||||
|
||||
checkedResources: { [key: string]: boolean } = {};
|
||||
|
||||
validCheckedResources: string[] = [];
|
||||
|
||||
checkedAll = false;
|
||||
|
||||
createResourceModal = false;
|
||||
|
||||
createLinkResourceModal = false;
|
||||
|
||||
renameModal = false;
|
||||
|
||||
groupObject: object = {
|
||||
name: "resources",
|
||||
pull: "clone",
|
||||
put: true,
|
||||
};
|
||||
|
||||
mapServiceTypeToIcon = mapServiceTypeToIcon;
|
||||
|
||||
async createResource() {
|
||||
if (!this.resource.actor) return;
|
||||
try {
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: CREATE_RESOURCE,
|
||||
variables: {
|
||||
title: this.newResource.title,
|
||||
summary: this.newResource.summary,
|
||||
actorId: this.resource.actor.id,
|
||||
resourceUrl: this.newResource.resourceUrl,
|
||||
parentId:
|
||||
this.resource.id && this.resource.id.startsWith("root_") ? null : this.resource.id,
|
||||
type: this.newResource.type,
|
||||
},
|
||||
update: (store, { data: { createResource } }) => {
|
||||
if (createResource == null) return;
|
||||
if (!this.resource.actor) return;
|
||||
const cachedData = store.readQuery<{ resource: IResource }>({
|
||||
query: GET_RESOURCE,
|
||||
variables: {
|
||||
path: this.resource.path,
|
||||
username: this.resource.actor.preferredUsername,
|
||||
},
|
||||
});
|
||||
if (cachedData == null) return;
|
||||
const { resource } = cachedData;
|
||||
if (resource == null) {
|
||||
console.error("Cannot update resource cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const newResource: IResource = createResource;
|
||||
resource.children.elements = resource.children.elements.concat([newResource]);
|
||||
|
||||
store.writeQuery({
|
||||
query: GET_RESOURCE,
|
||||
variables: {
|
||||
path: this.resource.path,
|
||||
username: this.resource.actor.preferredUsername,
|
||||
},
|
||||
data: { resource },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.createLinkResourceModal = false;
|
||||
this.createResourceModal = false;
|
||||
this.newResource.title = "";
|
||||
this.newResource.summary = "";
|
||||
this.newResource.resourceUrl = "";
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async previewResource() {
|
||||
if (this.newResource.resourceUrl === "") return;
|
||||
const { data } = await this.$apollo.mutate({
|
||||
mutation: PREVIEW_RESOURCE_LINK,
|
||||
variables: {
|
||||
resourceUrl: this.newResource.resourceUrl,
|
||||
},
|
||||
});
|
||||
this.newResource.title = data.previewResourceLink.title;
|
||||
this.newResource.summary = data.previewResourceLink.description;
|
||||
this.newResource.metadata = data.previewResourceLink;
|
||||
this.newResource.type = "link";
|
||||
}
|
||||
|
||||
createSentenceForType(type: string) {
|
||||
switch (type) {
|
||||
case "pad":
|
||||
return this.$t("Create a pad");
|
||||
case "calc":
|
||||
return this.$t("Create a calc");
|
||||
case "visio":
|
||||
return this.$t("Create a visioconference");
|
||||
}
|
||||
}
|
||||
|
||||
createFolderModal() {
|
||||
this.newResource.type = "folder";
|
||||
this.createResourceModal = true;
|
||||
}
|
||||
|
||||
createResourceFromProvider(provider: IProvider) {
|
||||
console.log(provider);
|
||||
this.newResource.resourceUrl = this.generateFullResourceUrl(provider);
|
||||
this.newResource.type = provider.software;
|
||||
this.createResourceModal = true;
|
||||
}
|
||||
|
||||
generateFullResourceUrl(provider: IProvider): string {
|
||||
const randomString = [...Array(10)]
|
||||
.map(() => Math.random().toString(36)[3])
|
||||
.join("")
|
||||
.replace(/(.|$)/g, (c) => c[!Math.round(Math.random()) ? "toString" : "toLowerCase"]());
|
||||
switch (provider.type) {
|
||||
case "ethercalc":
|
||||
case "etherpad":
|
||||
case "jitsi":
|
||||
default:
|
||||
return `${provider.endpoint}${randomString}`;
|
||||
}
|
||||
}
|
||||
|
||||
get createResourceButtonLabel() {
|
||||
if (!this.newResource.type) return;
|
||||
return this.createSentenceForType(this.newResource.type);
|
||||
}
|
||||
|
||||
@Watch("checkedAll")
|
||||
watchCheckedAll() {
|
||||
this.resource.children.elements.forEach(({ id }) => {
|
||||
if (!id) return;
|
||||
this.checkedResources[id] = this.checkedAll;
|
||||
});
|
||||
}
|
||||
|
||||
@Watch("checkedResources", { deep: true })
|
||||
watchValidCheckedResources(): string[] {
|
||||
const validCheckedResources: string[] = [];
|
||||
for (const [key, value] of Object.entries(this.checkedResources)) {
|
||||
if (value) {
|
||||
validCheckedResources.push(key);
|
||||
}
|
||||
}
|
||||
return (this.validCheckedResources = validCheckedResources);
|
||||
}
|
||||
|
||||
async deleteMultipleResources() {
|
||||
for (const resourceID of this.validCheckedResources) {
|
||||
await this.deleteResource(resourceID);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteResource(resourceID: string) {
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: DELETE_RESOURCE,
|
||||
variables: {
|
||||
id: resourceID,
|
||||
},
|
||||
update: (store, { data: { deleteResource } }) => {
|
||||
if (deleteResource == null) return;
|
||||
if (!this.resource.actor) return;
|
||||
const cachedData = store.readQuery<{ resource: IResource }>({
|
||||
query: GET_RESOURCE,
|
||||
variables: {
|
||||
path: this.resource.path,
|
||||
username: this.resource.actor.preferredUsername,
|
||||
},
|
||||
});
|
||||
if (cachedData == null) return;
|
||||
const { resource } = cachedData;
|
||||
if (resource == null) {
|
||||
console.error("Cannot update resource cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const oldResource: IResource = deleteResource;
|
||||
|
||||
resource.children.elements = resource.children.elements.filter(
|
||||
(resource) => resource.id !== oldResource.id
|
||||
);
|
||||
|
||||
store.writeQuery({
|
||||
query: GET_RESOURCE,
|
||||
variables: {
|
||||
path: this.resource.path,
|
||||
username: this.resource.actor.preferredUsername,
|
||||
},
|
||||
data: { resource },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.validCheckedResources = this.validCheckedResources.filter((id) => id !== resourceID);
|
||||
delete this.checkedResources[resourceID];
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleRename(resource: IResource) {
|
||||
this.renameModal = true;
|
||||
this.updatedResource = resource;
|
||||
}
|
||||
|
||||
async renameResource() {
|
||||
await this.updateResource(this.updatedResource);
|
||||
}
|
||||
|
||||
async updateResource(resource: IResource) {
|
||||
try {
|
||||
if (!resource.parent) return;
|
||||
await this.$apollo.mutate<{ updateResource: IResource }>({
|
||||
mutation: UPDATE_RESOURCE,
|
||||
variables: {
|
||||
id: resource.id,
|
||||
title: resource.title,
|
||||
parentId: resource.parent.id,
|
||||
path: resource.path,
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
nav.breadcrumb ul {
|
||||
align-items: center;
|
||||
|
||||
li:last-child .dropdown {
|
||||
margin-left: 5px;
|
||||
|
||||
a {
|
||||
justify-content: left;
|
||||
color: inherit;
|
||||
padding: 0.375rem 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.list-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.list-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.actions {
|
||||
margin-right: 5px;
|
||||
|
||||
& > * {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resource-item,
|
||||
.new-resource-preview {
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
border: 1px solid #c0cdd9;
|
||||
border-radius: 4px;
|
||||
color: #444b5d;
|
||||
margin-top: 14px;
|
||||
|
||||
.resource-checkbox {
|
||||
align-self: center;
|
||||
padding: 0 3px 0 10px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
&:hover .resource-checkbox,
|
||||
.resource-checkbox.checked {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +1,66 @@
|
||||
<template>
|
||||
<section class="container">
|
||||
<h1>
|
||||
{{ $t('Search results: "{search}"', { search: this.searchTerm }) }}
|
||||
</h1>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<b-tabs v-model="activeTab" type="is-boxed" class="searchTabs" @change="changeTab">
|
||||
<b-tab-item>
|
||||
<template slot="header">
|
||||
<b-icon icon="calendar"></b-icon>
|
||||
<span>
|
||||
{{ $t('Events') }} <b-tag rounded>{{ searchEvents.total }}</b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="searchEvents.total > 0" class="columns is-multiline">
|
||||
<div class="column is-one-quarter-desktop is-half-mobile"
|
||||
v-for="event in searchEvents.elements"
|
||||
:key="event.uuid">
|
||||
<EventCard
|
||||
:event="event"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-else-if="$apollo.loading === false" type="is-danger">
|
||||
{{ $t('No events found') }}
|
||||
</b-message>
|
||||
</b-tab-item>
|
||||
<!-- <b-tab-item>-->
|
||||
<!-- <template slot="header">-->
|
||||
<!-- <b-icon icon="account-multiple"></b-icon>-->
|
||||
<!-- <span>-->
|
||||
<!-- {{ $t('Groups') }} <b-tag rounded>{{ searchGroups.total }}</b-tag>-->
|
||||
<!-- </span>-->
|
||||
<!-- </template>-->
|
||||
<!-- <div v-if="searchGroups.total > 0" class="columns is-multiline">-->
|
||||
<!-- <div class="column is-one-quarter-desktop is-half-mobile"-->
|
||||
<!-- v-for="group in groups"-->
|
||||
<!-- :key="group.uuid">-->
|
||||
<!-- <group-card :group="group" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <b-message v-else-if="$apollo.loading === false" type="is-danger">-->
|
||||
<!-- {{ $t('No groups found') }}-->
|
||||
<!-- </b-message>-->
|
||||
<!-- </b-tab-item>-->
|
||||
</b-tabs>
|
||||
</section>
|
||||
<section class="container">
|
||||
<h1>{{ $t('Search results: "{search}"', { search: this.searchTerm }) }}</h1>
|
||||
<b-loading :active.sync="$apollo.loading" />
|
||||
<b-tabs v-model="activeTab" type="is-boxed" class="searchTabs" @change="changeTab">
|
||||
<b-tab-item>
|
||||
<template slot="header">
|
||||
<b-icon icon="calendar"></b-icon>
|
||||
<span>
|
||||
{{ $t("Events") }}
|
||||
<b-tag rounded>{{ searchEvents.total }}</b-tag>
|
||||
</span>
|
||||
</template>
|
||||
<div v-if="searchEvents.total > 0" class="columns is-multiline">
|
||||
<div
|
||||
class="column is-one-quarter-desktop is-half-mobile"
|
||||
v-for="event in searchEvents.elements"
|
||||
:key="event.uuid"
|
||||
>
|
||||
<EventCard :event="event" />
|
||||
</div>
|
||||
</div>
|
||||
<b-message v-else-if="$apollo.loading === false" type="is-danger">{{
|
||||
$t("No events found")
|
||||
}}</b-message>
|
||||
</b-tab-item>
|
||||
<!-- <b-tab-item>-->
|
||||
<!-- <template slot="header">-->
|
||||
<!-- <b-icon icon="account-multiple"></b-icon>-->
|
||||
<!-- <span>-->
|
||||
<!-- {{ $t('Groups') }} <b-tag rounded>{{ searchGroups.total }}</b-tag>-->
|
||||
<!-- </span>-->
|
||||
<!-- </template>-->
|
||||
<!-- <div v-if="searchGroups.total > 0" class="columns is-multiline">-->
|
||||
<!-- <div class="column is-one-quarter-desktop is-half-mobile"-->
|
||||
<!-- v-for="group in groups"-->
|
||||
<!-- :key="group.uuid">-->
|
||||
<!-- <group-card :group="group" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <b-message v-else-if="$apollo.loading === false" type="is-danger">-->
|
||||
<!-- {{ $t('No groups found') }}-->
|
||||
<!-- </b-message>-->
|
||||
<!-- </b-tab-item>-->
|
||||
</b-tabs>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { SEARCH_EVENTS, SEARCH_GROUPS } from '@/graphql/search';
|
||||
import { RouteName } from '@/router';
|
||||
import EventCard from '@/components/Event/EventCard.vue';
|
||||
import GroupCard from '@/components/Group/GroupCard.vue';
|
||||
import { Group, IGroup } from '@/types/actor';
|
||||
import { SearchEvent, SearchGroup } from '@/types/search.model';
|
||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||
import { SEARCH_EVENTS, SEARCH_GROUPS } from "../graphql/search";
|
||||
import RouteName from "../router/name";
|
||||
import EventCard from "../components/Event/EventCard.vue";
|
||||
import GroupCard from "../components/Group/GroupCard.vue";
|
||||
import { Group, IGroup } from "../types/actor";
|
||||
import { SearchEvent, SearchGroup } from "../types/search.model";
|
||||
|
||||
enum SearchTabs {
|
||||
EVENTS = 0,
|
||||
GROUPS = 1,
|
||||
PERSONS = 2, // not used right now
|
||||
EVENTS = 0,
|
||||
GROUPS = 1,
|
||||
PERSONS = 2, // not used right now
|
||||
}
|
||||
|
||||
const tabsName = {
|
||||
const tabsName: { events: number; groups: number } = {
|
||||
events: SearchTabs.EVENTS,
|
||||
groups: SearchTabs.GROUPS,
|
||||
};
|
||||
@@ -98,31 +97,43 @@ const tabsName = {
|
||||
})
|
||||
export default class Search extends Vue {
|
||||
@Prop({ type: String, required: true }) searchTerm!: string;
|
||||
@Prop({ type: String, required: false, default: 'events' }) searchType!: string;
|
||||
|
||||
@Prop({ type: String, required: false, default: "events" }) searchType!: "events" | "groups";
|
||||
|
||||
searchEvents: SearchEvent = { total: 0, elements: [] };
|
||||
|
||||
searchGroups: SearchGroup = { total: 0, elements: [] };
|
||||
|
||||
activeTab: SearchTabs = tabsName[this.searchType];
|
||||
|
||||
@Watch('searchEvents')
|
||||
@Watch("searchEvents")
|
||||
async redirectURLToEvent() {
|
||||
if (this.searchEvents.total === 1 && this.isURL(this.searchTerm)) {
|
||||
return await this.$router.replace({ name: RouteName.EVENT, params: { uuid: this.searchEvents.elements[0].uuid } });
|
||||
return await this.$router.replace({
|
||||
name: RouteName.EVENT,
|
||||
params: { uuid: this.searchEvents.elements[0].uuid },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
changeTab(index: number) {
|
||||
switch (index) {
|
||||
case SearchTabs.EVENTS:
|
||||
this.$router.push({ name: RouteName.SEARCH, params: { searchTerm: this.searchTerm, searchType: 'events' } });
|
||||
this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
params: { searchTerm: this.searchTerm, searchType: "events" },
|
||||
});
|
||||
break;
|
||||
case SearchTabs.GROUPS:
|
||||
this.$router.push({ name: RouteName.SEARCH, params: { searchTerm: this.searchTerm, searchType: 'groups' } });
|
||||
this.$router.push({
|
||||
name: RouteName.SEARCH,
|
||||
params: { searchTerm: this.searchTerm, searchType: "groups" },
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('search')
|
||||
@Watch("search")
|
||||
changeTabForResult() {
|
||||
if (this.searchEvents.total === 0 && this.searchGroups.total > 0) {
|
||||
this.activeTab = SearchTabs.GROUPS;
|
||||
@@ -132,33 +143,32 @@ export default class Search extends Vue {
|
||||
}
|
||||
}
|
||||
|
||||
@Watch('search')
|
||||
@Watch('$route')
|
||||
@Watch("search")
|
||||
@Watch("$route")
|
||||
async loadSearch() {
|
||||
await this.$apollo.queries['searchEvents'].refetch() && this.$apollo.queries['searchGroups'].refetch();
|
||||
(await this.$apollo.queries.searchEvents.refetch()) &&
|
||||
this.$apollo.queries.searchGroups.refetch();
|
||||
}
|
||||
|
||||
|
||||
get groups(): IGroup[] {
|
||||
return this.searchGroups.elements.map(group => Object.assign(new Group(), group));
|
||||
return this.searchGroups.elements.map((group) => Object.assign(new Group(), group));
|
||||
}
|
||||
|
||||
isURL(url: string): boolean {
|
||||
const a = document.createElement('a');
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
return (a.host && a.host !== window.location.host) as boolean;
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
@import "~bulma/sass/components/tabs";
|
||||
@import "~buefy/src/scss/components/tabs";
|
||||
@import "~bulma/sass/elements/tag";
|
||||
@import "~bulma/sass/utilities/_all";
|
||||
@import "~bulma/sass/components/tabs";
|
||||
@import "~buefy/src/scss/components/tabs";
|
||||
@import "~bulma/sass/elements/tag";
|
||||
|
||||
.searchTabs .tab-content {
|
||||
background: #fff;
|
||||
min-height: 10em;
|
||||
}
|
||||
.searchTabs .tab-content {
|
||||
background: #fff;
|
||||
min-height: 10em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,34 +1,40 @@
|
||||
<template>
|
||||
<aside class="section container">
|
||||
<h1 class="title">{{ $t('Settings') }}</h1>
|
||||
<div class="columns">
|
||||
<SettingsMenu class="column is-one-quarter-desktop" :menu="menu" />
|
||||
<div class="column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li v-for="route in routes.get($route.name)" :class="{ 'is-active': route.to.name === $route.name }"><router-link :to="{ name: route.to.name }">{{ route.title }}</router-link></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<aside class="section container">
|
||||
<h1 class="title">{{ $t("Settings") }}</h1>
|
||||
<div class="columns">
|
||||
<SettingsMenu class="column is-one-quarter-desktop" :menu="menu" />
|
||||
<div class="column">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li
|
||||
v-for="route in routes.get($route.name)"
|
||||
:class="{ 'is-active': route.to.name === $route.name }"
|
||||
:key="route.to.name"
|
||||
>
|
||||
<router-link :to="{ name: route.to.name }">{{ route.title }}</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue, Watch } from 'vue-property-decorator';
|
||||
import SettingsMenu from '@/components/Settings/SettingsMenu.vue';
|
||||
import { RouteName } from '@/router';
|
||||
import { ISettingMenuSection } from '@/types/setting-menu.model';
|
||||
import { Route } from 'vue-router';
|
||||
import { IPerson, Person } from '@/types/actor';
|
||||
import { IDENTITIES } from '@/graphql/actor';
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { Route } from "vue-router";
|
||||
import SettingsMenu from "../components/Settings/SettingsMenu.vue";
|
||||
import RouteName from "../router/name";
|
||||
import { ISettingMenuSection } from "../types/setting-menu.model";
|
||||
import { IPerson, Person } from "../types/actor";
|
||||
import { IDENTITIES } from "../graphql/actor";
|
||||
|
||||
@Component({
|
||||
components: { SettingsMenu },
|
||||
apollo: {
|
||||
identities: {
|
||||
query: IDENTITIES,
|
||||
update: (data) => { return data.identities.map(identity => new Person(identity)); },
|
||||
update: (data) => data.identities.map((identity: IPerson) => new Person(identity)),
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -36,80 +42,82 @@ export default class Settings extends Vue {
|
||||
RouteName = RouteName;
|
||||
|
||||
menu: ISettingMenuSection[] = [];
|
||||
|
||||
identities!: IPerson[];
|
||||
|
||||
newIdentity!: ISettingMenuSection;
|
||||
|
||||
mounted() {
|
||||
this.newIdentity = {
|
||||
title: this.$t('New profile') as string,
|
||||
title: this.$t("New profile") as string,
|
||||
to: { name: RouteName.CREATE_IDENTITY } as Route,
|
||||
};
|
||||
this.menu = [
|
||||
{
|
||||
title: this.$t('Account') as string,
|
||||
title: this.$t("Account") as string,
|
||||
to: { name: RouteName.ACCOUNT_SETTINGS } as Route,
|
||||
items: [
|
||||
{
|
||||
title: this.$t('General') as string,
|
||||
title: this.$t("General") as string,
|
||||
to: { name: RouteName.ACCOUNT_SETTINGS_GENERAL } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t('Preferences') as string,
|
||||
title: this.$t("Preferences") as string,
|
||||
to: { name: RouteName.PREFERENCES } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t('Notifications') as string,
|
||||
title: this.$t("Notifications") as string,
|
||||
to: { name: RouteName.NOTIFICATIONS } as Route,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this.$t('Profiles') as string,
|
||||
title: this.$t("Profiles") as string,
|
||||
to: { name: RouteName.IDENTITIES } as Route,
|
||||
items: [this.newIdentity],
|
||||
},
|
||||
{
|
||||
title: this.$t('Moderation') as string,
|
||||
title: this.$t("Moderation") as string,
|
||||
to: { name: RouteName.MODERATION } as Route,
|
||||
items: [
|
||||
{
|
||||
title: this.$t('Reports') as string,
|
||||
title: this.$t("Reports") as string,
|
||||
to: { name: RouteName.REPORTS } as Route,
|
||||
items: [
|
||||
{
|
||||
title: this.$t('Report') as string,
|
||||
title: this.$t("Report") as string,
|
||||
to: { name: RouteName.REPORT } as Route,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this.$t('Moderation log') as string,
|
||||
title: this.$t("Moderation log") as string,
|
||||
to: { name: RouteName.REPORT_LOGS } as Route,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this.$t('Admin') as string,
|
||||
title: this.$t("Admin") as string,
|
||||
to: { name: RouteName.ADMIN } as Route,
|
||||
items: [
|
||||
{
|
||||
title: this.$t('Dashboard') as string,
|
||||
title: this.$t("Dashboard") as string,
|
||||
to: { name: RouteName.ADMIN_DASHBOARD } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t('Instance settings') as string,
|
||||
title: this.$t("Instance settings") as string,
|
||||
to: { name: RouteName.ADMIN_SETTINGS } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t('Federation') as string,
|
||||
title: this.$t("Federation") as string,
|
||||
to: { name: RouteName.RELAYS } as Route,
|
||||
items: [
|
||||
{
|
||||
title: this.$t('Followings') as string,
|
||||
title: this.$t("Followings") as string,
|
||||
to: { name: RouteName.RELAY_FOLLOWINGS } as Route,
|
||||
},
|
||||
{
|
||||
title: this.$t('Followers') as string,
|
||||
title: this.$t("Followers") as string,
|
||||
to: { name: RouteName.RELAY_FOLLOWERS } as Route,
|
||||
},
|
||||
],
|
||||
@@ -119,17 +127,20 @@ export default class Settings extends Vue {
|
||||
];
|
||||
}
|
||||
|
||||
@Watch('identities')
|
||||
updateIdentities(identities) {
|
||||
@Watch("identities")
|
||||
updateIdentities(identities: IPerson[]) {
|
||||
if (!identities) return;
|
||||
if (!this.menu[1].items) return;
|
||||
this.menu[1].items = [];
|
||||
this.menu[1].items.push(...identities.map((identity: IPerson) => {
|
||||
return {
|
||||
to: { name: RouteName.UPDATE_IDENTITY, params: { identityName: identity.preferredUsername } } as unknown as Route,
|
||||
this.menu[1].items.push(
|
||||
...identities.map((identity: IPerson) => ({
|
||||
to: ({
|
||||
name: RouteName.UPDATE_IDENTITY,
|
||||
params: { identityName: identity.preferredUsername },
|
||||
} as unknown) as Route,
|
||||
title: `@${identity.preferredUsername}`,
|
||||
};
|
||||
}));
|
||||
}))
|
||||
);
|
||||
this.menu[1].items.push(this.newIdentity);
|
||||
}
|
||||
|
||||
@@ -138,7 +149,7 @@ export default class Settings extends Vue {
|
||||
}
|
||||
|
||||
getPath(object: ISettingMenuSection[]) {
|
||||
function iter(menu: ISettingMenuSection[]|ISettingMenuSection, acc: ISettingMenuSection[]) {
|
||||
function iter(menu: ISettingMenuSection[] | ISettingMenuSection, acc: ISettingMenuSection[]) {
|
||||
if (Array.isArray(menu)) {
|
||||
return menu.forEach((item: ISettingMenuSection) => {
|
||||
iter(item, acc.concat(item));
|
||||
@@ -160,7 +171,7 @@ export default class Settings extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
aside.section {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
aside.section {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,130 +1,153 @@
|
||||
<template>
|
||||
<section>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t('Email') }}</h2>
|
||||
</div>
|
||||
<i18n tag="p" class="content" v-if="loggedUser" path="Your current email is {email}. You use it to log in.">
|
||||
<b slot="email">{{ loggedUser.email }}</b>
|
||||
</i18n>
|
||||
<b-notification
|
||||
type="is-danger"
|
||||
has-icon
|
||||
aria-close-label="Close notification"
|
||||
role="alert"
|
||||
:key="error"
|
||||
v-for="error in changeEmailErrors"
|
||||
>
|
||||
{{ error }}
|
||||
</b-notification>
|
||||
<form @submit.prevent="resetEmailAction" ref="emailForm" class="form">
|
||||
<b-field :label="$t('New email')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="email"
|
||||
v-model="newEmail"
|
||||
/>
|
||||
</b-field>
|
||||
<p class="help">{{ $t("You'll receive a confirmation email.") }}</p>
|
||||
<b-field :label="$t('Password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="passwordForEmailChange"
|
||||
/>
|
||||
</b-field>
|
||||
<button class="button is-primary" :disabled="!($refs.emailForm && $refs.emailForm.checkValidity())">
|
||||
{{ $t('Change my email') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t('Password') }}</h2>
|
||||
</div>
|
||||
<b-notification
|
||||
type="is-danger"
|
||||
has-icon
|
||||
aria-close-label="Close notification"
|
||||
role="alert"
|
||||
:key="error"
|
||||
v-for="error in changePasswordErrors"
|
||||
>
|
||||
{{ error }}
|
||||
</b-notification>
|
||||
<form @submit.prevent="resetPasswordAction" ref="passwordForm" class="form">
|
||||
<b-field :label="$t('Old password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="oldPassword"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field :label="$t('New password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="newPassword"
|
||||
/>
|
||||
</b-field>
|
||||
<button class="button is-primary" :disabled="!($refs.passwordForm && $refs.passwordForm.checkValidity())">
|
||||
{{ $t('Change my password') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t('Delete account') }}</h2>
|
||||
</div>
|
||||
<p class="content">{{ $t('Deleting my account will delete all of my identities.')}}</p>
|
||||
<b-button @click="openDeleteAccountModal" type="is-danger">{{ $t('Delete my account') }}</b-button>
|
||||
<section>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t("Email") }}</h2>
|
||||
</div>
|
||||
<i18n
|
||||
tag="p"
|
||||
class="content"
|
||||
v-if="loggedUser"
|
||||
path="Your current email is {email}. You use it to log in."
|
||||
>
|
||||
<b slot="email">{{ loggedUser.email }}</b>
|
||||
</i18n>
|
||||
<b-notification
|
||||
type="is-danger"
|
||||
has-icon
|
||||
aria-close-label="Close notification"
|
||||
role="alert"
|
||||
:key="error"
|
||||
v-for="error in changeEmailErrors"
|
||||
>{{ error }}</b-notification
|
||||
>
|
||||
<form @submit.prevent="resetEmailAction" ref="emailForm" class="form">
|
||||
<b-field :label="$t('New email')">
|
||||
<b-input aria-required="true" required type="email" v-model="newEmail" />
|
||||
</b-field>
|
||||
<p class="help">{{ $t("You'll receive a confirmation email.") }}</p>
|
||||
<b-field :label="$t('Password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="passwordForEmailChange"
|
||||
/>
|
||||
</b-field>
|
||||
<button
|
||||
class="button is-primary"
|
||||
:disabled="!($refs.emailForm && $refs.emailForm.checkValidity())"
|
||||
>
|
||||
{{ $t("Change my email") }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t("Password") }}</h2>
|
||||
</div>
|
||||
<b-notification
|
||||
type="is-danger"
|
||||
has-icon
|
||||
aria-close-label="Close notification"
|
||||
role="alert"
|
||||
:key="error"
|
||||
v-for="error in changePasswordErrors"
|
||||
>{{ error }}</b-notification
|
||||
>
|
||||
<form @submit.prevent="resetPasswordAction" ref="passwordForm" class="form">
|
||||
<b-field :label="$t('Old password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="oldPassword"
|
||||
/>
|
||||
</b-field>
|
||||
<b-field :label="$t('New password')">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="newPassword"
|
||||
/>
|
||||
</b-field>
|
||||
<button
|
||||
class="button is-primary"
|
||||
:disabled="!($refs.passwordForm && $refs.passwordForm.checkValidity())"
|
||||
>
|
||||
{{ $t("Change my password") }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="setting-title">
|
||||
<h2>{{ $t("Delete account") }}</h2>
|
||||
</div>
|
||||
<p class="content">{{ $t("Deleting my account will delete all of my identities.") }}</p>
|
||||
<b-button @click="openDeleteAccountModal" type="is-danger">
|
||||
{{ $t("Delete my account") }}
|
||||
</b-button>
|
||||
|
||||
<b-modal :active.sync="isDeleteAccountModalActive"
|
||||
has-modal-card full-screen :can-cancel="false">
|
||||
<section class="hero is-primary is-fullheight">
|
||||
<div class="hero-body has-text-centered">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third-desktop is-offset-one-third-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t('Deleting your Mobilizon account') }}
|
||||
</h1>
|
||||
<p class="content">
|
||||
{{ $t("Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever.") }}
|
||||
<br>
|
||||
<b>{{ $t('There will be no way to recover your data.') }}</b>
|
||||
</p>
|
||||
<p class="content">{{ $t('Please enter your password to confirm this action.')}}</p>
|
||||
<form @submit.prevent="deleteAccount">
|
||||
<b-field>
|
||||
<b-input type="password" v-model="passwordForAccountDeletion" password-reveal icon="lock" :placeholder="$t('Password')"/>
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-danger" size="is-large">{{ $t('Delete everything') }}</b-button>
|
||||
</form>
|
||||
<div class="cancel-button">
|
||||
<b-button type="is-light" @click="isDeleteAccountModalActive = false">{{ $t('Cancel') }}</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<b-modal
|
||||
:active.sync="isDeleteAccountModalActive"
|
||||
has-modal-card
|
||||
full-screen
|
||||
:can-cancel="false"
|
||||
>
|
||||
<section class="hero is-primary is-fullheight">
|
||||
<div class="hero-body has-text-centered">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column is-one-third-desktop is-offset-one-third-desktop">
|
||||
<h1 class="title">{{ $t("Deleting your Mobilizon account") }}</h1>
|
||||
<p class="content">
|
||||
{{
|
||||
$t(
|
||||
"Are you really sure you want to delete your whole account? You'll lose everything. Identities, settings, events created, messages and participations will be gone forever."
|
||||
)
|
||||
}}
|
||||
<br />
|
||||
<b>{{ $t("There will be no way to recover your data.") }}</b>
|
||||
</p>
|
||||
<p class="content">
|
||||
{{ $t("Please enter your password to confirm this action.") }}
|
||||
</p>
|
||||
<form @submit.prevent="deleteAccount">
|
||||
<b-field>
|
||||
<b-input
|
||||
type="password"
|
||||
v-model="passwordForAccountDeletion"
|
||||
password-reveal
|
||||
icon="lock"
|
||||
:placeholder="$t('Password')"
|
||||
/>
|
||||
</b-field>
|
||||
<b-button native-type="submit" type="is-danger" size="is-large">
|
||||
{{ $t("Delete everything") }}
|
||||
</b-button>
|
||||
</form>
|
||||
<div class="cancel-button">
|
||||
<b-button type="is-light" @click="isDeleteAccountModalActive = false">
|
||||
{{ $t("Cancel") }}
|
||||
</b-button>
|
||||
</div>
|
||||
</section>
|
||||
</b-modal>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</b-modal>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { CHANGE_EMAIL, CHANGE_PASSWORD, DELETE_ACCOUNT, LOGGED_USER } from '@/graphql/user';
|
||||
import { RouteName } from '@/router';
|
||||
import { Refs } from '@/shims-vue';
|
||||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
import { logout } from '@/utils/auth';
|
||||
import { Component, Vue, Ref } from "vue-property-decorator";
|
||||
import { CHANGE_EMAIL, CHANGE_PASSWORD, DELETE_ACCOUNT, LOGGED_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
import { logout } from "../../utils/auth";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -132,21 +155,25 @@ import { logout } from '@/utils/auth';
|
||||
},
|
||||
})
|
||||
export default class AccountSettings extends Vue {
|
||||
$refs!: Refs<{
|
||||
passwordForm: HTMLElement,
|
||||
}>;
|
||||
@Ref("passwordForm") readonly passwordForm!: HTMLElement;
|
||||
|
||||
loggedUser!: ICurrentUser;
|
||||
|
||||
passwordForEmailChange: string = '';
|
||||
newEmail: string = '';
|
||||
passwordForEmailChange = "";
|
||||
|
||||
newEmail = "";
|
||||
|
||||
changeEmailErrors: string[] = [];
|
||||
|
||||
oldPassword: string = '';
|
||||
newPassword: string = '';
|
||||
oldPassword = "";
|
||||
|
||||
newPassword = "";
|
||||
|
||||
changePasswordErrors: string[] = [];
|
||||
|
||||
isDeleteAccountModalActive: boolean = false;
|
||||
passwordForAccountDeletion: string = '';
|
||||
isDeleteAccountModalActive = false;
|
||||
|
||||
passwordForAccountDeletion = "";
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
@@ -162,11 +189,15 @@ export default class AccountSettings extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
this.$notifier.info(this.$t("The account's email address was changed. Check your emails to verify it.") as string);
|
||||
this.newEmail = '';
|
||||
this.passwordForEmailChange = '';
|
||||
this.$notifier.info(
|
||||
this.$t(
|
||||
"The account's email address was changed. Check your emails to verify it."
|
||||
) as string
|
||||
);
|
||||
this.newEmail = "";
|
||||
this.passwordForEmailChange = "";
|
||||
} catch (err) {
|
||||
this.handleErrors('email', err);
|
||||
this.handleErrors("email", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,14 +213,14 @@ export default class AccountSettings extends Vue {
|
||||
},
|
||||
});
|
||||
|
||||
this.$notifier.success(this.$t('The password was successfully changed') as string);
|
||||
this.$notifier.success(this.$t("The password was successfully changed") as string);
|
||||
} catch (err) {
|
||||
this.handleErrors('password', err);
|
||||
this.handleErrors("password", err);
|
||||
}
|
||||
}
|
||||
|
||||
protected async openDeleteAccountModal() {
|
||||
this.passwordForAccountDeletion = '';
|
||||
this.passwordForAccountDeletion = "";
|
||||
this.isDeleteAccountModalActive = true;
|
||||
}
|
||||
|
||||
@@ -203,15 +234,15 @@ export default class AccountSettings extends Vue {
|
||||
});
|
||||
await logout(this.$apollo.provider.defaultClient);
|
||||
this.$buefy.notification.open({
|
||||
message: this.$t('Your account has been successfully deleted') as string,
|
||||
type: 'is-success',
|
||||
position: 'is-bottom-right',
|
||||
message: this.$t("Your account has been successfully deleted") as string,
|
||||
type: "is-success",
|
||||
position: "is-bottom-right",
|
||||
duration: 5000,
|
||||
});
|
||||
|
||||
return await this.$router.push({ name: RouteName.HOME });
|
||||
} catch (err) {
|
||||
this.handleErrors('delete', err);
|
||||
this.handleErrors("delete", err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,12 +250,12 @@ export default class AccountSettings extends Vue {
|
||||
console.error(err);
|
||||
|
||||
if (err.graphQLErrors !== undefined) {
|
||||
err.graphQLErrors.forEach(({ message }) => {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
switch (type) {
|
||||
case 'email':
|
||||
case "email":
|
||||
this.changeEmailErrors.push(this.convertMessage(message) as string);
|
||||
break;
|
||||
case 'password':
|
||||
case "password":
|
||||
this.changePasswordErrors.push(this.convertMessage(message) as string);
|
||||
break;
|
||||
}
|
||||
@@ -234,40 +265,40 @@ export default class AccountSettings extends Vue {
|
||||
|
||||
private convertMessage(message: string) {
|
||||
switch (message) {
|
||||
case 'The password provided is invalid':
|
||||
return this.$t('The password provided is invalid');
|
||||
case 'The new email must be different':
|
||||
return this.$t('The new email must be different');
|
||||
case "The password provided is invalid":
|
||||
return this.$t("The password provided is invalid");
|
||||
case "The new email must be different":
|
||||
return this.$t("The new email must be different");
|
||||
case "The new email doesn't seem to be valid":
|
||||
return this.$t("The new email doesn't seem to be valid");
|
||||
case 'The current password is invalid':
|
||||
return this.$t('The current password is invalid');
|
||||
case 'The new password must be different':
|
||||
return this.$t('The new password must be different');
|
||||
case "The current password is invalid":
|
||||
return this.$t("The current password is invalid");
|
||||
case "The new password must be different":
|
||||
return this.$t("The new password must be different");
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "@/variables.scss";
|
||||
@import "@/variables.scss";
|
||||
|
||||
.setting-title {
|
||||
margin-top: 3rem;
|
||||
.setting-title {
|
||||
margin-top: 3rem;
|
||||
|
||||
h2 {
|
||||
display: inline;
|
||||
background: $secondary;
|
||||
padding: 2px 7.5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
display: inline;
|
||||
background: $secondary;
|
||||
padding: 2px 7.5px;
|
||||
text-transform: uppercase;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
.cancel-button {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
/deep/ .modal .modal-background {
|
||||
background-color: initial;
|
||||
}
|
||||
/deep/ .modal .modal-background {
|
||||
background-color: initial;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,81 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="subtitle">{{ $t('No notification settings yet') }}</h2>
|
||||
<div v-if="loggedUser">
|
||||
<h2 class="subtitle">{{ $t("No notification settings yet") }}</h2>
|
||||
<div class="field">
|
||||
<b-checkbox disabled v-model="notificationEventUpdates">
|
||||
<strong>{{ $t("Important event updates") }}</strong>
|
||||
<p>
|
||||
{{
|
||||
$t("Like title update, start or end date change, event being confirmed or cancelled.")
|
||||
}}
|
||||
</p>
|
||||
</b-checkbox>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-checkbox v-model="notificationOnDay" @input="updateSetting({ notificationOnDay })">
|
||||
<strong>{{ $t("Notification on the day of the event") }}</strong>
|
||||
<p>
|
||||
{{ $t("We'll use your timezone settings to send a recap of the morning of the event.") }}
|
||||
</p>
|
||||
<span v-if="loggedUser.settings.timezone">{{
|
||||
$t("Your timezone is currently set to {timezone}.", {
|
||||
timezone: loggedUser.settings.timezone,
|
||||
})
|
||||
}}</span>
|
||||
<span v-else>{{ $t("You can pick your timezone into your preferences.") }}</span>
|
||||
</b-checkbox>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-checkbox v-model="notificationEachWeek" @input="updateSetting({ notificationEachWeek })">
|
||||
<strong>{{ $t("Recap every week") }}</strong>
|
||||
<p>
|
||||
{{ $t("You'll get a weekly recap every Monday for upcoming events, if you have any.") }}
|
||||
</p>
|
||||
</b-checkbox>
|
||||
</div>
|
||||
<div class="field">
|
||||
<b-checkbox
|
||||
v-model="notificationBeforeEvent"
|
||||
@input="updateSetting({ notificationBeforeEvent })"
|
||||
>
|
||||
<strong>{{ $t("Notification before the event") }}</strong>
|
||||
<p>
|
||||
{{
|
||||
$t(
|
||||
"We'll send you an email one hour before the event begins, to be sure you won't forget about it."
|
||||
)
|
||||
}}
|
||||
</p>
|
||||
</b-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { USER_SETTINGS, SET_USER_SETTINGS } from "../../graphql/user";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
apollo: {
|
||||
loggedUser: USER_SETTINGS,
|
||||
},
|
||||
})
|
||||
export default class Notifications extends Vue {
|
||||
loggedUser!: ICurrentUser;
|
||||
|
||||
notificationEventUpdates = true;
|
||||
|
||||
notificationOnDay = true;
|
||||
|
||||
notificationEachWeek = false;
|
||||
|
||||
notificationBeforeEvent = false;
|
||||
|
||||
async updateSetting(variables: object) {
|
||||
await this.$apollo.mutate<{ setUserSettings: string }>({
|
||||
mutation: SET_USER_SETTINGS,
|
||||
variables,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,13 +1,91 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="subtitle">{{ $t('No preferences yet') }}</h2>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="subtitle">{{ $t("No preferences yet") }}</h2>
|
||||
<b-field :label="$t('Timezone')">
|
||||
<b-select
|
||||
:placeholder="$t('Select a timezone')"
|
||||
:loading="!config || !loggedUser"
|
||||
v-model="selectedTimezone"
|
||||
>
|
||||
<optgroup :label="group" v-for="(groupTimezones, group) in timezones" :key="group">
|
||||
<option
|
||||
v-for="timezone in groupTimezones"
|
||||
:value="`${group}/${timezone}`"
|
||||
:key="timezone"
|
||||
>
|
||||
{{ sanitize(timezone) }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</b-select>
|
||||
</b-field>
|
||||
<span>{{
|
||||
$t("Timezone detected as {timezone}.", {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
})
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { TIMEZONES } from "../../graphql/config";
|
||||
import { USER_SETTINGS, SET_USER_SETTINGS } from "../../graphql/user";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
|
||||
@Component
|
||||
@Component({
|
||||
apollo: {
|
||||
config: TIMEZONES,
|
||||
loggedUser: USER_SETTINGS,
|
||||
},
|
||||
})
|
||||
export default class Preferences extends Vue {
|
||||
config!: IConfig;
|
||||
|
||||
loggedUser!: ICurrentUser;
|
||||
|
||||
selectedTimezone: string | null = null;
|
||||
|
||||
@Watch("loggedUser")
|
||||
setSavedTimezone(loggedUser: ICurrentUser) {
|
||||
if (loggedUser && loggedUser.settings.timezone) {
|
||||
this.selectedTimezone = loggedUser.settings.timezone;
|
||||
} else {
|
||||
this.selectedTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
}
|
||||
|
||||
sanitize(timezone: string): string {
|
||||
return timezone.split("_").join(" ").replace("St ", "St. ").split("/").join(" - ");
|
||||
}
|
||||
|
||||
get timezones() {
|
||||
if (!this.config || !this.config.timezones) return {};
|
||||
return this.config.timezones.reduce((acc: { [key: string]: Array<string> }, val: string) => {
|
||||
const components = val.split("/");
|
||||
const [prefix, suffix] = [components.shift() as string, components.join("/")];
|
||||
const pushOrCreate = (
|
||||
acc: { [key: string]: Array<string> },
|
||||
prefix: string,
|
||||
suffix: string
|
||||
) => {
|
||||
(acc[prefix] = acc[prefix] || []).push(suffix);
|
||||
return acc;
|
||||
};
|
||||
if (suffix) {
|
||||
return pushOrCreate(acc, prefix, suffix);
|
||||
}
|
||||
return pushOrCreate(acc, this.$t("Other") as string, prefix);
|
||||
}, {});
|
||||
}
|
||||
|
||||
@Watch("selectedTimezone")
|
||||
async updateTimezone() {
|
||||
await this.$apollo.mutate<{ setUserSetting: string }>({
|
||||
mutation: SET_USER_SETTINGS,
|
||||
variables: {
|
||||
timezone: this.selectedTimezone,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
<template>
|
||||
<div class="container section">
|
||||
<h2 class="title">{{ $t('Privacy Policy')}}</h2>
|
||||
<div class="content" v-html="config.terms.bodyHtml" />
|
||||
</div>
|
||||
<div class="container section">
|
||||
<h2 class="title">{{ $t("Privacy Policy") }}</h2>
|
||||
<div class="content" v-html="config.terms.bodyHtml" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Component,
|
||||
Vue, Watch,
|
||||
} from 'vue-property-decorator';
|
||||
import { TERMS } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { InstanceTermsType } from '@/types/admin.model';
|
||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
||||
import { TERMS } from "@/graphql/config";
|
||||
import { IConfig } from "@/types/config.model";
|
||||
import { InstanceTermsType } from "@/types/admin.model";
|
||||
import RouteName from "../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -32,13 +29,14 @@ import { InstanceTermsType } from '@/types/admin.model';
|
||||
})
|
||||
export default class Terms extends Vue {
|
||||
config!: IConfig;
|
||||
locale: string|null = null;
|
||||
|
||||
locale: string | null = null;
|
||||
|
||||
created() {
|
||||
this.locale = this.$i18n.locale;
|
||||
}
|
||||
|
||||
@Watch('config', { deep: true })
|
||||
@Watch("config", { deep: true })
|
||||
watchConfig(config: IConfig) {
|
||||
if (config.terms.type) {
|
||||
console.log(this.config.terms);
|
||||
@@ -55,10 +53,10 @@ export default class Terms extends Vue {
|
||||
RouteName = RouteName;
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@/variables.scss";
|
||||
<style lang="scss" scoped>
|
||||
@import "@/variables.scss";
|
||||
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
</style>
|
||||
main > .container {
|
||||
background: $white;
|
||||
}
|
||||
</style>
|
||||
|
||||
58
js/src/views/Todos/Todo.vue
Normal file
58
js/src/views/Todos/Todo.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<section class="section container" v-if="todo">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: todo.todoList.actor.preferredUsername },
|
||||
}"
|
||||
>{{ todo.todoList.actor.preferredUsername }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link :to="{ name: RouteName.TODO_LIST, params: { id: todo.todoList.id } }">
|
||||
{{ todo.todoList.title }}
|
||||
</router-link>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link :to="{ name: RouteName.TODO }" aria-current="page">
|
||||
{{ todo.title }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<full-todo :todo="todo" />
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { GET_TODO } from "@/graphql/todos";
|
||||
import { ITodo } from "@/types/todos";
|
||||
import FullTodo from "@/components/Todo/FullTodo.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
FullTodo,
|
||||
},
|
||||
apollo: {
|
||||
todo: {
|
||||
query: GET_TODO,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.todoId,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
export default class Todo extends Vue {
|
||||
@Prop({ type: String, required: true }) todoId!: string;
|
||||
|
||||
todo!: ITodo;
|
||||
|
||||
RouteName = RouteName;
|
||||
}
|
||||
</script>
|
||||
114
js/src/views/Todos/TodoList.vue
Normal file
114
js/src/views/Todos/TodoList.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<section class="container section" v-if="todoList">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.GROUP,
|
||||
params: { preferredUsername: todoList.actor.preferredUsername },
|
||||
}"
|
||||
>{{ todoList.actor.preferredUsername }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.TODO_LISTS,
|
||||
params: { preferredUsername: todoList.actor.preferredUsername },
|
||||
}"
|
||||
>{{ $t("Task lists") }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link :to="{ name: RouteName.TODO_LIST, params: { id: todoList.id } }">
|
||||
{{ todoList.title }}
|
||||
</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<h2 class="title">{{ todoList.title }}</h2>
|
||||
<div v-for="todo in todoList.todos.elements" :key="todo.id">
|
||||
<compact-todo :todo="todo" />
|
||||
</div>
|
||||
<form class="form box" @submit.prevent="createNewTodo">
|
||||
<b-field>
|
||||
<b-checkbox v-model="newTodo.status" />
|
||||
<b-input expanded v-model="newTodo.title" />
|
||||
</b-field>
|
||||
<b-button native-type="submit">{{ $t("Add a todo") }}</b-button>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { ITodo, ITodoList } from "@/types/todos";
|
||||
import { CREATE_TODO, FETCH_TODO_LIST } from "@/graphql/todos";
|
||||
import CompactTodo from "@/components/Todo/CompactTodo.vue";
|
||||
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
|
||||
import { IActor } from "@/types/actor";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
CompactTodo,
|
||||
},
|
||||
apollo: {
|
||||
todoList: {
|
||||
query: FETCH_TODO_LIST,
|
||||
variables() {
|
||||
return {
|
||||
id: this.$route.params.id,
|
||||
};
|
||||
},
|
||||
},
|
||||
currentActor: CURRENT_ACTOR_CLIENT,
|
||||
},
|
||||
})
|
||||
export default class TodoList extends Vue {
|
||||
@Prop({ type: String, required: true }) id!: string;
|
||||
|
||||
todoList!: ITodoList;
|
||||
|
||||
currentActor!: IActor;
|
||||
|
||||
newTodo: ITodo = { title: "", status: false };
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
async createNewTodo() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: CREATE_TODO,
|
||||
variables: {
|
||||
title: this.newTodo.title,
|
||||
status: this.newTodo.status,
|
||||
todoListId: this.id,
|
||||
},
|
||||
update: (store, { data }) => {
|
||||
if (data == null) return;
|
||||
const cachedData = store.readQuery<{ todoList: ITodoList }>({
|
||||
query: FETCH_TODO_LIST,
|
||||
variables: { id: this.todoList.id },
|
||||
});
|
||||
if (cachedData == null) return;
|
||||
const { todoList } = cachedData;
|
||||
if (todoList === null) {
|
||||
console.error("Cannot update event notes cache, because of null value.");
|
||||
return;
|
||||
}
|
||||
const newTodo: ITodo = data.createTodo;
|
||||
newTodo.creator = this.currentActor;
|
||||
|
||||
todoList.todos.elements = todoList.todos.elements.concat([newTodo]);
|
||||
|
||||
store.writeQuery({
|
||||
query: FETCH_TODO_LIST,
|
||||
variables: { id: this.todoList.id },
|
||||
data: { todoList },
|
||||
});
|
||||
},
|
||||
});
|
||||
this.newTodo = { title: "", status: false };
|
||||
}
|
||||
}
|
||||
</script>
|
||||
100
js/src/views/Todos/TodoLists.vue
Normal file
100
js/src/views/Todos/TodoLists.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<template>
|
||||
<div class="container section" v-if="group">
|
||||
<nav class="breadcrumb" aria-label="breadcrumbs">
|
||||
<ul>
|
||||
<li>
|
||||
<router-link
|
||||
:to="{ name: RouteName.GROUP, params: { preferredUsername: group.preferredUsername } }"
|
||||
>{{ group.preferredUsername }}</router-link
|
||||
>
|
||||
</li>
|
||||
<li class="is-active">
|
||||
<router-link
|
||||
:to="{
|
||||
name: RouteName.TODO_LISTS,
|
||||
params: { preferredUsername: group.preferredUsername },
|
||||
}"
|
||||
>{{ $t("Task lists") }}</router-link
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<section>
|
||||
<form class="form" @submit.prevent="createNewTodoList">
|
||||
<b-field :label="$t('List title')">
|
||||
<b-input v-model="newTodoList.title" />
|
||||
</b-field>
|
||||
<b-button native-type="submit">{{ $t("Create a new list") }}</b-button>
|
||||
</form>
|
||||
<div v-for="todoList in todoLists" :key="todoList.id">
|
||||
<router-link :to="{ name: RouteName.TODO_LIST, params: { id: todoList.id } }">
|
||||
<h3 class="is-size-3">
|
||||
{{
|
||||
$tc("{title} ({count} todos)", todoList.todos.total, {
|
||||
count: todoList.todos.total,
|
||||
title: todoList.title,
|
||||
})
|
||||
}}
|
||||
</h3>
|
||||
</router-link>
|
||||
<compact-todo :todo="todo" v-for="todo in todoList.todos.elements" :key="todo.id" />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { FETCH_GROUP } from "@/graphql/actor";
|
||||
import { IGroup } from "@/types/actor";
|
||||
import { ITodoList } from "@/types/todos";
|
||||
import { CREATE_TODO_LIST } from "@/graphql/todos";
|
||||
import CompactTodo from "@/components/Todo/CompactTodo.vue";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
group: {
|
||||
query: FETCH_GROUP,
|
||||
variables() {
|
||||
return {
|
||||
name: this.$route.params.preferredUsername,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
CompactTodo,
|
||||
},
|
||||
})
|
||||
export default class TodoLists extends Vue {
|
||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||
|
||||
group!: IGroup;
|
||||
|
||||
newTodoList: ITodoList = {
|
||||
title: "",
|
||||
id: "",
|
||||
todos: { elements: [], total: 0 },
|
||||
};
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
get todoLists() {
|
||||
return this.group.todoLists.elements;
|
||||
}
|
||||
|
||||
get todoListsCount() {
|
||||
return this.group.todoLists.total;
|
||||
}
|
||||
|
||||
async createNewTodoList() {
|
||||
await this.$apollo.mutate({
|
||||
mutation: CREATE_TODO_LIST,
|
||||
variables: {
|
||||
title: this.newTodoList.title,
|
||||
groupId: this.group.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -1,31 +1,33 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ $t('Your email is being changed') }}
|
||||
</h1>
|
||||
<h1 class="title" v-if="loading">{{ $t("Your email is being changed") }}</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed">
|
||||
<b-message :title="$t('Error while changing email')" type="is-danger">
|
||||
{{ $t('Either the email has already been changed, either the validation token is incorrect.') }}
|
||||
{{
|
||||
$t(
|
||||
"Either the email has already been changed, either the validation token is incorrect."
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
</div>
|
||||
<h1 class="title" v-else>
|
||||
{{ $t('Your email has been changed') }}
|
||||
</h1>
|
||||
<h1 class="title" v-else>{{ $t("Your email has been changed") }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { VALIDATE_EMAIL } from '@/graphql/user';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { VALIDATE_EMAIL } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
|
||||
@Component
|
||||
export default class Validate extends Vue {
|
||||
@Prop({ type: String, required: true }) token!: string;
|
||||
|
||||
loading = true;
|
||||
|
||||
failed = false;
|
||||
|
||||
async created() {
|
||||
@@ -34,7 +36,7 @@ export default class Validate extends Vue {
|
||||
|
||||
async validateAction() {
|
||||
try {
|
||||
await this.$apollo.mutate<{ validateEmail }>({
|
||||
await this.$apollo.mutate<{ validateEmail: ICurrentUser }>({
|
||||
mutation: VALIDATE_EMAIL,
|
||||
variables: {
|
||||
token: this.token,
|
||||
@@ -42,10 +44,10 @@ export default class Validate extends Vue {
|
||||
});
|
||||
this.loading = false;
|
||||
return await this.$router.push({ name: RouteName.HOME });
|
||||
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
this.failed = true;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,36 +2,55 @@
|
||||
<section class="section container" v-if="!currentUser.isLoggedIn">
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t('Welcome back!') }}
|
||||
</h1>
|
||||
<b-message v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN" title="Info" type="is-info">
|
||||
{{ $t('You need to login.') }}
|
||||
</b-message>
|
||||
<h1 class="title">{{ $t("Welcome back!") }}</h1>
|
||||
<b-message
|
||||
v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN"
|
||||
title="Info"
|
||||
type="is-info"
|
||||
:aria-close-label="$t('Close')"
|
||||
>{{ $t("You need to login.") }}</b-message
|
||||
>
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">
|
||||
<span v-if="error === LoginError.USER_NOT_CONFIRMED">
|
||||
<span>{{ $t("The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder.") }}</span>
|
||||
<span>
|
||||
{{
|
||||
$t(
|
||||
"The user account you're trying to login as has not been confirmed yet. Check your email inbox and eventually your spam folder."
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<i18n path="You may also ask to {resend_confirmation_email}.">
|
||||
<router-link slot="resend_confirmation_email" :to="{ name: RouteName.RESEND_CONFIRMATION }">{{ $t('resend confirmation email') }}</router-link>
|
||||
<router-link
|
||||
slot="resend_confirmation_email"
|
||||
:to="{ name: RouteName.RESEND_CONFIRMATION }"
|
||||
>{{ $t("resend confirmation email") }}</router-link
|
||||
>
|
||||
</i18n>
|
||||
</span>
|
||||
<span v-if="error === LoginError.USER_EMAIL_PASSWORD_INVALID">
|
||||
{{ $t('Impossible to login, your email or password seems incorrect.') }}
|
||||
</span>
|
||||
<!-- TODO: Shouldn't we hide this information ? -->
|
||||
<span v-if="error === LoginError.USER_DOES_NOT_EXIST">
|
||||
{{ $t('No user account with this email was found. Maybe you made a typo?') }}
|
||||
</span>
|
||||
<span v-if="error === LoginError.USER_EMAIL_PASSWORD_INVALID">{{
|
||||
$t("Impossible to login, your email or password seems incorrect.")
|
||||
}}</span>
|
||||
<!-- TODO: Shouldn't we hide this information? -->
|
||||
<span v-if="error === LoginError.USER_DOES_NOT_EXIST">{{
|
||||
$t("No user account with this email was found. Maybe you made a typo?")
|
||||
}}</span>
|
||||
</b-message>
|
||||
<form @submit="loginAction">
|
||||
<b-field :label="$t('Email')">
|
||||
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Password')">
|
||||
<b-field :label="$t('Email')" label-for="email">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="email"
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
/>
|
||||
</b-field>
|
||||
|
||||
<b-field :label="$t('Password')" label-for="password">
|
||||
<b-input
|
||||
aria-required="true"
|
||||
id="password"
|
||||
required
|
||||
type="password"
|
||||
password-reveal
|
||||
v-model="credentials.password"
|
||||
@@ -39,25 +58,27 @@
|
||||
</b-field>
|
||||
|
||||
<p class="control has-text-centered">
|
||||
<button class="button is-primary is-large">
|
||||
{{ $t('Login') }}
|
||||
</button>
|
||||
<button class="button is-primary is-large">{{ $t("Login") }}</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{ name: RouteName.SEND_PASSWORD_RESET, params: { email: credentials.email }}"
|
||||
:to="{ name: RouteName.SEND_PASSWORD_RESET, params: { email: credentials.email } }"
|
||||
>{{ $t("Forgot your password ?") }}</router-link
|
||||
>
|
||||
{{ $t('Forgot your password ?') }}
|
||||
</router-link>
|
||||
</p>
|
||||
<p class="control" v-if="config && config.registrationsOpen">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{ name: RouteName.REGISTER, params: { default_email: credentials.email, default_password: credentials.password }}"
|
||||
:to="{
|
||||
name: RouteName.REGISTER,
|
||||
params: {
|
||||
default_email: credentials.email,
|
||||
default_password: credentials.password,
|
||||
},
|
||||
}"
|
||||
>{{ $t("Register") }}</router-link
|
||||
>
|
||||
{{ $t('Register') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
@@ -66,18 +87,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { LOGIN } from '@/graphql/auth';
|
||||
import { validateEmailField, validateRequiredField } from '@/utils/validators';
|
||||
import { initializeCurrentActor, NoIdentitiesException, saveUserData } from '@/utils/auth';
|
||||
import { ILogin } from '@/types/login.model';
|
||||
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { onLogin } from '@/vue-apollo';
|
||||
import { RouteName } from '@/router';
|
||||
import { LoginErrorCode, LoginError } from '@/types/login-error-code.model';
|
||||
import { ICurrentUser } from '@/types/current-user.model';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { LOGIN } from "../../graphql/auth";
|
||||
import { validateEmailField, validateRequiredField } from "../../utils/validators";
|
||||
import { initializeCurrentActor, NoIdentitiesException, saveUserData } from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { LoginErrorCode, LoginError } from "../../types/login-error-code.model";
|
||||
import { ICurrentUser } from "../../types/current-user.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
|
||||
@Component({
|
||||
apollo: {
|
||||
@@ -91,31 +111,36 @@ import { IConfig } from '@/types/config.model';
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t('Login on Mobilizon!') as string,
|
||||
title: this.$t("Login on Mobilizon!") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
})
|
||||
export default class Login extends Vue {
|
||||
@Prop({ type: String, required: false, default: '' }) email!: string;
|
||||
@Prop({ type: String, required: false, default: '' }) password!: string;
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
|
||||
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||
|
||||
LoginErrorCode = LoginErrorCode;
|
||||
|
||||
LoginError = LoginError;
|
||||
|
||||
errorCode: LoginErrorCode | null = null;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
currentUser!: ICurrentUser;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
credentials = {
|
||||
email: '',
|
||||
password: '',
|
||||
email: "",
|
||||
password: "",
|
||||
};
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
rules = {
|
||||
required: validateRequiredField,
|
||||
email: validateEmailField,
|
||||
@@ -127,9 +152,9 @@ export default class Login extends Vue {
|
||||
this.credentials.email = this.email;
|
||||
this.credentials.password = this.password;
|
||||
|
||||
const query = this.$route.query;
|
||||
this.errorCode = query['code'] as LoginErrorCode;
|
||||
this.redirect = query['redirect'] as string;
|
||||
const { query } = this.$route;
|
||||
this.errorCode = query.code as LoginErrorCode;
|
||||
this.redirect = query.redirect as string;
|
||||
}
|
||||
|
||||
async loginAction(e: Event) {
|
||||
@@ -146,7 +171,7 @@ export default class Login extends Vue {
|
||||
},
|
||||
});
|
||||
if (data == null) {
|
||||
throw new Error('Data is undefined');
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
saveUserData(data.login);
|
||||
@@ -162,35 +187,36 @@ export default class Login extends Vue {
|
||||
});
|
||||
try {
|
||||
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
||||
} catch (e) {
|
||||
if (e instanceof NoIdentitiesException) {
|
||||
return await this.$router.push({
|
||||
} catch (err) {
|
||||
if (err instanceof NoIdentitiesException) {
|
||||
return this.$router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: { email: this.currentUser.email, userAlreadyActivated: 'true' },
|
||||
params: {
|
||||
email: this.currentUser.email,
|
||||
userAlreadyActivated: "true",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onLogin(this.$apollo);
|
||||
|
||||
if (this.redirect) {
|
||||
await this.$router.push(this.redirect);
|
||||
} else {
|
||||
window.localStorage.setItem('welcome-back', 'yes');
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
return this.$router.push(this.redirect);
|
||||
}
|
||||
window.localStorage.setItem("welcome-back", "yes");
|
||||
return this.$router.push({ name: RouteName.HOME });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
err.graphQLErrors.forEach(({ message }) => {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
this.errors.push(message);
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<section class="section container columns is-mobile is-centered">
|
||||
<div class="card column is-half-desktop">
|
||||
<h1>
|
||||
{{ $t('Password reset') }}
|
||||
</h1>
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
|
||||
<h1>{{ $t("Password reset") }}</h1>
|
||||
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{
|
||||
error
|
||||
}}</b-message>
|
||||
<form @submit="resetAction">
|
||||
<b-field :label="$t('Password')">
|
||||
<b-input
|
||||
@@ -23,50 +23,50 @@
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
v-model="credentials.password_confirmation"
|
||||
v-model="credentials.passwordConfirmation"
|
||||
/>
|
||||
</b-field>
|
||||
<button class="button is-primary">
|
||||
{{ $t('Reset my password') }}
|
||||
</button>
|
||||
<button class="button is-primary">{{ $t("Reset my password") }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { validateRequiredField } from '@/utils/validators';
|
||||
import { RESET_PASSWORD } from '@/graphql/auth';
|
||||
import { saveUserData } from '@/utils/auth';
|
||||
import { ILogin } from '@/types/login.model';
|
||||
import { RouteName } from '@/router';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { validateRequiredField } from "../../utils/validators";
|
||||
import { RESET_PASSWORD } from "../../graphql/auth";
|
||||
import { saveUserData } from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
import RouteName from "../../router/name";
|
||||
|
||||
@Component
|
||||
export default class PasswordReset extends Vue {
|
||||
@Prop({ type: String, required: true }) token!: string;
|
||||
|
||||
credentials = {
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
} as { password: string; password_confirmation: string };
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
} as { password: string; passwordConfirmation: string };
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
rules = {
|
||||
password_length: (value: string) =>
|
||||
value.length > 6 || 'Password must be at least 6 characters long',
|
||||
passwordLength: (value: string) =>
|
||||
value.length > 6 || "Password must be at least 6 characters long",
|
||||
required: validateRequiredField,
|
||||
password_equal: (value: string) =>
|
||||
value === this.credentials.password || 'Passwords must be the same',
|
||||
passwordEqual: (value: string) =>
|
||||
value === this.credentials.password || "Passwords must be the same",
|
||||
};
|
||||
|
||||
get samePasswords() {
|
||||
return (
|
||||
this.rules.password_length(this.credentials.password) === true &&
|
||||
this.credentials.password === this.credentials.password_confirmation
|
||||
this.rules.passwordLength(this.credentials.password) === true &&
|
||||
this.credentials.password === this.credentials.passwordConfirmation
|
||||
);
|
||||
}
|
||||
|
||||
async resetAction(e) {
|
||||
async resetAction(e: Event) {
|
||||
e.preventDefault();
|
||||
this.errors.splice(0);
|
||||
|
||||
@@ -79,14 +79,14 @@ export default class PasswordReset extends Vue {
|
||||
},
|
||||
});
|
||||
if (data == null) {
|
||||
throw new Error('Data is undefined');
|
||||
throw new Error("Data is undefined");
|
||||
}
|
||||
|
||||
saveUserData(data.resetPassword);
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
err.graphQLErrors.forEach(({ message }) => {
|
||||
err.graphQLErrors.forEach(({ message }: { message: any }) => {
|
||||
this.errors.push(message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,54 +2,52 @@
|
||||
<div class="section container">
|
||||
<section class="hero">
|
||||
<div class="hero-body">
|
||||
<h1 class="title">
|
||||
{{ $t('Register an account on Mobilizon!') }}
|
||||
</h1>
|
||||
<h1 class="title">{{ $t("Register an account on Mobilizon!") }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="columns">
|
||||
<div class="column">
|
||||
<div>
|
||||
<subtitle>{{ $t('Features') }}</subtitle>
|
||||
<subtitle>{{ $t("Features") }}</subtitle>
|
||||
<div class="content">
|
||||
<ul>
|
||||
<li>{{ $t('Create and manage several identities from the same account') }}</li>
|
||||
<li>{{ $t('Create, edit or delete events') }}</li>
|
||||
<li>{{ $t('Register for an event by choosing one of your identities') }}</li>
|
||||
<li>{{ $t("Create and manage several identities from the same account") }}</li>
|
||||
<li>{{ $t("Create, edit or delete events") }}</li>
|
||||
<li>{{ $t("Register for an event by choosing one of your identities") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<router-link :to="{ name: RouteName.ABOUT }">
|
||||
{{ $t('Learn more') }}
|
||||
</router-link>
|
||||
<hr>
|
||||
<router-link :to="{ name: RouteName.ABOUT }">{{ $t("Learn more") }}</router-link>
|
||||
<hr />
|
||||
<div class="content">
|
||||
<subtitle>{{ $t('About this instance') }}</subtitle>
|
||||
<subtitle>{{ $t("About this instance") }}</subtitle>
|
||||
<div class="content">
|
||||
<p>
|
||||
{{ $t("Your local administrator resumed its policy:") }}
|
||||
</p>
|
||||
<p>{{ $t("Your local administrator resumed its policy:") }}</p>
|
||||
<ul>
|
||||
<li>{{ $t('Enjoy discovering Mobilizon!') }}</li>
|
||||
<li>{{ $t("Enjoy discovering Mobilizon!") }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<!-- <p>-->
|
||||
<!-- {{ $t('Please read the full rules') }}-->
|
||||
<!-- </p>-->
|
||||
<!-- <p>-->
|
||||
<!-- {{ $t('Please read the full rules') }}-->
|
||||
<!-- </p>-->
|
||||
</div>
|
||||
</div>
|
||||
<div class="column">
|
||||
<b-message type="is-warning" v-if="config.registrationsWhitelist">{{ $t('Registrations are restricted by whitelisting.') }}</b-message>
|
||||
<b-message type="is-warning" v-if="config.registrationsWhitelist">
|
||||
{{ $t("Registrations are restricted by whitelisting.") }}
|
||||
</b-message>
|
||||
<form v-on:submit.prevent="submit()">
|
||||
<b-field
|
||||
:label="$t('Email')"
|
||||
:type="errors.email ? 'is-danger' : null"
|
||||
:message="errors.email"
|
||||
label-for="email"
|
||||
>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="email"
|
||||
type="email"
|
||||
v-model="credentials.email"
|
||||
@blur="showGravatar = true"
|
||||
@@ -61,10 +59,12 @@
|
||||
:label="$t('Password')"
|
||||
:type="errors.password ? 'is-danger' : null"
|
||||
:message="errors.password"
|
||||
label-for="password"
|
||||
>
|
||||
<b-input
|
||||
aria-required="true"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
password-reveal
|
||||
minlength="6"
|
||||
@@ -73,26 +73,25 @@
|
||||
</b-field>
|
||||
|
||||
<p class="control has-text-centered">
|
||||
<button class="button is-primary is-large">
|
||||
{{ $t('Register') }}
|
||||
</button>
|
||||
<button class="button is-primary is-large">{{ $t("Register") }}</button>
|
||||
</p>
|
||||
<p class="control">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{ name: RouteName.RESEND_CONFIRMATION, params: { email: credentials.email }}"
|
||||
:to="{ name: RouteName.RESEND_CONFIRMATION, params: { email: credentials.email } }"
|
||||
>{{ $t("Didn't receive the instructions ?") }}</router-link
|
||||
>
|
||||
{{ $t("Didn't receive the instructions ?") }}
|
||||
</router-link>
|
||||
</p>
|
||||
<p class="control">
|
||||
<router-link
|
||||
class="button is-text"
|
||||
:to="{ name: RouteName.LOGIN, params: { email: credentials.email, password: credentials.password }}"
|
||||
:to="{
|
||||
name: RouteName.LOGIN,
|
||||
params: { email: credentials.email, password: credentials.password },
|
||||
}"
|
||||
:disabled="sendingValidation"
|
||||
>{{ $t("Login") }}</router-link
|
||||
>
|
||||
{{ $t('Login') }}
|
||||
</router-link>
|
||||
</p>
|
||||
</form>
|
||||
|
||||
@@ -106,21 +105,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { CREATE_USER } from '@/graphql/user';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { RouteName } from '@/router';
|
||||
import { IConfig } from '@/types/config.model';
|
||||
import { CONFIG } from '@/graphql/config';
|
||||
import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { CREATE_USER } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { IConfig } from "../../types/config.model";
|
||||
import { CONFIG } from "../../graphql/config";
|
||||
import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||
|
||||
@Component({
|
||||
components: { Subtitle },
|
||||
metaInfo() {
|
||||
return {
|
||||
// if no subcomponents specify a metaInfo.title, this title will be used
|
||||
title: this.$t('Register an account on Mobilizon!') as string,
|
||||
title: this.$t("Register an account on Mobilizon!") as string,
|
||||
// all titles will be injected into this template
|
||||
titleTemplate: '%s | Mobilizon',
|
||||
titleTemplate: "%s | Mobilizon",
|
||||
};
|
||||
},
|
||||
apollo: {
|
||||
@@ -128,18 +127,24 @@ import Subtitle from '@/components/Utils/Subtitle.vue';
|
||||
},
|
||||
})
|
||||
export default class Register extends Vue {
|
||||
@Prop({ type: String, required: false, default: '' }) email!: string;
|
||||
@Prop({ type: String, required: false, default: '' }) password!: string;
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
|
||||
@Prop({ type: String, required: false, default: "" }) password!: string;
|
||||
|
||||
credentials = {
|
||||
email: this.email,
|
||||
password: this.password,
|
||||
locale: 'en',
|
||||
locale: "en",
|
||||
};
|
||||
|
||||
errors: object = {};
|
||||
sendingValidation: boolean = false;
|
||||
validationSent: boolean = false;
|
||||
|
||||
sendingValidation = false;
|
||||
|
||||
validationSent = false;
|
||||
|
||||
RouteName = RouteName;
|
||||
|
||||
config!: IConfig;
|
||||
|
||||
async submit() {
|
||||
@@ -161,17 +166,17 @@ export default class Register extends Vue {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.errors = error.graphQLErrors.reduce((acc, error) => {
|
||||
acc[error.details] = error.message;
|
||||
this.errors = error.graphQLErrors.reduce((acc: { [key: string]: any }, localError: any) => {
|
||||
acc[localError.details] = localError.message;
|
||||
return acc;
|
||||
}, {});
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "../../variables";
|
||||
@import "../../variables";
|
||||
|
||||
.avatar-enter-active {
|
||||
transition: opacity 1s ease;
|
||||
@@ -186,15 +191,15 @@ export default class Register extends Vue {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
|
||||
h2.title {
|
||||
color: $primary;
|
||||
font-size: 2.5rem;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: $secondary;
|
||||
display: inline;
|
||||
}
|
||||
h2.title {
|
||||
color: $primary;
|
||||
font-size: 2.5rem;
|
||||
text-decoration: underline;
|
||||
text-decoration-color: $secondary;
|
||||
display: inline;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,21 +3,26 @@
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t('Resend confirmation email') }}
|
||||
{{ $t("Resend confirmation email") }}
|
||||
</h1>
|
||||
<form v-if="!validationSent" @submit="resendConfirmationAction">
|
||||
<b-field label="Email">
|
||||
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
|
||||
<b-input aria-required="true" required type="email" v-model="credentials.email" />
|
||||
</b-field>
|
||||
<p class="control has-text-centered">
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
{{ $t('Send me the confirmation email once again') }}
|
||||
{{ $t("Send me the confirmation email once again") }}
|
||||
</b-button>
|
||||
</p>
|
||||
</form>
|
||||
<div v-else>
|
||||
<b-message type="is-success" :closable="false" title="Success">
|
||||
{{ $t('If an account with this email exists, we just sent another confirmation email to {email}', {email: credentials.email}) }}
|
||||
{{
|
||||
$t(
|
||||
"If an account with this email exists, we just sent another confirmation email to {email}",
|
||||
{ email: credentials.email }
|
||||
)
|
||||
}}
|
||||
</b-message>
|
||||
<b-message type="is-info">
|
||||
{{ $t("Please check your spam folder if you didn't receive the email.") }}
|
||||
@@ -29,25 +34,29 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { validateEmailField, validateRequiredField } from '@/utils/validators';
|
||||
import { RESEND_CONFIRMATION_EMAIL } from '@/graphql/auth';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { validateEmailField, validateRequiredField } from "../../utils/validators";
|
||||
import { RESEND_CONFIRMATION_EMAIL } from "../../graphql/auth";
|
||||
|
||||
@Component
|
||||
export default class ResendConfirmation extends Vue {
|
||||
@Prop({ type: String, required: false, default: '' }) email!: string;
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
|
||||
credentials = {
|
||||
email: '',
|
||||
email: "",
|
||||
};
|
||||
|
||||
validationSent = false;
|
||||
|
||||
error = false;
|
||||
|
||||
state = {
|
||||
email: {
|
||||
status: null,
|
||||
msg: '',
|
||||
msg: "",
|
||||
},
|
||||
};
|
||||
|
||||
rules = {
|
||||
required: validateRequiredField,
|
||||
email: validateEmailField,
|
||||
@@ -57,7 +66,7 @@ export default class ResendConfirmation extends Vue {
|
||||
this.credentials.email = this.email;
|
||||
}
|
||||
|
||||
async resendConfirmationAction(e) {
|
||||
async resendConfirmationAction(e: Event) {
|
||||
e.preventDefault();
|
||||
this.error = false;
|
||||
|
||||
@@ -79,7 +88,7 @@ export default class ResendConfirmation extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,25 +3,30 @@
|
||||
<div class="columns is-mobile is-centered">
|
||||
<div class="column is-half-desktop">
|
||||
<h1 class="title">
|
||||
{{ $t('Password reset') }}
|
||||
{{ $t("Password reset") }}
|
||||
</h1>
|
||||
<b-message
|
||||
title="Error" type="is-danger" v-for="error in errors" :key="error" @close="removeError(error)">
|
||||
<b-message
|
||||
title="Error"
|
||||
type="is-danger"
|
||||
v-for="error in errors"
|
||||
:key="error"
|
||||
@close="removeError(error)"
|
||||
>
|
||||
{{ error }}
|
||||
</b-message>
|
||||
<form @submit="sendResetPasswordTokenAction" v-if="!validationSent">
|
||||
<b-field label="Email">
|
||||
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
|
||||
<b-input aria-required="true" required type="email" v-model="credentials.email" />
|
||||
</b-field>
|
||||
<p class="control has-text-centered">
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
{{ $t('Send me an email to reset my password') }}
|
||||
<b-button type="is-primary" native-type="submit">
|
||||
{{ $t("Send me an email to reset my password") }}
|
||||
</b-button>
|
||||
</p>
|
||||
</form>
|
||||
<div v-else>
|
||||
<b-message type="is-success" :closable="false" title="Success">
|
||||
{{ $t('We just sent an email to {email}', {email: credentials.email}) }}
|
||||
{{ $t("We just sent an email to {email}", { email: credentials.email }) }}
|
||||
</b-message>
|
||||
<b-message type="is-info">
|
||||
{{ $t("Please check your spam folder if you didn't receive the email.") }}
|
||||
@@ -33,23 +38,26 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { validateEmailField, validateRequiredField } from '@/utils/validators';
|
||||
import { SEND_RESET_PASSWORD } from '@/graphql/auth';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { validateEmailField, validateRequiredField } from "../../utils/validators";
|
||||
import { SEND_RESET_PASSWORD } from "../../graphql/auth";
|
||||
|
||||
@Component
|
||||
export default class SendPasswordReset extends Vue {
|
||||
@Prop({ type: String, required: false, default: '' }) email!: string;
|
||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||
|
||||
credentials = {
|
||||
email: '',
|
||||
email: "",
|
||||
} as { email: string };
|
||||
validationSent: boolean = false;
|
||||
|
||||
validationSent = false;
|
||||
|
||||
errors: string[] = [];
|
||||
|
||||
state = {
|
||||
email: {
|
||||
status: null,
|
||||
msg: '',
|
||||
msg: "",
|
||||
} as { status: boolean | null; msg: string },
|
||||
};
|
||||
|
||||
@@ -66,7 +74,7 @@ export default class SendPasswordReset extends Vue {
|
||||
this.errors.splice(this.errors.indexOf(message));
|
||||
}
|
||||
|
||||
async sendResetPasswordTokenAction(e) {
|
||||
async sendResetPasswordTokenAction(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
@@ -80,7 +88,7 @@ export default class SendPasswordReset extends Vue {
|
||||
this.validationSent = true;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
err.graphQLErrors.forEach(({ message }) => {
|
||||
err.graphQLErrors.forEach(({ message }: { message: string }) => {
|
||||
if (this.errors.indexOf(message) < 0) {
|
||||
this.errors.push(message);
|
||||
}
|
||||
@@ -92,7 +100,7 @@ export default class SendPasswordReset extends Vue {
|
||||
this.state = {
|
||||
email: {
|
||||
status: null,
|
||||
msg: '',
|
||||
msg: "",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -100,7 +108,7 @@ export default class SendPasswordReset extends Vue {
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
.container .columns {
|
||||
margin: 1rem auto 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,35 +1,33 @@
|
||||
<template>
|
||||
<section class="section container">
|
||||
<h1 class="title" v-if="loading">
|
||||
{{ $t('Your account is being validated') }}
|
||||
</h1>
|
||||
<h1 class="title" v-if="loading">{{ $t("Your account is being validated") }}</h1>
|
||||
<div v-else>
|
||||
<div v-if="failed">
|
||||
<b-message :title="$t('Error while validating account')" type="is-danger">
|
||||
{{ $t('Either the account is already validated, either the validation token is incorrect.') }}
|
||||
{{
|
||||
$t("Either the account is already validated, either the validation token is incorrect.")
|
||||
}}
|
||||
</b-message>
|
||||
</div>
|
||||
<h1 class="title" v-else>
|
||||
{{ $t('Your account has been validated') }}
|
||||
</h1>
|
||||
<h1 class="title" v-else>{{ $t("Your account has been validated") }}</h1>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { VALIDATE_USER, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { AUTH_USER_ID } from '@/constants';
|
||||
import { RouteName } from '@/router';
|
||||
import { saveUserData, changeIdentity } from '@/utils/auth';
|
||||
import { ILogin } from '@/types/login.model';
|
||||
import { ICurrentUserRole } from '@/types/current-user.model';
|
||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||
import { VALIDATE_USER, UPDATE_CURRENT_USER_CLIENT } from "../../graphql/user";
|
||||
import RouteName from "../../router/name";
|
||||
import { saveUserData, changeIdentity } from "../../utils/auth";
|
||||
import { ILogin } from "../../types/login.model";
|
||||
import { ICurrentUserRole } from "../../types/current-user.model";
|
||||
|
||||
@Component
|
||||
export default class Validate extends Vue {
|
||||
@Prop({ type: String, required: true }) token!: string;
|
||||
|
||||
loading = true;
|
||||
|
||||
failed = false;
|
||||
|
||||
async created() {
|
||||
@@ -48,7 +46,7 @@ export default class Validate extends Vue {
|
||||
if (data) {
|
||||
saveUserData(data.validateUser);
|
||||
|
||||
const user = data.validateUser.user;
|
||||
const { user } = data.validateUser;
|
||||
|
||||
await this.$apollo.mutate({
|
||||
mutation: UPDATE_CURRENT_USER_CLIENT,
|
||||
@@ -63,10 +61,11 @@ export default class Validate extends Vue {
|
||||
if (user.defaultActor) {
|
||||
await changeIdentity(this.$apollo.provider.defaultClient, user.defaultActor);
|
||||
await this.$router.push({ name: RouteName.HOME });
|
||||
} else { // If the user didn't register any profile yet, let's create one for them
|
||||
} else {
|
||||
// If the user didn't register any profile yet, let's create one for them
|
||||
await this.$router.push({
|
||||
name: RouteName.REGISTER_PROFILE,
|
||||
params: { email: user.email, userAlreadyActivated: 'true' },
|
||||
params: { email: user.email, userAlreadyActivated: "true" },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user