Merge branch 'feature/my-account' into 'master'

Test implementation of my account page

See merge request framasoft/mobilizon!127
This commit is contained in:
Thomas Citharel 2019-04-30 14:32:29 +02:00
commit 70b93db8fa
27 changed files with 1200 additions and 296 deletions

View File

@ -3,4 +3,6 @@ projects:
schemaPath: schema.graphql schemaPath: schema.graphql
extensions: extensions:
endpoints: endpoints:
dev: 'http://localhost:4000/api' dev:
url: 'http://localhost:4001/api'
introspect: true

View File

@ -79,6 +79,7 @@ export default class App extends Vue {
@import "~bulma/sass/elements/tag.sass"; @import "~bulma/sass/elements/tag.sass";
@import "~bulma/sass/components/navbar.sass"; @import "~bulma/sass/components/navbar.sass";
@import "~bulma/sass/components/modal.sass"; @import "~bulma/sass/components/modal.sass";
@import "~bulma/sass/components/media.sass";
@import "~bulma/sass/grid/_all.sass"; @import "~bulma/sass/grid/_all.sass";
@import "~bulma/sass/layout/section.sass"; @import "~bulma/sass/layout/section.sass";
@import "~bulma/sass/layout/footer.sass"; @import "~bulma/sass/layout/footer.sass";

View File

@ -0,0 +1,44 @@
<template>
<!-- TODO -->
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { CREATE_PERSON, LOGGED_PERSON } from '../../graphql/actor';
import { IPerson } from '@/types/actor';
@Component({
apollo: {
loggedPerson: {
query: LOGGED_PERSON,
},
},
})
export default class Identities extends Vue {
loggedPerson!: IPerson;
errors: string[] = [];
newPerson!: IPerson;
async createProfile(e) {
e.preventDefault();
try {
await this.$apollo.mutate({
mutation: CREATE_PERSON,
variables: this.newPerson,
});
this.$apollo.queries.identities.refresh();
} catch (err) {
console.error(err);
err.graphQLErrors.forEach(({ message }) => {
this.errors.push(message);
});
}
}
host() {
return `@${window.location.host}`;
}
}
</script>

View File

@ -0,0 +1,82 @@
<template>
<section>
<h1 class="title">
<translate>My identities</translate>
</h1>
<ul class="identities">
<li v-for="identity in identities" :key="identity.id">
<div class="media identity" v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }">
<div class="media-left">
<figure class="image is-48x48">
<img class="is-rounded" :src="identity.avatarUrl">
</figure>
</div>
<div class="media-content">
{{ identity.displayName() }}
</div>
</div>
</li>
</ul>
<a class="button create-identity is-primary">
<translate>Create a new identity</translate>
</a>
</section>
</template>
<style lang="scss" scoped>
.identities {
border-right: 1px solid grey;
padding: 15px 0;
}
.media.identity {
align-items: center;
font-size: 1.3rem;
padding-bottom: 0;
margin-bottom: 15px;
&.is-current-identity {
background-color: rgba(0, 0, 0, 0.1);
}
}
.title {
margin-bottom: 30px;
}
</style>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { IDENTITIES, LOGGED_PERSON } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor';
@Component({
apollo: {
loggedPerson: {
query: LOGGED_PERSON,
},
},
})
export default class Identities extends Vue {
identities: Person[] = [];
loggedPerson!: IPerson;
errors: string[] = [];
async mounted() {
const result = await this.$apollo.query({
query: IDENTITIES,
});
this.identities = result.data.identities
.map(i => new Person(i));
}
isCurrentIdentity(identity: IPerson) {
return identity.preferredUsername === this.loggedPerson.preferredUsername;
}
}
</script>

View File

@ -47,7 +47,7 @@
import { IEvent, ParticipantRole } from '@/types/event.model'; import { IEvent, ParticipantRole } from '@/types/event.model';
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue'; import DateCalendarIcon from '@/components/Event/DateCalendarIcon.vue';
import { IActor, IPerson, Person } from '@/types/actor.model'; import { IActor, IPerson, Person } from '@/types/actor';
const lineClamp = require('line-clamp'); const lineClamp = require('line-clamp');
export interface IEventCardOptions { export interface IEventCardOptions {

View File

@ -20,7 +20,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { Group } from '@/types/actor.model'; import { Group } from '@/types/actor';
import { RouteName } from '@/router'; import { RouteName } from '@/router';
@Component @Component

View File

@ -36,9 +36,9 @@
</div> </div>
<div class="navbar-item has-dropdown is-hoverable" v-else> <div class="navbar-item has-dropdown is-hoverable" v-else>
<router-link <router-link
class="navbar-link" class="navbar-link"
v-if="currentUser.isLoggedIn && loggedPerson" v-if="currentUser.isLoggedIn && loggedPerson"
:to="{ name: 'Profile', params: { name: loggedPerson.preferredUsername} }" :to="{ name: 'MyAccount' }"
> >
<figure class="image is-24x24"> <figure class="image is-24x24">
<img :src="loggedPerson.avatarUrl"> <img :src="loggedPerson.avatarUrl">
@ -47,8 +47,13 @@
</router-link> </router-link>
<div class="navbar-dropdown"> <div class="navbar-dropdown">
<a class="navbar-item"><translate>My account</translate></a> <router-link :to="{ name: 'MyAccount' }" class="navbar-item">
<a class="navbar-item" v-on:click="logout()"><translate>Log out</translate></a> <translate>My account</translate>
</router-link>
<a class="navbar-item" v-on:click="logout()">
<translate>Log out</translate>
</a>
</div> </div>
</div> </div>
</div> </div>
@ -63,7 +68,7 @@ import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'
import { onLogout } from '@/vue-apollo'; import { onLogout } from '@/vue-apollo';
import { deleteUserData } from '@/utils/auth'; import { deleteUserData } from '@/utils/auth';
import { LOGGED_PERSON } from '@/graphql/actor'; import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson } from '@/types/actor.model'; import { IPerson } from '@/types/actor';
import { CONFIG } from '@/graphql/config'; import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model'; import { IConfig } from '@/types/config.model';
import { ICurrentUser } from '@/types/current-user.model'; import { ICurrentUser } from '@/types/current-user.model';

View File

@ -1,25 +1,19 @@
import Profile from '@/views/Account/Profile.vue'; import Profile from '@/views/Account/Profile.vue';
import MyAccount from '@/views/Account/MyAccount.vue';
import CreateGroup from '@/views/Group/Create.vue'; import CreateGroup from '@/views/Group/Create.vue';
import Group from '@/views/Group/Group.vue'; import Group from '@/views/Group/Group.vue';
import GroupList from '@/views/Group/GroupList.vue'; import GroupList from '@/views/Group/GroupList.vue';
import Identities from '@/views/Account/Identities.vue';
import { RouteConfig } from 'vue-router'; import { RouteConfig } from 'vue-router';
export enum ActorRouteName { export enum ActorRouteName {
IDENTITIES = 'Identities',
GROUP_LIST = 'GroupList', GROUP_LIST = 'GroupList',
GROUP = 'Group', GROUP = 'Group',
CREATE_GROUP = 'CreateGroup', CREATE_GROUP = 'CreateGroup',
PROFILE = 'Profile', PROFILE = 'Profile',
MY_ACCOUNT = 'MyAccount',
} }
export const actorRoutes: RouteConfig[] = [ export const actorRoutes: RouteConfig[] = [
{
path: '/identities',
name: ActorRouteName.IDENTITIES,
component: Identities,
meta: { requiredAuth: true },
},
{ {
path: '/groups', path: '/groups',
name: ActorRouteName.GROUP_LIST, name: ActorRouteName.GROUP_LIST,
@ -46,4 +40,11 @@ export const actorRoutes: RouteConfig[] = [
props: true, props: true,
meta: { requiredAuth: false }, meta: { requiredAuth: false },
}, },
{
path: '/my-account',
name: ActorRouteName.MY_ACCOUNT,
component: MyAccount,
props: true,
meta: { requiredAuth: true },
},
]; ];

View File

@ -1,75 +0,0 @@
import { ICurrentUser } from '@/types/current-user.model';
import { IEvent } from '@/types/event.model';
export interface IActor {
id?: string;
url: string;
name: string;
domain: string|null;
summary: string;
preferredUsername: string;
suspended: boolean;
avatarUrl: string;
bannerUrl: string;
}
export class Actor implements IActor {
avatarUrl: string = '';
bannerUrl: string = '';
domain: string | null = null;
name: string = '';
preferredUsername: string = '';
summary: string = '';
suspended: boolean = false;
url: string = '';
get displayNameAndUsername(): string {
return `${this.name} (${this.usernameWithDomain})`;
}
public usernameWithDomain(): string {
const domain = this.domain ? `@${this.domain}` : '';
return `@${this.preferredUsername}${domain}`;
}
public displayName(): string {
return this.name != null && this.name !== '' ? this.name : this.usernameWithDomain();
}
}
export interface IPerson extends IActor {
feedTokens: IFeedToken[];
goingToEvents: IEvent[];
}
export interface IGroup extends IActor {
members: IMember[];
}
export class Person extends Actor implements IPerson {
feedTokens: IFeedToken[] = [];
goingToEvents: IEvent[] = [];
}
export class Group extends Actor implements IGroup {
members: IMember[] = [];
}
export interface IFeedToken {
token: string;
actor?: IPerson;
user: ICurrentUser;
}
export enum MemberRole {
PENDING,
MEMBER,
MODERATOR,
ADMIN,
}
export interface IMember {
role: MemberRole;
parent: IGroup;
actor: IActor;
}

View File

@ -0,0 +1,39 @@
export interface IActor {
id?: string;
url: string;
name: string;
domain: string|null;
summary: string;
preferredUsername: string;
suspended: boolean;
avatarUrl: string;
bannerUrl: string;
}
export class Actor implements IActor {
avatarUrl: string = '';
bannerUrl: string = '';
domain: string | null = null;
name: string = '';
preferredUsername: string = '';
summary: string = '';
suspended: boolean = false;
url: string = '';
constructor (hash: IActor | {} = {}) {
Object.assign(this, hash);
}
get displayNameAndUsername(): string {
return `${this.name} (${this.usernameWithDomain})`;
}
usernameWithDomain(): string {
const domain = this.domain ? `@${this.domain}` : '';
return `@${this.preferredUsername}${domain}`;
}
displayName(): string {
return this.name != null && this.name !== '' ? this.name : this.usernameWithDomain();
}
}

View File

@ -0,0 +1,22 @@
import { Actor, IActor } from '@/types/actor/actor.model';
export enum MemberRole {
PENDING,
MEMBER,
MODERATOR,
ADMIN,
}
export interface IGroup extends IActor {
members: IMember[];
}
export interface IMember {
role: MemberRole;
parent: IGroup;
actor: IActor;
}
export class Group extends Actor implements IGroup {
members: IMember[] = [];
}

View File

@ -0,0 +1,3 @@
export * from './actor.model';
export * from './group.model';
export * from './person.model';

View File

@ -0,0 +1,25 @@
import { ICurrentUser } from '@/types/current-user.model';
import { IEvent } from '@/types/event.model';
import { Actor, IActor } from '@/types/actor/actor.model';
export interface IFeedToken {
token: string;
actor?: IPerson;
user: ICurrentUser;
}
export interface IPerson extends IActor {
feedTokens: IFeedToken[];
goingToEvents: IEvent[];
}
export class Person extends Actor implements IPerson {
feedTokens: IFeedToken[] = [];
goingToEvents: IEvent[] = [];
constructor(hash: IPerson | {} = {}) {
super(hash);
Object.assign(this, hash);
}
}

View File

@ -1,6 +1,5 @@
import { Actor, IActor } from './actor.model'; import { Actor, IActor } from './actor';
import { IAddress } from '@/types/address.model'; import { IAddress } from '@/types/address.model';
import { ITag } from '@/types/tag.model';
export enum EventStatus { export enum EventStatus {
TENTATIVE, TENTATIVE,

View File

@ -1,4 +1,4 @@
import { IGroup } from '@/types/actor.model'; import { IGroup } from '@/types/actor';
import { IEvent } from '@/types/event.model'; import { IEvent } from '@/types/event.model';
export interface SearchEvent { export interface SearchEvent {

3
js/src/utils/html.ts Normal file
View File

@ -0,0 +1,3 @@
export function nl2br(text: string) {
return text.replace(/(?:\r\n|\r|\n)/g, '<br>');
}

View File

@ -1,92 +0,0 @@
<template>
<section>
<b-loading :active.sync="$apollo.loading"></b-loading>
<h1 class="title">
<translate>Identities</translate>
</h1>
<a class="button is-primary" @click="showCreateProfileForm = true">
<translate>Add a new profile</translate>
</a>
<div class="columns" v-if="showCreateProfileForm">
<form @submit="createProfile" class="column is-half">
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<b-field label="Username">
<b-input aria-required="true" required v-model="newPerson.preferredUsername"/>
</b-field>
<button class="button is-primary">
<translate>Register</translate>
</button>
</form>
</div>
<ul>
<li v-for="identity in identities" :key="identity.id">
<hr>
<div class="media">
<div class="media-left">
<figure class="image is-48x48">
<img :src="identity.avatarUrl">
</figure>
</div>
<div class="media-content">
<p class="title is-5">
{{ identity.name }}
<span
v-if="identity.preferredUsername === loggedPerson.preferredUsername"
class="tag is-primary"
>
<translate>Current</translate>
</span>
</p>
<p class="subtitle is-6">@{{ identity.preferredUsername }}</p>
</div>
</div>
</li>
</ul>
</section>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { IDENTITIES, LOGGED_PERSON, CREATE_PERSON } from '../../graphql/actor';
import { IPerson } from '@/types/actor.model';
@Component({
apollo: {
identities: {
query: IDENTITIES,
},
loggedPerson: {
query: LOGGED_PERSON,
},
},
})
export default class Identities extends Vue {
identities: IPerson[] = [];
loggedPerson!: IPerson;
newPerson!: IPerson;
showCreateProfileForm: boolean = false;
errors: string[] = [];
async createProfile(e) {
e.preventDefault();
try {
await this.$apollo.mutate({
mutation: CREATE_PERSON,
variables: this.newPerson,
});
this.showCreateProfileForm = false;
this.$apollo.queries.identities.refresh();
} catch (err) {
console.error(err);
err.graphQLErrors.forEach(({ message }) => {
this.errors.push(message);
});
}
}
host() {
return `@${window.location.host}`;
}
}
</script>

View File

@ -0,0 +1,68 @@
<template>
<section class="container">
<div v-if="person">
<div class="header">
<figure v-if="person.bannerUrl" class="image is-3by1">
<img :src="person.bannerUrl" alt="banner">
</figure>
</div>
<div class="columns">
<div class="identities column is-4">
<identities></identities>
</div>
</div>
</div>
</section>
</template>
<style lang="scss">
.header {
padding-bottom: 30px;
}
.identities {
padding-right: 45px;
margin-right: 45px;
}
</style>
<script lang="ts">
import { LOGGED_PERSON } from '@/graphql/actor';
import { Component, Vue } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue';
import { IPerson } from '@/types/actor';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import Identities from '@/components/Account/Identities.vue';
@Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT,
},
loggedPerson: {
query: LOGGED_PERSON,
},
},
components: {
EventCard,
Identities,
},
})
export default class MyAccount extends Vue {
person: IPerson | null = null;
async mounted () {
const result = await this.$apollo.query({
query: LOGGED_PERSON,
});
this.person = result.data.loggedPerson;
}
}
</script>
<style lang="scss">
@import "../../variables";
@import "~bulma/sass/utilities/_all";
@import "~bulma/sass/components/dropdown.sass";
</style>

View File

@ -1,103 +1,102 @@
<template> <template>
<section class="container"> <section class="container">
<div v-if="person"> <div v-if="person">
<div class="card-image" v-if="person.bannerUrl"> <div class="card-image" v-if="person.bannerUrl">
<figure class="image"> <figure class="image">
<img :src="person.bannerUrl"> <img :src="person.bannerUrl">
</figure> </figure>
</div> </div>
<div class="card-content"> <div class="card-content">
<div class="media"> <div class="media">
<div class="media-left"> <div class="media-left">
<figure class="image is-48x48"> <figure class="image is-48x48">
<img :src="person.avatarUrl"> <img :src="person.avatarUrl">
</figure> </figure>
</div> </div>
<div class="media-content"> <div class="media-content">
<p class="title">{{ person.name }}</p> <p class="title">{{ person.name }}</p>
<p class="subtitle">@{{ person.preferredUsername }}</p> <p class="subtitle">@{{ person.preferredUsername }}</p>
</div> </div>
</div>
<div class="content">
<vue-simple-markdown :source="person.summary"></vue-simple-markdown>
</div>
<b-dropdown hoverable has-link aria-role="list">
<button class="button is-primary" slot="trigger">
<translate>Public feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', true)">
<translate>Public RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', true)">
<translate>Public iCal Feed</translate>
</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">
<translate>Private feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', false)">
<translate>RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', false)">
<translate>iCal Feed</translate>
</a>
</b-dropdown-item>
</b-dropdown>
<a class="button" v-else-if="loggedPerson" @click="createToken">
<translate>Create token</translate>
</a>
</div>
<section v-if="person.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
</h2>
<div class="columns">
<EventCard
v-for="event in person.organizedEvents"
:event="event"
:options="{ hideDetails: true, organizerActor: person }"
:key="event.uuid"
class="column is-one-third"
/>
</div>
<div class="field is-grouped">
<p class="control">
<a
class="button"
@click="deleteProfile()"
v-if="loggedPerson && loggedPerson.id === person.id"
>
<translate>Delete</translate>
</a>
</p>
</div>
</section>
</div> </div>
</section>
<div class="content">
<vue-simple-markdown :source="person.summary"></vue-simple-markdown>
</div>
<b-dropdown hoverable has-link aria-role="list">
<button class="button is-primary" slot="trigger">
<translate>Public feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', true)">
<translate>Public RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', true)">
<translate>Public iCal Feed</translate>
</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">
<translate>Private feeds</translate>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', false)">
<translate>RSS/Atom Feed</translate>
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', false)">
<translate>iCal Feed</translate>
</a>
</b-dropdown-item>
</b-dropdown>
<a class="button" v-else-if="loggedPerson" @click="createToken">
<translate>Create token</translate>
</a>
</div>
<section v-if="person.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
</h2>
<div class="columns">
<EventCard
v-for="event in person.organizedEvents"
:event="event"
:options="{ hideDetails: true, organizerActor: person }"
:key="event.uuid"
class="column is-one-third"
/>
</div>
<div class="field is-grouped">
<p class="control">
<a
class="button"
@click="deleteProfile()"
v-if="loggedPerson && loggedPerson.id === person.id"
>
<translate>Delete</translate>
</a>
</p>
</div>
</section>
</div>
</section>
</template> </template>
<script lang="ts"> <script lang="ts">
import { FETCH_PERSON, LOGGED_PERSON } from '@/graphql/actor'; import { FETCH_PERSON, LOGGED_PERSON } from '@/graphql/actor';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue'; import EventCard from '@/components/Event/EventCard.vue';
import { RouteName } from '@/router';
import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint'; import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint';
import { IPerson } from '@/types/actor.model'; import { IPerson } from '@/types/actor';
import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens'; import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens';
@Component({ @Component({
@ -118,19 +117,15 @@ import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens';
EventCard, EventCard,
}, },
}) })
export default class Profile extends Vue { export default class MyAccount extends Vue {
@Prop({ type: String, required: true }) name!: string; @Prop({ type: String, required: true }) name!: string;
person!: IPerson; person!: IPerson;
// call again the method if the route changes // call again the method if the route changes
@Watch('$route') @Watch('$route')
onRouteChange() { onRouteChange() {
// this.fetchData() // this.fetchData()
}
nl2br(text) {
return text.replace(/(?:\r\n|\r|\n)/g, '<br>');
} }
feedUrls(format, isPublic = true): string { feedUrls(format, isPublic = true): string {
@ -155,7 +150,7 @@ export default class Profile extends Vue {
} }
</script> </script>
<style lang="scss"> <style lang="scss">
@import "../../variables"; @import "../../variables";
@import "~bulma/sass/utilities/_all"; @import "~bulma/sass/utilities/_all";
@import "~bulma/sass/components/dropdown.sass"; @import "~bulma/sass/components/dropdown.sass";
</style> </style>

View File

@ -71,7 +71,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { IPerson } from '@/types/actor.model'; import { IPerson } from '@/types/actor';
import { REGISTER_PERSON } from '@/graphql/actor'; import { REGISTER_PERSON } from '@/graphql/actor';
import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint'; import { MOBILIZON_INSTANCE_HOST } from '@/api/_entrypoint';
import { RouteName } from '@/router'; import { RouteName } from '@/router';

View File

@ -40,7 +40,7 @@ import {
EventModel, EventModel,
} from '@/types/event.model'; } from '@/types/event.model';
import { LOGGED_PERSON } from '@/graphql/actor'; import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor.model'; import { IPerson, Person } from '@/types/actor';
@Component({ @Component({
apollo: { apollo: {

View File

@ -233,7 +233,7 @@ import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/ev
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
import { LOGGED_PERSON } from '@/graphql/actor'; import { LOGGED_PERSON } from '@/graphql/actor';
import { EventVisibility, IEvent, IParticipant } from '@/types/event.model'; import { EventVisibility, IEvent, IParticipant } from '@/types/event.model';
import { IPerson } from '@/types/actor.model'; import { IPerson } from '@/types/actor';
import { RouteName } from '@/router'; import { RouteName } from '@/router';
import 'vue-simple-markdown/dist/vue-simple-markdown.css'; import 'vue-simple-markdown/dist/vue-simple-markdown.css';
import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint'; import { GRAPHQL_API_ENDPOINT } from '@/api/_entrypoint';

View File

@ -59,7 +59,7 @@
import { Component, Prop, Vue, Watch } from 'vue-property-decorator'; import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue'; import EventCard from '@/components/Event/EventCard.vue';
import { FETCH_GROUP, LOGGED_PERSON } from '@/graphql/actor'; import { FETCH_GROUP, LOGGED_PERSON } from '@/graphql/actor';
import { IGroup } from '@/types/actor.model'; import { IGroup } from '@/types/actor';
@Component({ @Component({
apollo: { apollo: {

View File

@ -88,7 +88,7 @@ import { FETCH_EVENTS } from '@/graphql/event';
import { Component, Vue } from 'vue-property-decorator'; import { Component, Vue } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue'; import EventCard from '@/components/Event/EventCard.vue';
import { LOGGED_PERSON_WITH_GOING_TO_EVENTS } from '@/graphql/actor'; import { LOGGED_PERSON_WITH_GOING_TO_EVENTS } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor.model'; import { IPerson, Person } from '@/types/actor';
import { ICurrentUser } from '@/types/current-user.model'; import { ICurrentUser } from '@/types/current-user.model';
import { CURRENT_USER_CLIENT } from '@/graphql/user'; import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { RouteName } from '@/router'; import { RouteName } from '@/router';

View File

@ -48,7 +48,7 @@ import { SEARCH_EVENTS, SEARCH_GROUPS } from '@/graphql/search';
import { RouteName } from '@/router'; import { RouteName } from '@/router';
import EventCard from '@/components/Event/EventCard.vue'; import EventCard from '@/components/Event/EventCard.vue';
import GroupCard from '@/components/Group/GroupCard.vue'; import GroupCard from '@/components/Group/GroupCard.vue';
import { Group, IGroup } from '@/types/actor.model'; import { Group, IGroup } from '@/types/actor';
import { SearchEvent, SearchGroup } from '@/types/search.model'; import { SearchEvent, SearchGroup } from '@/types/search.model';
enum SearchTabs { enum SearchTabs {

View File

@ -3,6 +3,7 @@
"rules": { "rules": {
"max-line-length": [ true, 140 ], "max-line-length": [ true, 140 ],
"import-name": false, "import-name": false,
"ter-arrow-parens": false "ter-arrow-parens": false,
"no-boolean-literal-compare": false
} }
} }

781
schema.graphql Normal file
View File

@ -0,0 +1,781 @@
# source: http://localhost:4001/api
# timestamp: Fri Apr 26 2019 14:47:01 GMT+0200 (heure dété dEurope centrale)
schema {
query: RootQueryType
mutation: RootMutationType
}
"""An ActivityPub actor"""
interface Actor {
"""The actor's avatar url"""
avatarUrl: String
"""The actor's banner url"""
bannerUrl: String
"""The actor's domain if (null if it's this instance)"""
domain: String
"""List of followers"""
followers: [Follower]
"""Number of followers for this actor"""
followersCount: Int
"""List of followings"""
following: [Follower]
"""Number of actors following this actor"""
followingCount: Int
"""Internal ID for this actor"""
id: Int
"""The actors RSA Keys"""
keys: String
"""If the actor is from this instance"""
local: Boolean
"""Whether the actors manually approves followers"""
manuallyApprovesFollowers: Boolean
"""The actor's displayed name"""
name: String
"""A list of the events this actor has organized"""
organizedEvents: [Event]
"""The actor's preferred username"""
preferredUsername: String
"""The actor's summary"""
summary: String
"""If the actor is suspended"""
suspended: Boolean
"""The type of Actor (Person, Group,)"""
type: ActorType
"""The ActivityPub actor's URL"""
url: String
}
"""The list of types an actor can be"""
enum ActorType {
"""An ActivityPub Application"""
APPLICATION
"""An ActivityPub Group"""
GROUP
"""An ActivityPub Organization"""
ORGANIZATION
"""An ActivityPub Person"""
PERSON
"""An ActivityPub Service"""
SERVICE
}
type Address {
country: String
description: String
"""The floor this event is at"""
floor: String
"""The geocoordinates for the point where this address is"""
geom: Point
"""The address's locality"""
locality: String
postalCode: String
region: String
"""The address's street name (with number)"""
street: String
}
"""A comment"""
type Comment {
"""Internal ID for this comment"""
id: ID
local: Boolean
primaryLanguage: String
replies: [Comment]
text: String
threadLanguages: [String]!
url: String
uuid: UUID
visibility: CommentVisibility
}
"""The list of visibility options for a comment"""
enum CommentVisibility {
"""visible only to people invited"""
INVITE
"""Visible only after a moderator accepted"""
MODERATED
"""Visible only to people members of the group or followers of the person"""
PRIVATE
"""Publically listed and federated. Can be shared."""
PUBLIC
"""Visible only to people with the link - or invited"""
UNLISTED
}
"""A config object"""
type Config {
description: String
name: String
registrationsOpen: Boolean
}
"""
The `DateTime` scalar type represents a date and time in the UTC
timezone. The DateTime appears in a JSON response as an ISO8601 formatted
string, including UTC timezone ("Z"). The parsed date and time string will
be converted to UTC and any UTC offset other than 0 will be rejected.
"""
scalar DateTime
"""Represents a deleted feed_token"""
type DeletedFeedToken {
actor: DeletedObject
user: DeletedObject
}
"""Represents a deleted member"""
type DeletedMember {
actor: DeletedObject
parent: DeletedObject
}
"""A struct containing the id of the deleted object"""
type DeletedObject {
id: Int
}
"""Represents a deleted participant"""
type DeletedParticipant {
actor: DeletedObject
event: DeletedObject
}
"""An event"""
type Event {
"""Who the event is attributed to (often a group)"""
attributedTo: Actor
"""Datetime for when the event begins"""
beginsOn: DateTime
"""The event's category"""
category: String
"""When the event was created"""
createdAt: DateTime
"""The event's description"""
description: String
"""Datetime for when the event ends"""
endsOn: DateTime
"""Internal ID for this event"""
id: Int
"""A large picture for the event"""
largeImage: String
"""Whether the event is local or not"""
local: Boolean
"""Online address of the event"""
onlineAddress: OnlineAddress
"""The event's organizer (as a person)"""
organizerActor: Actor
"""The event's participants"""
participants: [Participant]
"""Phone address for the event"""
phoneAddress: PhoneAddress
"""The type of the event's address"""
physicalAddress: Address
"""When the event was published"""
publishAt: DateTime
"""Events related to this one"""
relatedEvents: [Event]
"""The event's description's slug"""
slug: String
"""Status of the event"""
status: EventStatus
"""The event's tags"""
tags: [Tag]
"""A thumbnail picture for the event"""
thumbnail: String
"""The event's title"""
title: String
"""When the event was last updated"""
updatedAt: DateTime
"""The ActivityPub Event URL"""
url: String
"""The Event UUID"""
uuid: UUID
"""The event's visibility"""
visibility: EventVisibility
}
"""Search events result"""
type Events {
"""Event elements"""
elements: [Event]!
"""Total elements"""
total: Int!
}
"""The list of possible options for the event's status"""
enum EventStatus {
"""The event is cancelled"""
CANCELLED
"""The event is confirmed"""
CONFIRMED
"""The event is tentative"""
TENTATIVE
}
"""The list of visibility options for an event"""
enum EventVisibility {
"""visible only to people invited"""
INVITE
"""Visible only after a moderator accepted"""
MODERATED
"""Visible only to people members of the group or followers of the person"""
PRIVATE
"""Publically listed and federated. Can be shared."""
PUBLIC
"""Visible only to people with the link - or invited"""
UNLISTED
}
"""Represents a participant to an event"""
type FeedToken {
"""The event which the actor participates in"""
actor: Actor
"""The role of this actor at this event"""
token: String
"""The actor that participates to the event"""
user: User
}
"""
Represents an actor's follower
"""
type Follower {
"""Which profile follows"""
actor: Actor
"""Whether the follow has been approved by the target actor"""
approved: Boolean
"""What or who the profile follows"""
targetActor: Actor
}
"""
Represents a group of actors
"""
type Group implements Actor {
"""The actor's avatar url"""
avatarUrl: String
"""The actor's banner url"""
bannerUrl: String
"""The actor's domain if (null if it's this instance)"""
domain: String
"""List of followers"""
followers: [Follower]
"""Number of followers for this actor"""
followersCount: Int
"""List of followings"""
following: [Follower]
"""Number of actors following this actor"""
followingCount: Int
"""Internal ID for this group"""
id: Int
"""The actors RSA Keys"""
keys: String
"""If the actor is from this instance"""
local: Boolean
"""Whether the actors manually approves followers"""
manuallyApprovesFollowers: Boolean
"""List of group members"""
members: [Member]!
"""The actor's displayed name"""
name: String
"""Whether the group is opened to all or has restricted access"""
openness: Openness
"""A list of the events this actor has organized"""
organizedEvents: [Event]
"""The actor's preferred username"""
preferredUsername: String
"""The actor's summary"""
summary: String
"""If the actor is suspended"""
suspended: Boolean
"""The type of Actor (Person, Group,)"""
type: ActorType
"""The type of group : Group, Community,"""
types: GroupType
"""The ActivityPub actor's URL"""
url: String
}
"""Search groups result"""
type Groups {
"""Group elements"""
elements: [Group]!
"""Total elements"""
total: Int!
}
"""
The types of Group that exist
"""
enum GroupType {
"""A public group of many actors"""
COMMUNITY
"""A private group of persons"""
GROUP
}
"""A JWT and the associated user ID"""
type Login {
"""A JWT Token for this session"""
token: String!
"""The user associated to this session"""
user: User!
}
"""
Represents a member of a group
"""
type Member {
"""Which profile is member of"""
actor: Person
"""Of which the profile is member"""
parent: Group
"""The role of this membership"""
role: Int
}
type OnlineAddress {
info: String
url: String
}
"""
Describes how an actor is opened to follows
"""
enum Openness {
"""The actor can only be followed by invitation"""
INVITE_ONLY
"""The actor needs to accept the following before it's effective"""
MODERATED
"""The actor is open to followings"""
OPEN
}
"""Represents a participant to an event"""
type Participant {
"""The actor that participates to the event"""
actor: Actor
"""The event which the actor participates in"""
event: Event
"""The role of this actor at this event"""
role: Int
}
"""
Represents a person identity
"""
type Person implements Actor {
"""The actor's avatar url"""
avatarUrl: String
"""The actor's banner url"""
bannerUrl: String
"""The actor's domain if (null if it's this instance)"""
domain: String
"""A list of the feed tokens for this person"""
feedTokens: [FeedToken]
"""List of followers"""
followers: [Follower]
"""Number of followers for this actor"""
followersCount: Int
"""List of followings"""
following: [Follower]
"""Number of actors following this actor"""
followingCount: Int
"""The list of events this person goes to"""
goingToEvents: [Event]
"""Internal ID for this person"""
id: Int
"""The actors RSA Keys"""
keys: String
"""If the actor is from this instance"""
local: Boolean
"""Whether the actors manually approves followers"""
manuallyApprovesFollowers: Boolean
"""The list of groups this person is member of"""
memberOf: [Member]
"""The actor's displayed name"""
name: String
"""A list of the events this actor has organized"""
organizedEvents: [Event]
"""The actor's preferred username"""
preferredUsername: String
"""The actor's summary"""
summary: String
"""If the actor is suspended"""
suspended: Boolean
"""The type of Actor (Person, Group,)"""
type: ActorType
"""The ActivityPub actor's URL"""
url: String
"""The user this actor is associated to"""
user: User
}
"""Search persons result"""
type Persons {
"""Person elements"""
elements: [Person]!
"""Total elements"""
total: Int!
}
type PhoneAddress {
info: String
phone: String
}
"""
The `Point` scalar type represents Point geographic information compliant string data,
represented as floats separated by a semi-colon. The geodetic system is WGS 84
"""
scalar Point
type RootMutationType {
"""Change default actor for user"""
changeDefaultActor(preferredUsername: String!): User
"""Create a comment"""
createComment(actorUsername: String!, text: String!): Comment
"""Create an event"""
createEvent(beginsOn: DateTime!, category: String!, description: String!, endsOn: DateTime, largeImage: String, onlineAddress: String, organizerActorId: ID!, phoneAddress: String, public: Boolean, publishAt: DateTime, state: Int, status: Int, thumbnail: String, title: String!): Event
"""Create a Feed Token"""
createFeedToken(actorId: Int): FeedToken
"""Create a group"""
createGroup(
"""
The actor's username which will be the admin (otherwise user's default one)
"""
adminActorUsername: String
"""The summary for the group"""
description: String = ""
"""The displayed name for the group"""
name: String
"""The name for the group"""
preferredUsername: String!
): Group
"""Create a new person for user"""
createPerson(
"""The displayed name for the new profile"""
name: String = ""
preferredUsername: String!
"""The summary for the new profile"""
summary: String = ""
): Person
"""Create an user"""
createUser(email: String!, password: String!): User
"""Delete an event"""
deleteEvent(actorId: Int!, eventId: Int!): DeletedObject
"""Delete a feed token"""
deleteFeedToken(token: String!): DeletedFeedToken
"""Delete a group"""
deleteGroup(actorId: Int!, groupId: Int!): DeletedObject
"""Join an event"""
joinEvent(actorId: Int!, eventId: Int!): Participant
"""Join a group"""
joinGroup(actorId: Int!, groupId: Int!): Member
"""Leave an event"""
leaveEvent(actorId: Int!, eventId: Int!): DeletedParticipant
"""Leave an event"""
leaveGroup(actorId: Int!, groupId: Int!): DeletedMember
"""Login an user"""
login(email: String!, password: String!): Login
"""Register a first profile on registration"""
registerPerson(
"""The email from the user previously created"""
email: String!
"""The displayed name for the new profile"""
name: String = ""
preferredUsername: String!
"""The summary for the new profile"""
summary: String = ""
): Person
"""Resend registration confirmation token"""
resendConfirmationEmail(email: String!, locale: String = "en"): String
"""Reset user password"""
resetPassword(locale: String = "en", password: String!, token: String!): Login
"""Send a link through email to reset user password"""
sendResetPassword(email: String!, locale: String = "en"): String
"""Validate an user after registration"""
validateUser(token: String!): Login
}
"""
Root Query
"""
type RootQueryType {
"""Get the instance config"""
config: Config
"""Get an event by uuid"""
event(uuid: UUID!): Event
"""Get all events"""
events(limit: Int = 10, page: Int = 1): [Event]
"""Get a group by it's preferred username"""
group(preferredUsername: String!): Group
"""Get all groups"""
groups(limit: Int = 10, page: Int = 1): [Group]
"""Get the persons for an user"""
identities: [Person]
"""Get the current actor for the logged-in user"""
loggedPerson: Person
"""Get the current user"""
loggedUser: User
"""Get all participants for an event uuid"""
participants(limit: Int = 10, page: Int = 1, uuid: UUID!): [Participant]
"""Get a person by it's preferred username"""
person(preferredUsername: String!): Person
"""Reverse geocode coordinates"""
reverseGeocode(latitude: Float!, longitude: Float!): [Address]
"""Search for an address"""
searchAddress(query: String!): [Address]
"""Search events"""
searchEvents(limit: Int = 10, page: Int = 1, search: String!): Events
"""Search groups"""
searchGroups(limit: Int = 10, page: Int = 1, search: String!): Groups
"""Search persons"""
searchPersons(limit: Int = 10, page: Int = 1, search: String!): Persons
"""Get the list of tags"""
tags(limit: Int = 10, page: Int = 1): [Tag]!
"""Get an user"""
user(id: ID!): User
"""List instance users"""
users(direction: SortDirection = DESC, limit: Int = 10, page: Int = 1, sort: SortableUserField = ID): Users
}
"""The list of possible options for the event's status"""
enum SortableUserField {
ID
}
"""Available sort directions"""
enum SortDirection {
ASC
DESC
}
"""A tag"""
type Tag {
"""The tag's ID"""
id: ID
"""Related tags to this tag"""
related: [Tag]
"""The tags's slug"""
slug: String
"""The tag's title"""
title: String
}
"""A local user of Mobilizon"""
type User {
"""The datetime the last activation/confirmation token was sent"""
confirmationSentAt: DateTime
"""The account activation/confirmation token"""
confirmationToken: String
"""The datetime when the user was confirmed/activated"""
confirmedAt: DateTime
"""The user's default actor"""
defaultActor: Person
"""The user's email"""
email: String!
"""A list of the feed tokens for this user"""
feedTokens: [FeedToken]
"""The user's ID"""
id: ID!
"""The user's list of profiles (identities)"""
profiles: [Person]!
"""The datetime last reset password email was sent"""
resetPasswordSentAt: DateTime
"""The token sent when requesting password token"""
resetPasswordToken: String
}
"""Users list"""
type Users {
"""User elements"""
elements: [User]!
"""Total elements"""
total: Int!
}
"""
The `UUID` scalar type represents UUID4 compliant string data, represented as UTF-8
character sequences. The UUID4 type is most often used to represent unique
human-readable ID strings.
"""
scalar UUID