Merge branch 'master' into refactoring-based-on-credo-and-dialyzer

This commit is contained in:
miffigriffi 2019-09-21 23:59:07 +02:00
commit 4c74248a04
126 changed files with 3311 additions and 2255 deletions

View File

@ -12,7 +12,7 @@ config :mobilizon, MobilizonWeb.Endpoint,
],
url: [
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local",
port: 80,
port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000,
scheme: "http"
],
debug_errors: true,

View File

@ -1,58 +0,0 @@
# On OSX the PATH variable isn't exported unless "SHELL" is also set, see: http://stackoverflow.com/a/25506676
SHELL = /bin/bash
NODE_BINDIR = ./node_modules/.bin
export PATH := $(NODE_BINDIR):$(PATH)
# Where to find input files (it can be multiple paths).
INPUT_FILES = ./src
# Where to write the files generated by this makefile.
OUTPUT_DIR = ./src/i18n
# Available locales for the app.
LOCALES = en_US fr_FR
# Name of the generated .po files for each available locale.
LOCALE_FILES ?= $(patsubst %,$(OUTPUT_DIR)/locale/%/LC_MESSAGES/app.po,$(LOCALES))
GETTEXT_HTML_SOURCES = $(shell find $(INPUT_FILES) -name '*.vue' -o -name '*.html' 2> /dev/null)
GETTEXT_JS_SOURCES = $(shell find $(INPUT_FILES) -name '*.vue' -o -name '*.js')
# Makefile Targets
.PHONY: clean makemessages translations
clean:
rm -f /tmp/template.pot $(OUTPUT_DIR)/translations.json
makemessages: /tmp/template.pot
translations: ./$(OUTPUT_DIR)/translations.json
# Create a main .pot template, then generate .po files for each available language.
# Thanx to Systematic: https://github.com/Polyconseil/systematic/blob/866d5a/mk/main.mk#L167-L183
/tmp/template.pot: $(GETTEXT_HTML_SOURCES)
# `dir` is a Makefile built-in expansion function which extracts the directory-part of `$@`.
# `$@` is a Makefile automatic variable: the file name of the target of the rule.
# => `mkdir -p /tmp/`
mkdir -p $(dir $@)
which gettext-extract
# Extract gettext strings from templates files and create a POT dictionary template.
gettext-extract --attribute v-translate --quiet --parseScript false --output $@ $(GETTEXT_HTML_SOURCES)
# Extract gettext strings from JavaScript files.
xgettext --language=JavaScript --keyword=npgettext:1c,2,3 \
--from-code=utf-8 --join-existing --no-wrap \
--package-name=$(shell node -e "console.log(require('./package.json').name);") \
--package-version=$(shell node -e "console.log(require('./package.json').version);") \
--output $@ $(GETTEXT_JS_SOURCES)
# Generate .po files for each available language.
@for lang in $(LOCALES); do \
export PO_FILE=$(OUTPUT_DIR)/locale/$$lang/LC_MESSAGES/app.po; \
echo "msgmerge --update $$PO_FILE $@"; \
mkdir -p $$(dirname $$PO_FILE); \
[ -f $$PO_FILE ] && msgmerge --lang=$$lang --update $$PO_FILE $@ || msginit --no-translator --locale=$$lang --input=$@ --output-file=$$PO_FILE; \
msgattrib --no-wrap --no-obsolete -o $$PO_FILE $$PO_FILE; \
done;
$(OUTPUT_DIR)/translations.json: clean /tmp/template.pot
mkdir -p $(OUTPUT_DIR)
gettext-compile --output $@ $(LOCALE_FILES)

View File

@ -9,7 +9,7 @@
"dev": "vue-cli-service build --watch",
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit",
"prepare": "patch-package"
"vue-i18n-extract": "vue-i18n-extract"
},
"dependencies": {
"apollo-absinthe-upload-link": "^1.5.0",
@ -18,7 +18,6 @@
"apollo-link": "^1.2.11",
"apollo-link-http": "^1.5.14",
"buefy": "^0.8.2",
"easygettext": "^2.7.0",
"graphql": "^14.2.1",
"graphql-tag": "^2.10.1",
"leaflet": "^1.4.0",
@ -33,7 +32,7 @@
"vue": "^2.6.10",
"vue-apollo": "^3.0.0-rc.1",
"vue-class-component": "^7.0.2",
"vue-gettext": "^2.1.3",
"vue-i18n": "^8.14.0",
"vue-property-decorator": "^8.1.0",
"vue-router": "^3.0.6",
"vue2-leaflet": "^2.0.3",
@ -58,12 +57,12 @@
"eslint": "^6.0.1",
"graphql-cli": "^3.0.12",
"node-sass": "^4.11.0",
"patch-package": "^6.1.2",
"sass-loader": "^8.0.0",
"tslint": "^5.16.0",
"tslint-config-airbnb": "^5.11.1",
"typescript": "^3.4.3",
"vue-cli-plugin-webpack-bundle-analyzer": "^1.3.0",
"vue-i18n-extract": "^1.0.2",
"vue-svg-inline-loader": "^1.2.15",
"vue-template-compiler": "^2.6.10",
"webpack": "^4.30.0"

View File

@ -1,41 +0,0 @@
patch-package
--- a/node_modules/easygettext/src/extract-cli.js
+++ b/node_modules/easygettext/src/extract-cli.js
@@ -22,9 +22,12 @@ const endDelimiter = argv.endDelimiter === undefined ? constants.DEFAULT_DELIMIT
const extraAttribute = argv.attribute || false;
const extraFilter = argv.filter || false;
const filterPrefix = argv.filterPrefix || constants.DEFAULT_FILTER_PREFIX;
+const parseScript = argv.parseScript === undefined ? true : argv.parseScript === 'true';
if (!quietMode && (!files || files.length === 0)) {
- console.log('Usage:\n\tgettext-extract [--attribute EXTRA-ATTRIBUTE] [--filterPrefix FILTER-PREFIX] [--output OUTFILE] <FILES>');
+ console.log(
+ 'Usage:\n\tgettext-extract [--attribute EXTRA-ATTRIBUTE] [--filterPrefix FILTER-PREFIX] [--parseScript BOOLEAN] [--output OUTFILE] <FILES>',
+ );
process.exit(1);
}
@@ -54,7 +57,7 @@ const extractor = new extract.Extractor({
});
-files.forEach(function(filename) {
+files.forEach(function (filename) {
let file = filename;
const ext = file.split('.').pop();
if (ALLOWED_EXTENSIONS.indexOf(ext) === -1) {
@@ -63,9 +66,13 @@ files.forEach(function(filename) {
}
console.log(`[${PROGRAM_NAME}] extracting: '${filename}`);
try {
- let data = fs.readFileSync(file, {encoding: 'utf-8'}).toString();
+ let data = fs.readFileSync(file, { encoding: 'utf-8' }).toString();
extractor.parse(file, extract.preprocessTemplate(data, ext));
+ if (!parseScript) {
+ return;
+ }
+
if (ext !== 'js') {
data = extract.preprocessScriptTags(data, ext);
}

View File

@ -11,11 +11,20 @@
<script lang="ts">
import NavBar from '@/components/NavBar.vue';
import { Component, Vue } from 'vue-property-decorator';
import { AUTH_ACCESS_TOKEN, AUTH_USER_ACTOR, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import {
AUTH_ACCESS_TOKEN,
AUTH_USER_ACTOR_ID,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
} from '@/constants';
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model';
import Footer from '@/components/Footer.vue';
import Logo from '@/components/Logo.vue';
import { CURRENT_ACTOR_CLIENT, IDENTITIES, UPDATE_CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { changeIdentity, saveActorData } from '@/utils/auth';
@Component({
apollo: {
@ -30,34 +39,49 @@ import Logo from '@/components/Logo.vue';
},
})
export default class App extends Vue {
currentUser!: ICurrentUser;
actor = localStorage.getItem(AUTH_USER_ACTOR);
async mounted() {
async created() {
await this.initializeCurrentUser();
}
getUser(): ICurrentUser | false {
return this.currentUser.id ? this.currentUser : false;
await this.initializeCurrentActor();
}
private initializeCurrentUser() {
const userId = localStorage.getItem(AUTH_USER_ID);
const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN);
const role = localStorage.getItem(AUTH_USER_ROLE);
if (userId && userEmail && accessToken) {
if (userId && userEmail && accessToken && role) {
return this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
variables: {
id: userId,
email: userEmail,
isLoggedIn: true,
role,
},
});
}
}
/**
* We fetch from localStorage the latest actor ID used,
* then fetch the current identities to set in cache
* the current identity used
*/
private async initializeCurrentActor() {
const actorId = localStorage.getItem(AUTH_USER_ACTOR_ID);
const result = await this.$apollo.query({
query: IDENTITIES,
});
const identities = result.data.identities;
if (identities.length < 1) return;
const activeIdentity = identities.find(identity => identity.id === actorId) || identities[0] as IPerson;
if (activeIdentity) {
return await changeIdentity(this.$apollo.provider.defaultClient, activeIdentity);
}
}
}
</script>
@ -74,6 +98,8 @@ export default class App extends Vue {
@import "~bulma/sass/components/navbar.sass";
@import "~bulma/sass/components/pagination.sass";
@import "~bulma/sass/components/dropdown.sass";
@import "~bulma/sass/components/breadcrumb.sass";
@import "~bulma/sass/components/list.sass";
@import "~bulma/sass/elements/box.sass";
@import "~bulma/sass/elements/button.sass";
@import "~bulma/sass/elements/container.sass";
@ -84,6 +110,7 @@ export default class App extends Vue {
@import "~bulma/sass/elements/tag.sass";
@import "~bulma/sass/elements/title.sass";
@import "~bulma/sass/elements/notification";
@import "~bulma/sass/elements/table";
@import "~bulma/sass/grid/_all.sass";
@import "~bulma/sass/layout/_all.sass";
@ -100,6 +127,7 @@ export default class App extends Vue {
@import "~buefy/src/scss/components/upload";
@import "~buefy/src/scss/components/radio";
@import "~buefy/src/scss/components/switch";
@import "~buefy/src/scss/components/table";
.router-enter-active,
.router-leave-active {

View File

@ -1,5 +1,6 @@
import { ApolloCache } from 'apollo-cache';
import { NormalizedCacheObject } from 'apollo-cache-inmemory';
import { ICurrentUserRole } from '@/types/current-user.model';
export function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
cache.writeData({
@ -9,22 +10,44 @@ export function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObjec
id: null,
email: null,
isLoggedIn: false,
role: ICurrentUserRole.USER,
},
currentActor: {
__typename: 'CurrentActor',
id: null,
preferredUsername: null,
name: null,
avatar: null,
},
},
});
return {
Mutation: {
updateCurrentUser: (_, { id, email, isLoggedIn }, { cache }) => {
updateCurrentUser: (_, { id, email, isLoggedIn, role }, { cache }) => {
const data = {
currentUser: {
id,
email,
isLoggedIn,
role,
__typename: 'CurrentUser',
},
};
cache.writeData({ data });
},
updateCurrentActor: (_, { id, preferredUsername, avatar, name }, { cache }) => {
const data = {
currentActor: {
id,
preferredUsername,
avatar,
name,
__typename: 'CurrentActor',
},
};
cache.writeData({ data });
},
},

View File

@ -1,7 +1,7 @@
<template>
<section>
<h1 class="title">
<translate>My identities</translate>
{{ $t('My identities') }}
</h1>
<ul class="identities">
@ -24,7 +24,7 @@
</ul>
<router-link :to="{ name: 'CreateIdentity' }" class="button create-identity is-primary" >
<translate>Create a new identity</translate>
{{ $t('Create a new identity') }}
</router-link>
</section>
</template>
@ -55,7 +55,7 @@
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IDENTITIES, LOGGED_PERSON } from '@/graphql/actor';
import { IDENTITIES } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor';
@Component({

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="editor" id="tiptab-editor" :data-actor-id="loggedPerson && loggedPerson.id">
<div class="editor" id="tiptab-editor" :data-actor-id="currentActor && currentActor.id">
<editor-menu-bar :editor="editor" v-slot="{ commands, isActive, focused }">
<div class="menubar bar-is-hidden" :class="{ 'is-focused': focused }">
@ -176,20 +176,20 @@ import { IActor, IPerson } from '@/types/actor';
import Image from '@/components/Editor/Image';
import { UPLOAD_PICTURE } from '@/graphql/upload';
import { listenFileUpload } from '@/utils/upload';
import { LOGGED_PERSON } from '@/graphql/actor';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
@Component({
components: { EditorContent, EditorMenuBar, EditorMenuBubble },
apollo: {
loggedPerson: {
query: LOGGED_PERSON,
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
export default class CreateEvent extends Vue {
@Prop({ required: true }) value!: String;
loggedPerson!: IPerson;
currentActor!: IPerson;
editor: Editor = null;
@ -438,7 +438,7 @@ export default class CreateEvent extends Vue {
variables: {
file: image,
name: image.name,
actorId: this.loggedPerson.id,
actorId: this.currentActor.id,
},
});
if (data.uploadPicture && data.uploadPicture.url) {

View File

@ -1,10 +1,10 @@
<template>
<div>
<b-field label="Find an address">
<b-field :label="$t('Find an address')">
<b-autocomplete
:data="data"
v-model="queryText"
placeholder="e.g. 10 Rue Jangot"
:placeholder="$t('e.g. 10 Rue Jangot')"
field="description"
:loading="isFetching"
@typing="getAsyncData"
@ -18,12 +18,12 @@
</p>
</template>
<template slot="empty">
<span v-if="queryText.length < 5">Please type at least 5 caracters</span>
<span v-else-if="isFetching">Searching</span>
<span v-if="queryText.length < 5">{{ $t('Please type at least 5 characters') }}</span>
<span v-else-if="isFetching">{{ $t('Searching…') }}</span>
<div v-else class="is-enabled">
<span>No results for « {{ queryText }} »</span>
<span>{{ $t('No results for "{queryText}"', { queryText }) }}</span>
<p class="control" @click="addressModalActive = true">
<button type="button" class="button is-primary">Add</button>
<button type="button" class="button is-primary">{{ $t('Add') }}</button>
</p>
</div>
</template>
@ -32,37 +32,37 @@
<b-modal :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">
<div class="modal-card" style="width: auto">
<header class="modal-card-head">
<p class="modal-card-title">Login</p>
<p class="modal-card-title">{{ $t('Add an address') }}</p>
</header>
<section class="modal-card-body">
<form>
<b-field :label="$gettext('Name')">
<b-field :label="$t('Name')">
<b-input aria-required="true" required v-model="selected.description" />
</b-field>
<b-field :label="$gettext('Street')">
<b-field :label="$t('Street')">
<b-input v-model="selected.street" />
</b-field>
<b-field :label="$gettext('Postal Code')">
<b-field :label="$t('Postal Code')">
<b-input v-model="selected.postalCode" />
</b-field>
<b-field :label="$gettext('Locality')">
<b-field :label="$t('Locality')">
<b-input v-model="selected.locality" />
</b-field>
<b-field :label="$gettext('Region')">
<b-field :label="$t('Region')">
<b-input v-model="selected.region" />
</b-field>
<b-field :label="$gettext('Country')">
<b-field :label="$t('Country')">
<b-input v-model="selected.country" />
</b-field>
</form>
</section>
<footer class="modal-card-foot">
<button class="button" type="button" @click="resetPopup()">Clear</button>
<button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>
</footer>
</div>
</b-modal>

View File

@ -1,6 +1,6 @@
<template>
<b-field grouped horizontal :label="label">
<b-datepicker expanded v-model="date" :placeholder="$gettext('Click to select')" icon="calendar"></b-datepicker>
<b-datepicker expanded v-model="date" :placeholder="$t('Click to select')" icon="calendar"></b-datepicker>
<b-input expanded type="time" required v-model="time" />
</b-field>
</template>

View File

@ -5,7 +5,7 @@
<div class="tag-container" v-if="event.tags">
<b-tag v-for="tag in event.tags.slice(0, 3)" :key="tag.slug" type="is-secondary">{{ tag.title }}</b-tag>
</div>
<img src="https://picsum.photos/g/400/225/?random">
<img src="https://picsum.photos/g/400/225/?random" />
</figure>
</div>
<div class="content">
@ -29,7 +29,7 @@
<!-- <div v-else-if="event.participants.length === 1">-->
<!-- <translate-->
<!-- :translate-params="{name: event.participants[0].actor.preferredUsername}"-->
<!-- >%{name} organizes this event</translate>-->
<!-- >{name} organizes this event</translate>-->
<!-- </div>-->
<!-- <div v-else>-->
<!-- <span v-for="participant in event.participants" :key="participant.actor.uuid">-->
@ -37,7 +37,7 @@
<!-- <span v-if="participant.role === ParticipantRole.CREATOR">(organizer)</span>,-->
<!-- &lt;!&ndash; <translate-->
<!-- :translate-params="{name: participant.actor.preferredUsername}"-->
<!-- >&nbsp;%{name} is in,</translate>&ndash;&gt;-->
<!-- >&nbsp;{name} is in,</translate>&ndash;&gt;-->
<!-- </span>-->
<!-- </div>-->
</router-link>

View File

@ -1,16 +1,12 @@
<template>
<translate
v-if="!endsOn"
:translate-params="{date: formatDate(beginsOn), time: formatTime(beginsOn)}"
>The %{ date } at %{ time }</translate>
<translate
v-else-if="isSameDay()"
:translate-params="{date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}"
>The %{ date } from %{ startTime } to %{ endTime }</translate>
<translate
v-else-if="endsOn"
:translate-params="{startDate: formatDate(beginsOn), startTime: formatTime(beginsOn), endDate: formatDate(endsOn), endTime: formatTime(endsOn)}"
>From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }</translate>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span>
<span v-else-if="isSameDay()">
{{ $t('The {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }}
</span>
<span v-else-if="endsOn">
{{ $t('From the {startDate} at {startTime} to the {endDate} at {endTime}',
{startDate: formatDate(beginsOn), startTime: formatTime(beginsOn), endDate: formatDate(endsOn), endTime: formatTime(endsOn)}) }}
</span>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
@ -21,11 +17,13 @@ export default class EventFullDate extends Vue {
@Prop({ required: false }) endsOn!: string;
formatDate(value) {
return value ? new Date(value).toLocaleString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) : null;
if (!this.$options.filters) return;
return this.$options.filters.formatDateString(value);
}
formatTime(value) {
return value ? new Date(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }) : null;
if (!this.$options.filters) return;
return this.$options.filters.formatTimeString(value);
}
isSameDay() {

View File

@ -0,0 +1,86 @@
<template>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">Join event {{ event.title }}</p>
</header>
<section class="modal-card-body is-flex">
<div class="media">
<div
class="media-left">
<b-icon
icon="alert"
type="is-warning"
size="is-large"/>
</div>
<div class="media-content">
<p>Do you want to participate in {{ event.title }}?</p>
<b-field :label="$t('Identity')">
<identity-picker v-model="identity"></identity-picker>
</b-field>
<p v-if="!event.local">
The event came from another instance. Your participation will be confirmed after we confirm it with the other instance.
</p>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button
class="button"
ref="cancelButton"
@click="close">
Cancel
</button>
<button
class="button is-primary"
ref="confirmButton"
@click="confirm">
Confirm my particpation
</button>
</footer>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IEvent } from '@/types/event.model';
import IdentityPicker from '@/views/Account/IdentityPicker.vue';
import { IPerson } from '@/types/actor';
@Component({
components: {
IdentityPicker,
},
mounted() {
this.$data.isActive = true;
},
})
export default class ReportModal extends Vue {
@Prop({ type: Function, default: () => {} }) onConfirm;
@Prop({ type: Object }) event! : IEvent;
@Prop({ type: Object }) defaultIdentity!: IPerson;
isActive: boolean = false;
identity: IPerson = this.defaultIdentity;
confirm() {
this.onConfirm(this.identity);
}
/**
* Close the Dialog.
*/
close() {
this.isActive = false;
this.$emit('close');
}
}
</script>
<style lang="scss">
.modal-card .modal-card-foot {
justify-content: flex-end;
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<b-field label="Enter some tags">
<b-field :label="$t('Enter some tags')">
<b-taginput
v-model="tagsStrings"
:data="filteredTags"
@ -7,7 +7,7 @@
:allow-new="true"
:field="path"
icon="label"
placeholder="Add a tag"
:placeholder="$t('Add a tag')"
@typing="getFilteredTags"
>
</b-taginput>

View File

@ -1,18 +1,13 @@
<template>
<footer class="footer">
<mobilizon-logo :invert="true" class="logo" />
<img src="../assets/footer.png" :alt="$gettext('World map')" />
<img src="../assets/footer.png" :alt="$t('World map')" />
<ul>
<li><router-link :to="{ name: 'About'}"><translate>About</translate></router-link></li>
<li><router-link :to="{ name: 'Licence'}"><translate>License</translate></router-link></li>
<li><router-link :to="{ name: 'Legal'}"><translate>Legal</translate></router-link></li>
<li><a href="https://joinmobilizon.org">{{ $t('About') }}</a></li>
<li><a href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">{{ $t('License') }}</a></li>
</ul>
<div class="content has-text-centered">
<span
v-translate="{
date: new Date().getFullYear(),
}"
>© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks</span>
<span>{{ $t('© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks', { date: new Date().getFullYear()}) }}</span>
</div>
</footer>
</template>

View File

@ -27,34 +27,54 @@
<div class="navbar-item has-dropdown is-hoverable" v-if="currentUser.isLoggedIn">
<a
class="navbar-link"
v-if="loggedPerson"
v-if="currentActor"
>
<figure class="image is-24x24" v-if="loggedPerson.avatar">
<img alt="avatarUrl" :src="loggedPerson.avatar.url">
<figure class="image is-24x24" v-if="currentActor.avatar">
<img alt="avatarUrl" :src="currentActor.avatar.url">
</figure>
<span>{{ loggedPerson.preferredUsername }}</span>
<span>{{ currentActor.preferredUsername }}</span>
</a>
<div class="navbar-dropdown">
<span class="navbar-item">
<router-link :to="{ name: 'UpdateIdentity' }" v-translate>My account</router-link>
</span>
<div class="navbar-dropdown is-boxed">
<div v-for="identity in identities" v-if="identities.length > 0">
<a class="navbar-item" @click="setIdentity(identity)" :class="{ 'is-active': identity.id === currentActor.id }">
<div class="media-left">
<figure class="image is-24x24" v-if="identity.avatar">
<img class="is-rounded" :src="identity.avatar.url">
</figure>
</div>
<span class="navbar-item">
<router-link :to="{ name: ActorRouteName.CREATE_GROUP }" v-translate>Create group</router-link>
</span>
<div class="media-content">
<h3>{{ identity.displayName() }}</h3>
</div>
</a>
<a v-translate class="navbar-item" v-on:click="logout()">Log out</a>
<hr class="navbar-divider">
</div>
<a class="navbar-item">
<router-link :to="{ name: 'UpdateIdentity' }">{{ $t('My account') }}</router-link>
</a>
<a class="navbar-item">
<router-link :to="{ name: ActorRouteName.CREATE_GROUP }">{{ $t('Create group') }}</router-link>
</a>
<a class="navbar-item" v-if="currentUser.role === ICurrentUserRole.ADMINISTRATOR">
<router-link :to="{ name: AdminRouteName.DASHBOARD }">{{ $t('Administration') }}</router-link>
</a>
<a class="navbar-item" v-on:click="logout()">{{ $t('Log out') }}</a>
</div>
</div>
<div class="navbar-item" v-else>
<div class="buttons">
<router-link class="button is-primary" v-if="config && config.registrationsOpen" :to="{ name: 'Register' }">
<strong v-translate>Sign up</strong>
<strong>{{ $t('Sign up') }}</strong>
</router-link>
<router-link class="button is-primary" :to="{ name: 'Login' }" v-translate>Log in</router-link>
<router-link class="button is-primary" :to="{ name: 'Login' }">{{ $t('Log in') }}</router-link>
</div>
</div>
</div>
@ -66,21 +86,30 @@
<script lang="ts">
import { Component, Vue, Watch } from 'vue-property-decorator';
import { CURRENT_USER_CLIENT } from '@/graphql/user';
import { logout } from '@/utils/auth';
import { LOGGED_PERSON } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { changeIdentity, logout } from '@/utils/auth';
import { CURRENT_ACTOR_CLIENT, IDENTITIES, UPDATE_CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor';
import { CONFIG } from '@/graphql/config';
import { IConfig } from '@/types/config.model';
import { ICurrentUser } from '@/types/current-user.model';
import { ICurrentUser, ICurrentUserRole } from '@/types/current-user.model';
import Logo from '@/components/Logo.vue';
import SearchField from '@/components/SearchField.vue';
import { ActorRouteName } from '@/router/actor';
import { AdminRouteName } from '@/router/admin';
import { RouteName } from '@/router';
@Component({
apollo: {
currentUser: {
query: CURRENT_USER_CLIENT,
},
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
identities: {
query: IDENTITIES,
update: ({ identities }) => identities.map(identity => new Person(identity)),
},
config: {
query: CONFIG,
},
@ -95,31 +124,40 @@ export default class NavBar extends Vue {
{ header: 'Coucou' },
{ title: 'T\'as une notification', subtitle: 'Et elle est cool' },
];
loggedPerson: IPerson | null = null;
currentActor!: IPerson;
config!: IConfig;
currentUser!: ICurrentUser;
ICurrentUserRole = ICurrentUserRole;
identities!: IPerson[];
showNavbar: boolean = false;
ActorRouteName = ActorRouteName;
AdminRouteName = AdminRouteName;
@Watch('currentUser')
async onCurrentUserChanged() {
// Refresh logged person object
if (this.currentUser.isLoggedIn) {
const result = await this.$apollo.query({
query: LOGGED_PERSON,
});
this.loggedPerson = result.data.loggedPerson;
} else {
this.loggedPerson = null;
}
}
// @Watch('currentUser')
// async onCurrentUserChanged() {
// // Refresh logged person object
// if (this.currentUser.isLoggedIn) {
// const result = await this.$apollo.query({
// query: CURRENT_ACTOR_CLIENT,
// });
// console.log(result);
//
// this.loggedPerson = result.data.currentActor;
// } else {
// this.loggedPerson = null;
// }
// }
async logout() {
await logout(this.$apollo.provider.defaultClient);
return this.$router.push({ path: '/' });
if (this.$route.name === RouteName.HOME) return;
return this.$router.push({ name: RouteName.HOME });
}
async setIdentity(identity: IPerson) {
return await changeIdentity(this.$apollo.provider.defaultClient, identity);
}
}
</script>

View File

@ -8,7 +8,7 @@
<b-upload @input="onFileChanged">
<a class="button is-primary">
<b-icon icon="upload"></b-icon>
<span>Click to upload</span>
<span>{{ $t('Click to upload') }}</span>
</a>
</b-upload>
</div>
@ -41,6 +41,10 @@ export default class PictureUpload extends Vue {
imageSrc: string | null = null;
mounted() {
this.updatePreview(this.pictureFile);
}
@Watch('pictureFile')
onPictureFileChanged(val: File) {
this.updatePreview(val);

View File

@ -0,0 +1,45 @@
<template>
<div class="card" v-if="report">
<div class="card-content">
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="report.reported.avatar">
<img :src="report.reported.avatar.url" />
</figure>
</div>
<div class="media-content">
<p class="title is-4">{{ report.reported.name }}</p>
<p class="subtitle is-6">@{{ report.reported.preferredUsername }}</p>
</div>
</div>
<div class="content columns">
<div class="column is-one-quarter box">Reported by <img v-if="report.reporter.avatar" class="image" :src="report.reporter.avatar.url" /> @{{ report.reporter.preferredUsername }}</div>
<div class="column box" v-if="report.event">
<img class="image" v-if="report.event.picture" :src="report.event.picture.url" />
<span>{{ report.event.title }}</span>
</div>
<div class="column box" v-if="report.reportContent">{{ report.reportContent }}</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IReport } from '@/types/report.model';
import { EventRouteName } from '@/router/event';
@Component
export default class ReportCard extends Vue {
@Prop({ required: true }) report!: IReport;
EventRouteName = EventRouteName;
}
</script>
<style lang="scss">
.content img.image {
display: inline;
height: 1.5em;
vertical-align: text-bottom;
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<div class="modal-card">
<header class="modal-card-head" v-if="title">
<p class="modal-card-title">{{ title }}</p>
</header>
<section
class="modal-card-body is-flex"
:class="{ 'is-titleless': !title }">
<div class="media">
<div
class="media-left">
<b-icon
icon="alert"
type="is-warning"
size="is-large"/>
</div>
<div class="media-content">
<p>The report will be sent to the moderators of your instance.
You can explain why you report this content below.</p>
<div class="control">
<b-input
v-model="content"
type="textarea"
@keyup.enter="confirm"
placeholder="Additional comments"
/>
</div>
<p v-if="outsideDomain">
The content came from another server. Transfer an anonymous copy of the report ?
</p>
<div class="control" v-if="outsideDomain">
<b-switch v-model="forward">Transfer to {{ outsideDomain }}</b-switch>
</div>
</div>
</div>
</section>
<footer class="modal-card-foot">
<button
class="button"
ref="cancelButton"
@click="close">
{{ cancelText }}
</button>
<button
class="button is-primary"
ref="confirmButton"
@click="confirm">
{{ confirmText }}
</button>
</footer>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { removeElement } from 'buefy/src/utils/helpers';
@Component({
mounted() {
this.$data.isActive = true;
},
})
export default class ReportModal extends Vue {
@Prop({ type: Function, default: () => {} }) onConfirm;
@Prop({ type: String }) title;
@Prop({ type: String, default: '' }) outsideDomain;
@Prop({ type: String, default: 'Cancel' }) cancelText;
@Prop({ type: String, default: 'Send the report' }) confirmText;
isActive: boolean = false;
content: string = '';
forward: boolean = false;
confirm() {
this.onConfirm(this.content, this.forward);
this.close();
}
/**
* Close the Dialog.
*/
close() {
this.isActive = false;
this.$emit('close');
}
}
</script>
<style lang="scss">
.modal-card .modal-card-foot {
justify-content: flex-end;
}
</style>

View File

@ -16,7 +16,7 @@ export default class SearchField extends Vue {
get defaultPlaceHolder(): string {
// We can't use "this" inside @Prop's default value.
return this.placeholder || this.$gettext('Search');
return this.placeholder || this.$t('Search') as string;
}
}
</script>

View File

@ -2,4 +2,5 @@ export const AUTH_ACCESS_TOKEN = 'auth-access-token';
export const AUTH_REFRESH_TOKEN = 'auth-refresh-token';
export const AUTH_USER_ID = 'auth-user-id';
export const AUTH_USER_EMAIL = 'auth-user-email';
export const AUTH_USER_ACTOR = 'auth-user-actor';
export const AUTH_USER_ACTOR_ID = 'auth-user-actor-id';
export const AUTH_USER_ROLE = 'auth-user-role';

View File

@ -0,0 +1,19 @@
function parseDateTime(value: string): Date {
return new Date(value);
}
function formatDateString(value: string): string {
return parseDateTime(value).toLocaleString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
}
function formatTimeString(value: string): string {
return parseDateTime(value).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' });
}
function formatDateTimeString(value: string): string {
return parseDateTime(value).toLocaleTimeString(undefined, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' });
}
export { formatDateString, formatTimeString, formatDateTimeString };

9
js/src/filters/index.ts Normal file
View File

@ -0,0 +1,9 @@
import { formatDateString, formatTimeString, formatDateTimeString } from './datetime';
export default {
install(vue) {
vue.filter('formatDateString', formatDateString);
vue.filter('formatTimeString', formatTimeString);
vue.filter('formatDateTimeString', formatDateTimeString);
},
};

View File

@ -40,6 +40,25 @@ query {
}
}`;
export const CURRENT_ACTOR_CLIENT = gql`
query {
currentActor @client {
id,
avatar {
url
},
preferredUsername,
name
}
}
`;
export const UPDATE_CURRENT_ACTOR_CLIENT = gql`
mutation UpdateCurrentActor($id: String!, $avatar: String, $preferredUsername: String!, $name: String!) {
updateCurrentActor(id: $id, avatar: $avatar, preferredUsername: $preferredUsername, name: $name) @client
}
`;
export const LOGGED_PERSON_WITH_GOING_TO_EVENTS = gql`
query {
loggedPerson {
@ -177,7 +196,7 @@ query($name:String!) {
export const CREATE_GROUP = gql`
mutation CreateGroup(
$creatorActorId: Int!,
$creatorActorId: ID!,
$preferredUsername: String!,
$name: String!,
$summary: String,

19
js/src/graphql/admin.ts Normal file
View File

@ -0,0 +1,19 @@
import gql from 'graphql-tag';
export const DASHBOARD = gql`
query {
dashboard {
lastPublicEventPublished {
title,
picture {
alt
url
},
},
numberOfUsers,
numberOfEvents,
numberOfComments,
numberOfReports
}
}
`;

View File

@ -7,7 +7,8 @@ mutation Login($email: String!, $password: String!) {
refreshToken,
user {
id,
email
email,
role
}
},
}

View File

@ -12,6 +12,43 @@ const participantQuery = `
}
`;
const physicalAddressQuery = `
description,
floor,
street,
locality,
postalCode,
region,
country,
geom
`;
const tagsQuery = `
id,
slug,
title
`;
const optionsQuery = `
maximumAttendeeCapacity,
remainingAttendeeCapacity,
showRemainingAttendeeCapacity,
offers {
price,
priceCurrency,
url
},
participationConditions {
title,
content,
url
},
attendees,
program,
commentModeration,
showParticipationPrice
`;
export const FETCH_EVENT = gql`
query($uuid:UUID!) {
event(uuid: $uuid) {
@ -29,20 +66,14 @@ export const FETCH_EVENT = gql`
picture {
id
url
name
},
publishAt,
category,
# online_address,
# phone_address,
onlineAddress,
phoneAddress,
physicalAddress {
description,
floor,
street,
locality,
postalCode,
region,
country,
geom
${physicalAddressQuery}
}
organizerActor {
avatar {
@ -64,10 +95,12 @@ export const FETCH_EVENT = gql`
participants {
${participantQuery}
},
participantStats {
approved,
unapproved
},
tags {
id,
slug,
title
${tagsQuery}
},
relatedEvents {
uuid,
@ -86,23 +119,7 @@ export const FETCH_EVENT = gql`
}
},
options {
maximumAttendeeCapacity,
remainingAttendeeCapacity,
showRemainingAttendeeCapacity,
offers {
price,
priceCurrency,
url
},
participationConditions {
title,
content,
url
},
attendees,
program,
commentModeration,
showParticipationPrice
${optionsQuery}
}
}
}
@ -159,37 +176,62 @@ export const FETCH_EVENTS = gql`
`;
export const CREATE_EVENT = gql`
mutation CreateEvent(
mutation createEvent(
$organizerActorId: ID!,
$title: String!,
$description: String!,
$organizerActorId: ID!,
$category: String,
$beginsOn: DateTime!,
$endsOn: DateTime,
$picture: PictureInput,
$tags: [String],
$options: EventOptionsInput,
$physicalAddress: AddressInput,
$status: EventStatus,
$visibility: EventVisibility
$tags: [String],
$picture: PictureInput,
$onlineAddress: String,
$phoneAddress: String,
$category: String,
$physicalAddress: AddressInput,
$options: EventOptionsInput,
) {
createEvent(
organizerActorId: $organizerActorId,
title: $title,
description: $description,
beginsOn: $beginsOn,
endsOn: $endsOn,
organizerActorId: $organizerActorId,
category: $category,
options: $options,
picture: $picture,
status: $status,
visibility: $visibility,
tags: $tags,
physicalAddress: $physicalAddress,
visibility: $visibility
picture: $picture,
onlineAddress: $onlineAddress,
phoneAddress: $phoneAddress,
category: $category,
physicalAddress: $physicalAddress
options: $options,
) {
id,
uuid,
title,
description,
beginsOn,
endsOn,
status,
visibility,
picture {
id
url
},
publishAt,
category,
onlineAddress,
phoneAddress,
physicalAddress {
${physicalAddressQuery}
},
tags {
${tagsQuery}
},
options {
${optionsQuery}
}
}
}
@ -198,37 +240,67 @@ export const CREATE_EVENT = gql`
export const EDIT_EVENT = gql`
mutation updateEvent(
$id: ID!,
$title: String!,
$description: String!,
$organizerActorId: ID!,
$category: String,
$beginsOn: DateTime!,
$title: String,
$description: String,
$beginsOn: DateTime,
$endsOn: DateTime,
$picture: PictureInput,
$tags: [String],
$options: EventOptionsInput,
$physicalAddress: AddressInput,
$status: EventStatus,
$visibility: EventVisibility
$tags: [String],
$picture: PictureInput,
$onlineAddress: String,
$phoneAddress: String,
$category: String,
$physicalAddress: AddressInput,
$options: EventOptionsInput,
) {
updateEvent(eventId: $id,
updateEvent(
eventId: $id,
title: $title,
description: $description,
beginsOn: $beginsOn,
endsOn: $endsOn,
organizerActorId: $organizerActorId,
category: $category,
options: $options,
picture: $picture,
status: $status,
visibility: $visibility,
tags: $tags,
physicalAddress: $physicalAddress,
visibility: $visibility) {
uuid
picture: $picture,
onlineAddress: $onlineAddress,
phoneAddress: $phoneAddress,
category: $category,
physicalAddress: $physicalAddress
options: $options,
) {
id,
uuid,
title,
description,
beginsOn,
endsOn,
status,
visibility,
picture {
id
url
},
publishAt,
category,
onlineAddress,
phoneAddress,
physicalAddress {
${physicalAddressQuery}
},
tags {
${tagsQuery}
},
options {
${optionsQuery}
}
}
}
`;
export const JOIN_EVENT = gql`
mutation JoinEvent($eventId: Int!, $actorId: Int!) {
mutation JoinEvent($eventId: ID!, $actorId: ID!) {
joinEvent(
eventId: $eventId,
actorId: $actorId
@ -239,7 +311,7 @@ export const JOIN_EVENT = gql`
`;
export const LEAVE_EVENT = gql`
mutation LeaveEvent($eventId: Int!, $actorId: Int!) {
mutation LeaveEvent($eventId: ID!, $actorId: ID!) {
leaveEvent(
eventId: $eventId,
actorId: $actorId
@ -252,9 +324,9 @@ export const LEAVE_EVENT = gql`
`;
export const DELETE_EVENT = gql`
mutation DeleteEvent($id: Int!, $actorId: Int!) {
mutation DeleteEvent($eventId: ID!, $actorId: ID!) {
deleteEvent(
eventId: $id,
eventId: $eventId,
actorId: $actorId
) {
id

View File

@ -12,7 +12,7 @@ query {
}`;
export const CREATE_FEED_TOKEN_ACTOR = gql`
mutation createFeedToken($actor_id: Int!) {
mutation createFeedToken($actor_id: ID!) {
createFeedToken(actorId: $actor_id) {
token,
actor {

161
js/src/graphql/report.ts Normal file
View File

@ -0,0 +1,161 @@
import gql from 'graphql-tag';
export const REPORTS = gql`
query Reports($status: ReportStatus) {
reports(status: $status) {
id,
reported {
id,
preferredUsername,
name,
avatar {
url
}
},
reporter {
id,
preferredUsername,
name,
avatar {
url
}
},
event {
id,
uuid,
title,
picture {
url
}
},
status
}
}
`;
const REPORT_FRAGMENT = gql`
fragment ReportFragment on Report {
id,
reported {
id,
preferredUsername,
name,
avatar {
url
}
},
reporter {
id,
preferredUsername,
name,
avatar {
url
}
},
event {
id,
uuid,
title,
description,
picture {
url
}
},
notes {
id,
content
moderator {
preferredUsername,
name,
avatar {
url
}
},
insertedAt
},
insertedAt,
updatedAt,
status,
content
}
`;
export const REPORT = gql`
query Report($id: ID!) {
report(id: $id) {
...ReportFragment
}
}
${REPORT_FRAGMENT}
`;
export const CREATE_REPORT = gql`
mutation CreateReport(
$eventId: ID!,
$reporterActorId: ID!,
$reportedActorId: ID!,
$content: String
) {
createReport(eventId: $eventId, reporterActorId: $reporterActorId, reportedActorId: $reportedActorId, content: $content) {
id
}
}
`;
export const UPDATE_REPORT = gql`
mutation UpdateReport(
$reportId: ID!,
$moderatorId: ID!,
$status: ReportStatus!
) {
updateReportStatus(reportId: $reportId, moderatorId: $moderatorId, status: $status) {
...ReportFragment
}
}
${REPORT_FRAGMENT}
`;
export const CREATE_REPORT_NOTE = gql`
mutation CreateReportNote(
$reportId: ID!,
$moderatorId: ID!,
$content: String!
) {
createReportNote(reportId: $reportId, moderatorId: $moderatorId, content: $content) {
id,
content,
insertedAt
}
}
`;
export const LOGS = gql`
query {
actionLogs {
id,
action,
actor {
id,
preferredUsername
avatar {
url
}
},
object {
...on Report {
id
},
... on ReportNote {
report {
id,
}
}
... on Event {
id,
title
}
},
insertedAt
}
}
`;

View File

@ -31,12 +31,13 @@ query {
id,
email,
isLoggedIn,
role
}
}
`;
export const UPDATE_CURRENT_USER_CLIENT = gql`
mutation UpdateCurrentUser($id: Int!, $email: String!, $isLoggedIn: Boolean!) {
updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn) @client
mutation UpdateCurrentUser($id: String!, $email: String!, $isLoggedIn: Boolean!, $role: UserRole!) {
updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn, role: $role) @client
}
`;

200
js/src/i18n/en_US.json Normal file
View File

@ -0,0 +1,200 @@
{
"A validation email was sent to {email}": "A validation email was sent to {email}",
"About this event": "About this event",
"About this instance": "About this instance",
"About": "About",
"Add a new profile": "Add a new profile",
"Add a tag": "Add a tag",
"Add an address": "Add an address",
"Add to my calendar": "Add to my calendar",
"Add": "Add",
"Administration": "Administration",
"Allow all comments": "Allow all comments",
"Are you going to this event?": "Are you going to this event?",
"Are you sure you want to delete this event? This action cannot be reverted.": "Are you sure you want to delete this event? This action cannot be reverted.",
"Before you can login, you need to click on the link inside it to validate your account": "Before you can login, you need to click on the link inside it to validate your account",
"By {name}": "By {name}",
"Category": "Category",
"Change": "Change",
"Clear": "Clear",
"Click to select": "Click to select",
"Click to upload": "Click to upload",
"Close comments for all (except for admins)": "Close comments for all (except for admins)",
"Comments on the event page": "Comments on the event page",
"Comments": "Comments",
"Confirmed: Will happen": "Confirmed: Will happen",
"Country": "Country",
"Create a new event": "Create a new event",
"Create a new group": "Create a new group",
"Create a new identity": "Create a new identity",
"Create group": "Create group",
"Create my event": "Create my event",
"Create my group": "Create my group",
"Create my profile": "Create my profile",
"Create token": "Create token",
"Create your communities and your events": "Create your communities and your events",
"Create": "Create",
"Current": "Current",
"Delete event": "Delete event",
"Delete this identity": "Delete this identity",
"Delete your identity": "Delete your identity",
"Delete {eventTitle}": "Delete {eventTitle}",
"Delete {preferredUsername}": "Delete {preferredUsername}",
"Delete": "Delete",
"Description": "Description",
"Didn't receive the instructions ?": "Didn't receive the instructions ?",
"Disallow promoting on Mobilizon": "Disallow promoting on Mobilizon",
"Display name": "Display name",
"Display participation price": "Display participation price",
"Displayed name": "Displayed name",
"Edit": "Edit",
"Either the account is already validated, either the validation token is incorrect.": "Either the account is already validated, either the validation token is incorrect.",
"Email": "Email",
"Ends on…": "Ends on…",
"Enter some tags": "Enter some tags",
"Error while validating account": "Error while validating account",
"Event list": "Event list",
"Event {eventTitle} deleted": "Event {eventTitle} deleted",
"Event {eventTitle} reported": "Event {eventTitle} reported",
"Event": "Event",
"Events nearby you": "Events nearby you",
"Events you're going at": "Events you're going at",
"Events": "Events",
"Features": "Features",
"Find an address": "Find an address",
"Forgot your password ?": "Forgot your password ?",
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "From the {startDate} at {startTime} to the {endDate} at {endTime}",
"General information": "General information",
"Group List": "Group List",
"Group full name": "Group full name",
"Group name": "Group name",
"Group {displayName} created": "Group {displayName} created",
"Group": "Group",
"Groups": "Groups",
"I create an identity": "I create an identity",
"I want to approve every participation request": "I want to approve every participation request",
"Identities": "Identities",
"Identity {displayName} created": "Identity {displayName} created",
"Identity {displayName} deleted": "Identity {displayName} deleted",
"Identity {displayName} updated": "Identity {displayName} updated",
"Identity": "Identity",
"If an account with this email exists, we just sent another confirmation email to {email}": "If an account with this email exists, we just sent another confirmation email to {email}",
"If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.": "If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.",
"Join": "Join",
"Last published event": "Last published event",
"Learn more on {0}": "Learn more on {0}",
"Learn more on": "Learn more on",
"Leave": "Leave",
"Legal": "Legal",
"License": "License",
"Limited places": "Limited places",
"Loading…": "Loading…",
"Locality": "Locality",
"Log in": "Log in",
"Log out": "Log out",
"Login": "Login",
"Members": "Members",
"Moderated comments (shown after approval)": "Moderated comments (shown after approval)",
"My account": "My account",
"My identities": "My identities",
"Name": "Name",
"No address defined": "No address defined",
"No events found": "No events found",
"No group found": "No group found",
"No groups found": "No groups found",
"No results for \"{queryText}\"": "No results for \"{queryText}\"",
"Number of places": "Number of places",
"One person is going": "No one is going | One person is going | {approved} persons are going",
"Only accessible through link and search (private)": "Only accessible through link and search (private)",
"Opened reports": "Opened reports",
"Organized": "Organized",
"Organizer": "Organizer",
"Other stuff…": "Other stuff…",
"Otherwise this identity will just be removed from the group administrators.": "Otherwise this identity will just be removed from the group administrators.",
"Page limited to my group (asks for auth)": "Page limited to my group (asks for auth)",
"Participation approval": "Participation approval",
"Password reset": "Password reset",
"Password": "Password",
"Pick an identity": "Pick an identity",
"Please be nice to each other": "Please be nice to each other",
"Please check you spam folder if you didn't receive the email.": "Please check you spam folder if you didn't receive the email.",
"Please contact this instance's Mobilizon admin if you think this is a mistake.": "Please contact this instance's Mobilizon admin if you think this is a mistake.",
"Please make sure the address is correct and that the page hasn't been moved.": "Please make sure the address is correct and that the page hasn't been moved.",
"Please read the full rules": "Please read the full rules",
"Please type at least 5 characters": "Please type at least 5 characters",
"Postal Code": "Postal Code",
"Private feeds": "Private feeds",
"Promotion": "Promotion",
"Public RSS/Atom Feed": "Public RSS/Atom Feed",
"Public comment moderation": "Public comment moderation",
"Public feeds": "Public feeds",
"Public iCal Feed": "Public iCal Feed",
"Published events": "Published events",
"RSS/Atom Feed": "RSS/Atom Feed",
"Region": "Region",
"Register an account on Mobilizon!": "Register an account on Mobilizon!",
"Register": "Register",
"Registration is currently closed.": "Registration is currently closed.",
"Report": "Signaler",
"Resend confirmation email": "Resend confirmation email",
"Reset my password": "Reset my password",
"Save": "Save",
"Search events, groups, etc.": "Search events, groups, etc.",
"Search results: \"{search}\"": "Search results: \"{search}\"",
"Search": "Search",
"Searching…": "Searching…",
"Send confirmation email again": "Send confirmation email again",
"Send email to reset my password": "Send email to reset my password",
"Share this event": "Share this event",
"Show map": "Show map",
"Show remaining number of places": "Show remaining number of places",
"Sign up": "Sign up",
"Starts on…": "Starts on…",
"Status": "Status",
"Street": "Street",
"Tentative: Will be confirmed later": "Tentative: Will be confirmed later",
"The event organizer didn't add any description.": "The event organizer didn't add any description.",
"The page you're looking for doesn't exist.": "The page you're looking for doesn't exist.",
"The {date} at {time}": "The {date} at {time}",
"The {date} from {startTime} to {endTime}": "The {date} from {startTime} to {endTime}",
"There are {participants} participants.": "There's only one participant | There are {participants} participants.",
"These events may interest you": "These events may interest you",
"This instance isn't opened to registrations, but you can register on other instances.": "This instance isn't opened to registrations, but you can register on other instances.",
"This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.",
"Title": "Title",
"To confirm, type your event title \"{eventTitle}\"": "To confirm, type your event title \"{eventTitle}\"",
"To confirm, type your identity username \"{preferredUsername}\"": "To confirm, type your identity username \"{preferredUsername}\"",
"Unknown error.": "Unknown error.",
"Update event {name}": "Update event {name}",
"Update my event": "Update my event",
"User logout": "User logout",
"Username": "Username",
"Users": "Users",
"Visible everywhere on the web (public)": "Visible everywhere on the web (public)",
"We just sent an email to {email}": "We just sent an email to {email}",
"Website / URL": "Website / URL",
"Welcome back {username}": "Welcome back {username}",
"Welcome back!": "Welcome back!",
"Welcome on your administration panel": "Welcome on your administration panel",
"Who can view this event and participate": "Who can view this event and participate",
"World map": "World map",
"You and one other person are going to this event": "You're the only one going to this event | You and one other person are going to this event | You and {approved} persons are going to this event.",
"You announced that you're going to this event.": "You announced that you're going to this event.",
"You are already logged-in.": "You are already logged-in.",
"You are an organizer.": "You are an organizer.",
"You have one event in {days} days.": "You have no events in {days} days | You have one event in {days} days. | You have {count} events in {days} days",
"You have one event today.": "You have no events today | You have one event today. | You have {count} events today",
"You have one event tomorrow.": "You have no events tomorrow | You have one event tomorrow. | You have {count} events tomorrow",
"You need to login.": "You need to login.",
"You're not going to any event yet": "You're not going to any event yet",
"Your account has been validated": "Your account has been validated",
"Your account is being validated": "Your account is being validated",
"Your account is nearly ready, {username}": "Your account is nearly ready, {username}",
"Your local administrator resumed it's policy:": "Your local administrator resumed it's policy:",
"e.g. 10 Rue Jangot": "e.g. 10 Rue Jangot",
"iCal Feed": "iCal Feed",
"meditate a bit": "meditate a bit",
"public event": "public event",
"{actor}'s avatar": "{actor}'s avatar",
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
}

200
js/src/i18n/fr_FR.json Normal file
View File

@ -0,0 +1,200 @@
{
"A validation email was sent to {email}": "Un email de validation a été envoyé à {email}",
"About this event": "À propos de cet événement",
"About this instance": "À propos de cette instance",
"About": "À propos",
"Add a new profile": "Ajouter un nouveau profil",
"Add a tag": "Ajouter un tag",
"Add an address": "Ajouter une adresse",
"Add to my calendar": "Ajouter à mon agenda",
"Add": "Ajouter",
"Administration": "Administration",
"Allow all comments": "Autoriser tous les commentaires",
"Are you going to this event?": "Allez-vous à cet événement ?",
"Are you sure you want to delete this event? This action cannot be reverted.": "Êtes-vous certain⋅e de vouloir supprimer cet événement ? Cette action ne peut être annulée.",
"Before you can login, you need to click on the link inside it to validate your account": "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte",
"By {name}": "Par {name}",
"Category": "Catégorie",
"Change": "Modifier",
"Clear": "Effacer",
"Click to select": "Cliquez pour sélectionner",
"Click to upload": "Cliquez pour uploader",
"Close comments for all (except for admins)": "Fermer les commentaires à tout le monde (excepté les administrateurs)",
"Comments on the event page": "Commentaires sur la page de l'événement",
"Comments": "Commentaires",
"Confirmed: Will happen": "Confirmé : aura lieu",
"Country": "Pays",
"Create a new event": "Créer un nouvel événement",
"Create a new group": "Créer un nouveau groupe",
"Create a new identity": "Créer une nouvelle identité",
"Create group": "Créer un groupe",
"Create my event": "Créer mon événement",
"Create my group": "Créer mon groupe",
"Create my profile": "Créer mon profil",
"Create token": "Créer un jeton",
"Create your communities and your events": "Créer vos communautés et vos événements",
"Create": "Créer",
"Current": "Actuel",
"Delete event": "Supprimer un événement",
"Delete this identity": "Supprimer cette identité",
"Delete your identity": "Supprimer votre identité",
"Delete {eventTitle}": "Supprimer {eventTitle}",
"Delete {preferredUsername}": "Supprimer {preferredUsername}",
"Delete": "Supprimer",
"Description": "Description",
"Didn't receive the instructions ?": "Vous n'avez pas reçu les instructions ?",
"Disallow promoting on Mobilizon": "Refuser la mise en avant sur Mobilizon",
"Display name": "Nom affiché",
"Display participation price": "Afficher un prix de participation",
"Displayed name": "Nom affiché",
"Edit": "Éditer",
"Either the account is already validated, either the validation token is incorrect.": "Soit le compte est déjà validé, soit le jeton de validation est incorrect.",
"Email": "Email",
"Ends on…": "Se termine le…",
"Enter some tags": "Écrire des tags",
"Error while validating account": "Erreur lors de la validation du compte",
"Event list": "Liste d'événements",
"Event {eventTitle} deleted": "Événement {eventTitle} supprimé",
"Event {eventTitle} reported": "Événement {eventTitle} signalé",
"Event": "Événement",
"Events nearby you": "Événements près de chez vous",
"Events you're going at": "Événements auxquels vous vous rendez",
"Events": "Événements",
"Features": "Fonctionnalités",
"Find an address": "Trouver une adresse",
"Forgot your password ?": "Mot de passe oublié ?",
"From the {startDate} at {startTime} to the {endDate} at {endTime}": "Du {startDate} à {startTime} au {endDate} à {endTime}",
"General information": "Information générales",
"Group List": "Liste de groupes",
"Group full name": "Nom complet du groupe",
"Group name": "Nom du groupe",
"Group {displayName} created": "Groupe {displayName} créé",
"Group": "Groupe",
"Groups": "Groupes",
"I create an identity": "Je crée une identité",
"I want to approve every participation request": "Je veux approuver chaque demande de participation",
"Identities": "Identités",
"Identity {displayName} created": "Identité {displayName} créée",
"Identity {displayName} deleted": "Identité {displayName} supprimée",
"Identity {displayName} updated": "Identité {displayName} mise à jour",
"Identity": "Identité",
"If an account with this email exists, we just sent another confirmation email to {email}": "Si un compte avec un tel email existe, nous venons juste d'envoyer un nouvel email de confirmation à {email}",
"If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity.": "Si cette identité est la seule administratrice de certains groupes, vous devez les supprimer avant de pouvoir supprimer cette identité.",
"Join": "Rejoindre",
"Last published event": "Dernier événement publié",
"Learn more on {0}": "En apprendre plus sur {0}",
"Learn more on": "En apprendre plus sur",
"Leave": "Quitter",
"Legal": "Mentions légales",
"License": "Licence",
"Limited places": "Places limitées",
"Loading…": "Chargement en cours…",
"Locality": "Commune",
"Log in": "Se connecter",
"Log out": "Se déconnecter",
"Login": "Se connecter",
"Members": "Membres",
"Moderated comments (shown after approval)": "Commentaires modérés (affichés après validation)",
"My account": "Mon compte",
"My identities": "Mes identités",
"Name": "Nom",
"No address defined": "Aucune adresse définie",
"No events found": "Aucun événement trouvé",
"No group found": "Aucun groupe trouvé",
"No groups found": "Aucun groupe trouvé",
"No results for \"{queryText}\"": "Pas de résultats pour « {queryText} »",
"Number of places": "Nombre de places",
"One person is going": "Personne n'y va | Une personne y va | {approved} personnes y vont",
"Only accessible through link and search (private)": "Uniquement accessibles par lien et la recherche (privé)",
"Opened reports": "Signalements ouverts",
"Organized": "Organisés",
"Organizer": "Organisateur",
"Other stuff…": "Autres trucs…",
"Otherwise this identity will just be removed from the group administrators.": "Sinon cette identité sera juste supprimée des administrateurs du groupe.",
"Page limited to my group (asks for auth)": "Accès limité à mon groupe (demande authentification)",
"Participation approval": "Validation des participations",
"Password reset": "Réinitialisation du mot de passe",
"Password": "Mot de passe",
"Pick an identity": "Choisissez une identité",
"Please be nice to each other": "Soyez sympas entre vous",
"Please check you spam folder if you didn't receive the email.": "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email.",
"Please contact this instance's Mobilizon admin if you think this is a mistake.": "Veuillez contacter l'administrateur de cette instance Mobilizon si vous pensez quil sagit dune erreur.",
"Please make sure the address is correct and that the page hasn't been moved.": "Assurezvous que ladresse est correcte et que la page na pas été déplacée.",
"Please read the full rules": "Merci de lire les règles complètes",
"Please type at least 5 characters": "Merci d'entrer au moins 5 caractères",
"Postal Code": "Code postal",
"Private feeds": "Flux privés",
"Promotion": "Mise en avant",
"Public RSS/Atom Feed": "Flux RSS/Atom public",
"Public comment moderation": "Modération des commentaires publics",
"Public feeds": "Flux publics",
"Public iCal Feed": "Flux iCal public",
"Published events": "Événements publiés",
"RSS/Atom Feed": "Flux RSS/Atom",
"Region": "Région",
"Register an account on Mobilizon!": "S'inscrire sur Mobilizon !",
"Register": "S'inscrire",
"Registration is currently closed.": "Les inscriptions sont actuellement fermées.",
"Report": "Report",
"Resend confirmation email": "Envoyer à nouveau l'email de confirmation",
"Reset my password": "Réinitialiser mon mot de passe",
"Save": "Enregistrer",
"Search events, groups, etc.": "Rechercher des événements, des groupes, etc.",
"Search results: \"{search}\"": "Résultats de recherche: « {search} »",
"Search": "Rechercher",
"Searching…": "Recherche en cours…",
"Send confirmation email again": "Envoyer l'email de confirmation à nouveau",
"Send email to reset my password": "Envoyer un email pour réinitialiser mon mot de passe",
"Share this event": "Partager l'événement",
"Show map": "Afficher la carte",
"Show remaining number of places": "Afficher le nombre de places restantes",
"Sign up": "S'enregistrer",
"Starts on…": "Débute le…",
"Status": "Statut",
"Street": "Rue",
"Tentative: Will be confirmed later": "Provisoire : sera confirmé plus tard",
"The event organizer didn't add any description.": "L'organisateur de l'événement n'a pas ajouté de description.",
"The page you're looking for doesn't exist.": "La page que vous recherchez n'existe pas.",
"The {date} at {time}": "Le {date} à {time}",
"The {date} from {startTime} to {endTime}": "Le {date} de {startTime} à {endTime}",
"There are {participants} participants.": "Il n'y a qu'un⋅e participant⋅e. | Il y a {participants} participants.",
"These events may interest you": "Ces événements peuvent vous intéresser",
"This instance isn't opened to registrations, but you can register on other instances.": "Cette instance n'autorise pas les inscriptions, mais vous pouvez vous enregistrer sur d'autres instances.",
"This will delete / anonymize all content (events, comments, messages, participations…) created from this identity.": "Cela supprimera / anonymisera tout le contenu (événements, commentaires, messages, participations…) créés avec cette identité.",
"Title": "Titre",
"To confirm, type your event title \"{eventTitle}\"": "Pour confirmer, entrez le titre de l'événement « {eventTitle} »",
"To confirm, type your identity username \"{preferredUsername}\"": "Pour confirmer, entrez le nom de lidentité « {preferredUsername} »",
"Unknown error.": "Erreur inconnue.",
"Update event {name}": "Éditer l'événement {name}",
"Update my event": "Éditer mon événement",
"User logout": "Déconnexion",
"Username": "Pseudo",
"Users": "Utilisateurs",
"Visible everywhere on the web (public)": "Visible partout sur le web (public)",
"We just sent an email to {email}": "Nous venons d'envoyer un email à {email}",
"Website / URL": "Site web / URL",
"Welcome back {username}": "Bon retour {username}",
"Welcome back!": "Bon retour !",
"Welcome on your administration panel": "Bienvenue sur votre espace d'administration",
"Who can view this event and participate": "Qui peut voir cet événement et y participer",
"World map": "Carte mondiale",
"You and one other person are going to this event": "Vous êtes le ou la seule à vous rendre à cet événement | Vous et une autre personne vous rendez à cet événement | Vous et {approved} autres personnes vous rendez à cet événement.",
"You announced that you're going to this event.": "Vous avez annoncé vous rendre à cet événement.",
"You are already logged-in.": "Vous êtes déjà connecté.",
"You are an organizer.": "Vous êtes un organisateur.",
"You have one event in {days} days.": "Vous n'avez pas d'événements dans {days} jours | Vous avez un événement dans {days} jours. | Vous avez {count} événements dans {days} jours",
"You have one event today.": "Vous n'avez pas d'évenement aujourd'hui | Vous avez un événement aujourd'hui. | Vous avez {count} événements aujourd'hui",
"You have one event tomorrow.": "Vous n'avez pas d'événement demain | Vous avez un événement demain. | Vous avez {count} événements demain",
"You need to login.": "Vous devez vous connecter.",
"You're not going to any event yet": "Vous n'allez à aucun événement pour le moment",
"Your account has been validated": "Votre compte a été validé",
"Your account is being validated": "Votre compte est en cours de validation",
"Your account is nearly ready, {username}": "Votre compte est presque prêt, {username}",
"Your local administrator resumed it's policy:": "Votre administrateur local a résumé sa politique ainsi :",
"e.g. 10 Rue Jangot": "par exemple : 10 Rue Jangot",
"iCal Feed": "Flux iCal",
"meditate a bit": "méditez un peu",
"public event": "événement public",
"{actor}'s avatar": "Avatar de {actor}",
"© The Mobilizon Contributors {date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks": "© Les contributeurs de Mobilizon {date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines"
}

7
js/src/i18n/index.js Normal file
View File

@ -0,0 +1,7 @@
import en_US from './en_US';
import fr_FR from './fr_FR';
export default {
en_US,
fr_FR
}

View File

@ -1,439 +0,0 @@
# English translations for mobilizon package.
# Copyright (C) 2019 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2019.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-12 16:47+0200\n"
"PO-Revision-Date: 2019-04-08 20:58+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en_US\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/components/Footer.vue:10
msgid "© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
msgstr "© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
#: src/views/Account/Register.vue:57
msgid "A validation email was sent to %{email}"
msgstr "A validation email was sent to %{email}"
#: src/components/Footer.vue:5
msgid "About"
msgstr "About"
#: src/views/Event/Event.vue:137
msgid "About this event"
msgstr "About this event"
#: src/views/User/Register.vue:26
msgid "About this instance"
msgstr "About this instance"
#: src/views/Account/Identities.vue:7
msgid "Add a new profile"
msgstr "Add a new profile"
#: src/views/Event/Event.vue:44 src/views/Event/Event.vue:216
msgid "Add to my calendar"
msgstr "Add to my calendar"
#: src/views/Event/Event.vue:2
msgid "Are you going to this event?"
msgstr "Are you going to this event?"
#: src/views/Account/Register.vue:60
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr "Before you can login, you need to click on the link inside it to validate your account"
#: src/views/Event/Event.vue:100
msgid "By %{ name }"
msgstr "By %{ name }"
#: src/views/Event/Create.vue:3
msgid "Create a new event"
msgstr "Create a new event"
#: src/views/Group/Create.vue:3
msgid "Create a new group"
msgstr "Create a new group"
#: src/views/Group/GroupList.vue:15
msgid "Create group"
msgstr "Create group"
#: src/views/Event/Create.vue:25
msgid "Create my event"
msgstr "Create my event"
#: src/views/Group/Create.vue:20
msgid "Create my group"
msgstr "Create my group"
#: src/views/Account/Register.vue:43
msgid "Create my profile"
msgstr "Create my profile"
#: src/views/Account/Profile.vue:61
msgid "Create token"
msgstr "Create token"
#: src/views/User/Register.vue:16
msgid "Create your communities and your events"
msgstr "Create your communities and your events"
#: src/views/Account/Identities.vue:36
msgid "Current"
msgstr "Current"
#: src/views/Account/Profile.vue:93 src/views/Event/Event.vue:63
msgid "Delete"
msgstr "Delete"
#: src/views/User/Register.vue:82
msgid "Didn't receive the instructions ?"
msgstr "Didn't receive the instructions ?"
#: src/views/Event/Event.vue:58
msgid "Edit"
msgstr "Edit"
#: src/views/User/Validate.vue:8
msgid "Either the account is already validated, either the validation token is incorrect."
msgstr "Either the account is already validated, either the validation token is incorrect."
#: src/views/Event/EventList.vue:3
msgid "Event list"
msgstr "Event list"
#: src/views/Search.vue:10
msgid "Events"
msgstr "Events"
#: src/views/Home.vue:68
msgid "Events nearby you"
msgstr "Events nearby you"
#: src/views/Home.vue:24
msgid "Events you're going at"
msgstr "Events you're going at"
#: src/views/User/Register.vue:14
msgid "Features"
msgstr "Features"
#: src/views/User/Login.vue:41
msgid "Forgot your password ?"
msgstr "Forgot your password ?"
#: src/components/Event/EventFullDate.vue:9
msgid "From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }"
msgstr "From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }"
#: src/views/Group/GroupList.vue:3
msgid "Group List"
msgstr "Group List"
#: src/views/Search.vue:28
msgid "Groups"
msgstr "Groups"
#: src/views/Account/Profile.vue:56
msgid "iCal Feed"
msgstr "iCal Feed"
#: src/views/Account/Identities.vue:4
msgid "Identities"
msgstr "Identities"
#: src/views/User/ResendConfirmation.vue:16
msgid "If an account with this email exists, we just sent another confirmation email to %{email}"
msgstr "If an account with this email exists, we just sent another confirmation email to %{email}"
#: src/views/Event/Event.vue:20
msgid "Join"
msgstr "Join"
#: src/views/User/Register.vue:20
msgid ""
"Learn more on\n"
" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>"
msgstr ""
"Learn more on\n"
" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>"
#: src/views/Event/Event.vue:24
msgid "Leave"
msgstr "Leave"
#: src/components/Footer.vue:7
msgid "Legal"
msgstr "Legal"
#: src/components/Footer.vue:6
msgid "License"
msgstr "License"
#: src/components/NavBar.vue:32
msgid "Log in"
msgstr "Log in"
#: src/components/NavBar.vue:50
msgid "Log out"
msgstr "Log out"
#: src/views/User/Login.vue:33 src/views/User/Register.vue:91
msgid "Login"
msgstr "Login"
#: src/views/User/Register.vue:32
msgid "meditate a bit"
msgstr "meditate a bit"
#: src/views/Group/Group.vue:41
msgid "Members"
msgstr "Members"
#: src/components/NavBar.vue:49
msgid "My account"
msgstr "My account"
#: src/views/Event/Event.vue:69
msgid "No address defined"
msgstr "No address defined"
#: src/views/Event/EventList.vue:15 src/views/Home.vue:78
#: src/views/Search.vue:22
msgid "No events found"
msgstr "No events found"
#: src/views/Group/Group.vue:52
msgid "No group found"
msgstr "No group found"
#: src/views/Search.vue:38
msgid "No groups found"
msgstr "No groups found"
#: src/views/Account/Profile.vue:66 src/views/Group/Group.vue:27
msgid "Organized"
msgstr "Organized"
#: src/components/Event/EventCard.vue:1
msgid "Organizer"
msgstr "Organizer"
#: src/views/User/Register.vue:17
msgid "Other stuff…"
msgstr "Other stuff…"
#: src/views/User/PasswordReset.vue:4 src/views/User/SendPasswordReset.vue:4
msgid "Password reset"
msgstr "Password reset"
#: src/views/User/Register.vue:31
msgid "Please be nice to each other"
msgstr "Please be nice to each other"
#: src/views/User/ResendConfirmation.vue:21
#: src/views/User/SendPasswordReset.vue:22
msgid "Please check you spam folder if you didn't receive the email."
msgstr "Please check you spam folder if you didn't receive the email."
#: src/views/PageNotFound.vue:12
msgid "Please contact this instance's Mobilizon admin if you think this is a mistake."
msgstr ""
#: src/views/PageNotFound.vue:9
msgid "Please make sure the address is correct and that the page hasn't been moved."
msgstr ""
#: src/views/User/Register.vue:35
msgid "Please read the full rules"
msgstr "Please read the full rules"
#: src/views/Account/Profile.vue:45
msgid "Private feeds"
msgstr "Private feeds"
#: src/views/Event/Event.vue:34
msgid "public event"
msgstr "public event"
#: src/views/Account/Profile.vue:27
msgid "Public feeds"
msgstr "Public feeds"
#: src/views/Account/Profile.vue:38
msgid "Public iCal Feed"
msgstr "Public iCal Feed"
#: src/views/Account/Profile.vue:33
msgid "Public RSS/Atom Feed"
msgstr "Public RSS/Atom Feed"
#: src/views/Account/Identities.vue:16 src/views/Home.vue:8
#: src/views/User/Login.vue:49 src/views/User/Register.vue:74
msgid "Register"
msgstr "Register"
#: src/views/Account/Register.vue:5 src/views/User/Register.vue:5
msgid "Register an account on Mobilizon!"
msgstr "Register an account on Mobilizon!"
#: src/views/Error.vue:2
msgid "Registration is currently closed."
msgstr "Registration is currently closed."
#: src/views/User/ResendConfirmation.vue:4
msgid "Resend confirmation email"
msgstr "Resend confirmation email"
#: src/views/User/PasswordReset.vue:29
msgid "Reset my password"
msgstr "Reset my password"
#: src/views/Account/Profile.vue:51
msgid "RSS/Atom Feed"
msgstr "RSS/Atom Feed"
#: src/views/PageNotFound.vue:19 src/components/SearchField.vue:19
msgid "Search"
msgstr "Search"
#: src/views/Search.vue:3
msgid "Search results: « %{ search } »"
msgstr "Search results: « %{ search } »"
#: src/views/User/ResendConfirmation.vue:11
msgid "Send confirmation email again"
msgstr "Send confirmation email again"
#: src/views/User/SendPasswordReset.vue:12
msgid "Send email to reset my password"
msgstr "Send email to reset my password"
#: src/views/Event/Event.vue:205
msgid "Share this event"
msgstr "Share this event"
#: src/views/Event/Event.vue:78
msgid "Show map"
msgstr "Show map"
#: src/components/NavBar.vue:28
msgid "Sign up"
msgstr "Sign up"
#: src/components/Event/EventFullDate.vue:1
msgid "The %{ date } at %{ time }"
msgstr "The %{ date } at %{ time }"
#: src/components/Event/EventFullDate.vue:5
msgid "The %{ date } from %{ startTime } to %{ endTime }"
msgstr "The %{ date } from %{ startTime } to %{ endTime }"
#: src/views/Event/Event.vue:140
msgid "The event organizer didn't add any description."
msgstr "The event organizer didn't add any description."
#: src/views/PageNotFound.vue:6
msgid "The page you're looking for doesn't exist."
msgstr ""
#: src/views/Event/Event.vue:223
msgid "These events may interest you"
msgstr "These events may interest you"
#: src/views/Home.vue:11
msgid "This instance isn't opened to registrations, but you can register on other instances."
msgstr "This instance isn't opened to registrations, but you can register on other instances."
#: src/views/Error.vue:6
msgid "Unknown error."
msgstr "Unknown error."
#: src/views/Account/Profile.vue:84
msgid "User logout"
msgstr "User logout"
#: src/views/User/SendPasswordReset.vue:17
msgid "We just sent an email to %{email}"
msgstr "We just sent an email to %{email}"
#: src/views/Home.vue:18
msgid "Welcome back %{username}"
msgstr "Welcome back %{username}"
#: src/views/User/Login.vue:4
msgid "Welcome back!"
msgstr "Welcome back!"
#: src/views/Event/Event.vue:2
msgid "You announced that you're going to this event."
msgstr "You announced that you're going to this event."
#: src/views/User/Login.vue:58
msgid "You are already logged-in."
msgstr "You are already logged-in."
#: src/views/Event/Event.vue:2
msgid "You are an organizer."
msgstr "You are an organizer."
#: src/views/Home.vue:45
msgid "You have one event in %{ days } days."
msgid_plural "You have %{ count } events in %{ days } days"
msgstr[0] "You have one event in %{ days } days."
msgstr[1] "You have %{ count } events in %{ days } days"
#: src/views/Home.vue:29
msgid "You have one event today."
msgid_plural "You have %{ count } events today"
msgstr[0] "You have one event today."
msgstr[1] "You have %{ count } events today"
#: src/views/Home.vue:37
msgid "You have one event tomorrow."
msgid_plural "You have %{ count } events tomorrow"
msgstr[0] "You have one event tomorrow."
msgstr[1] "You have %{ count } events tomorrow"
#: src/views/User/Login.vue:9
msgid "You need to login."
msgstr "You need to login."
#: src/views/Home.vue:64
msgid "You're not going to any event yet"
msgstr "You're not going to any event yet"
#: src/views/User/Validate.vue:12
msgid "Your account has been validated"
msgstr "Your account has been validated"
#: src/views/User/Validate.vue:3
msgid "Your account is being validated"
msgstr "Your account is being validated"
#: src/views/Account/Register.vue:52
msgid "Your account is nearly ready, %{username}"
msgstr "Your account is nearly ready, %{username}"
#: src/views/User/Register.vue:28
msgid "Your local administrator resumed it's policy:"
msgstr "Your local administrator resumed it's policy:"
#: src/components/Footer.vue:4
msgid "World map"
msgstr "World map"
#: src/views/PageNotFound.vue:42
msgid "Search events, groups, etc."
msgstr ""

View File

@ -1,440 +0,0 @@
# French translations for mobilizon package.
# Copyright (C) 2018 THE mobilizon'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mobilizon package.
# Automatically generated, 2018.
#
msgid ""
msgstr ""
"Project-Id-Version: mobilizon 0.1.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-04-12 16:47+0200\n"
"PO-Revision-Date: 2019-04-12 16:45+0200\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: fr_FR\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 2.2.1\n"
#: src/components/Footer.vue:10
msgid "© The Mobilizon Contributors %{date} - Made with Elixir, Phoenix, VueJS & with some love and some weeks"
msgstr "© Les contributeurs de Mobilizon %{date} - Fait avec Elixir, Phoenix, VueJS & et de l'amour et des semaines"
#: src/views/Account/Register.vue:57
msgid "A validation email was sent to %{email}"
msgstr "Un email de validation a été envoyé à %{email}"
#: src/components/Footer.vue:5
msgid "About"
msgstr "À propos"
#: src/views/Event/Event.vue:137
msgid "About this event"
msgstr "À propos de cet événement"
#: src/views/User/Register.vue:26
msgid "About this instance"
msgstr "À propos de cette instance"
#: src/views/Account/Identities.vue:7
msgid "Add a new profile"
msgstr "Ajouter un nouveau profil"
#: src/views/Event/Event.vue:44 src/views/Event/Event.vue:216
msgid "Add to my calendar"
msgstr "Ajouter à mon agenda"
#: src/views/Event/Event.vue:2
msgid "Are you going to this event?"
msgstr "Allez-vous à cet événement ?"
#: src/views/Account/Register.vue:60
msgid "Before you can login, you need to click on the link inside it to validate your account"
msgstr "Avant que vous puissiez vous enregistrer, vous devez cliquer sur le lien à l'intérieur pour valider votre compte"
#: src/views/Event/Event.vue:100
msgid "By %{ name }"
msgstr "Par %{name}"
#: src/views/Event/Create.vue:3
msgid "Create a new event"
msgstr "Créer un nouvel événement"
#: src/views/Group/Create.vue:3
msgid "Create a new group"
msgstr "Créer un nouveau groupe"
#: src/views/Group/GroupList.vue:15
msgid "Create group"
msgstr "Créer un groupe"
#: src/views/Event/Create.vue:25
msgid "Create my event"
msgstr "Créer mon événement"
#: src/views/Group/Create.vue:20
msgid "Create my group"
msgstr "Créer mon groupe"
#: src/views/Account/Register.vue:43
msgid "Create my profile"
msgstr "Créer mon profil"
#: src/views/Account/Profile.vue:61
msgid "Create token"
msgstr "Créer un jeton"
#: src/views/User/Register.vue:16
msgid "Create your communities and your events"
msgstr "Créer vos communautés et vos événements"
#: src/views/Account/Identities.vue:36
msgid "Current"
msgstr "Actuel"
#: src/views/Account/Profile.vue:93 src/views/Event/Event.vue:63
msgid "Delete"
msgstr "Supprimer"
#: src/views/User/Register.vue:82
msgid "Didn't receive the instructions ?"
msgstr "Vous n'avez pas reçu les instructions ?"
#: src/views/Event/Event.vue:58
msgid "Edit"
msgstr "Éditer"
#: src/views/User/Validate.vue:8
msgid "Either the account is already validated, either the validation token is incorrect."
msgstr "Soit le compte est déjà validé, soit le jeton de validation est incorrect."
#: src/views/Event/EventList.vue:3
msgid "Event list"
msgstr "Liste d'événements"
#: src/views/Search.vue:10
msgid "Events"
msgstr "Événements"
#: src/views/Home.vue:68
msgid "Events nearby you"
msgstr "Événements près de chez vous"
#: src/views/Home.vue:24
msgid "Events you're going at"
msgstr "Événements auxquels vous vous rendez"
#: src/views/User/Register.vue:14
msgid "Features"
msgstr "Fonctionnalités"
#: src/views/User/Login.vue:41
msgid "Forgot your password ?"
msgstr "Mot de passe oublié ?"
#: src/components/Event/EventFullDate.vue:9
msgid "From the %{ startDate } at %{ startTime } to the %{ endDate } at %{ endTime }"
msgstr "Du %{ startDate } à %{ startTime } au %{ endDate } à %{ endTime }"
#: src/views/Group/GroupList.vue:3
msgid "Group List"
msgstr "Liste de groupes"
#: src/views/Search.vue:28
msgid "Groups"
msgstr "Groupes"
#: src/views/Account/Profile.vue:56
msgid "iCal Feed"
msgstr "Flux iCal"
#: src/views/Account/Identities.vue:4
msgid "Identities"
msgstr "Identités"
#: src/views/User/ResendConfirmation.vue:16
msgid "If an account with this email exists, we just sent another confirmation email to %{email}"
msgstr "Si un compte avec un tel email existe, nous venons juste d'envoyer un nouvel email de confirmation à %{email}"
#: src/views/Event/Event.vue:20
msgid "Join"
msgstr "Rejoindre"
#: src/views/User/Register.vue:20
msgid ""
"Learn more on\n"
" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>"
msgstr ""
"En apprendre plus sur\n"
" <a target=\"_blank\" href=\"https://joinmobilizon.org\">joinmobilizon.org</a>"
#: src/views/Event/Event.vue:24
msgid "Leave"
msgstr "Quitter"
#: src/components/Footer.vue:7
msgid "Legal"
msgstr "Mentions légales"
#: src/components/Footer.vue:6
msgid "License"
msgstr "License"
#: src/components/NavBar.vue:32
msgid "Log in"
msgstr "Se connecter"
#: src/components/NavBar.vue:50
msgid "Log out"
msgstr "Se déconnecter"
#: src/views/User/Login.vue:33 src/views/User/Register.vue:91
msgid "Login"
msgstr "Se connecter"
#: src/views/User/Register.vue:32
msgid "meditate a bit"
msgstr "méditez un peu"
#: src/views/Group/Group.vue:41
msgid "Members"
msgstr "Membres"
#: src/components/NavBar.vue:49
msgid "My account"
msgstr "Mon compte"
#: src/views/Event/Event.vue:69
msgid "No address defined"
msgstr "Aucune adresse définie"
#: src/views/Event/EventList.vue:15 src/views/Home.vue:78
#: src/views/Search.vue:22
msgid "No events found"
msgstr "Aucun événement trouvé"
#: src/views/Group/Group.vue:52
msgid "No group found"
msgstr "Aucun groupe trouvé"
#: src/views/Search.vue:38
msgid "No groups found"
msgstr "Aucun groupe trouvé"
#: src/views/Account/Profile.vue:66 src/views/Group/Group.vue:27
msgid "Organized"
msgstr "Organisés"
#: src/components/Event/EventCard.vue:1
msgid "Organizer"
msgstr "Organisateur"
#: src/views/User/Register.vue:17
msgid "Other stuff…"
msgstr "Autres trucs…"
#: src/views/User/PasswordReset.vue:4 src/views/User/SendPasswordReset.vue:4
msgid "Password reset"
msgstr "Réinitialisation du mot de passe"
#: src/views/User/Register.vue:31
msgid "Please be nice to each other"
msgstr "Soyez sympas entre vous"
#: src/views/User/ResendConfirmation.vue:21
#: src/views/User/SendPasswordReset.vue:22
msgid "Please check you spam folder if you didn't receive the email."
msgstr "Merci de vérifier votre dossier des indésirables si vous n'avez pas reçu l'email."
#: src/views/PageNotFound.vue:12
msgid "Please contact this instance's Mobilizon admin if you think this is a mistake."
msgstr "Veuillez contacter l'administrateur de cette instance Mobilizon si vous pensez quil sagit dune erreur."
#: src/views/PageNotFound.vue:9
msgid "Please make sure the address is correct and that the page hasn't been moved."
msgstr "Assurezvous que ladresse est correcte et que la page na pas été déplacée."
#: src/views/User/Register.vue:35
msgid "Please read the full rules"
msgstr "Merci de lire les règles complètes"
#: src/views/Account/Profile.vue:45
msgid "Private feeds"
msgstr "Flux privés"
#: src/views/Event/Event.vue:34
msgid "public event"
msgstr "événement public"
#: src/views/Account/Profile.vue:27
msgid "Public feeds"
msgstr "Flux publics"
#: src/views/Account/Profile.vue:38
msgid "Public iCal Feed"
msgstr "Flux iCal public"
#: src/views/Account/Profile.vue:33
msgid "Public RSS/Atom Feed"
msgstr "Flux RSS/Atom public"
#: src/views/Account/Identities.vue:16 src/views/Home.vue:8
#: src/views/User/Login.vue:49 src/views/User/Register.vue:74
msgid "Register"
msgstr "S'inscrire"
#: src/views/Account/Register.vue:5 src/views/User/Register.vue:5
msgid "Register an account on Mobilizon!"
msgstr "S'inscrire sur Mobilizon !"
#: src/views/Error.vue:2
msgid "Registration is currently closed."
msgstr "Les inscriptions sont actuellement fermées."
#: src/views/User/ResendConfirmation.vue:4
msgid "Resend confirmation email"
msgstr "Envoyer à nouveau l'email de confirmation"
#: src/views/User/PasswordReset.vue:29
msgid "Reset my password"
msgstr "Réinitialiser mon mot de passe"
#: src/views/Account/Profile.vue:51
msgid "RSS/Atom Feed"
msgstr "Flux RSS/Atom"
#: src/views/PageNotFound.vue:19 src/components/SearchField.vue:19
msgid "Search"
msgstr "Rechercher"
#: src/views/Search.vue:3
msgid "Search results: « %{ search } »"
msgstr "Résultats de recherche : « %{ search } »"
#: src/views/User/ResendConfirmation.vue:11
msgid "Send confirmation email again"
msgstr "Envoyer l'email de confirmation à nouveau"
#: src/views/User/SendPasswordReset.vue:12
msgid "Send email to reset my password"
msgstr "Envoyer un email pour réinitialiser mon mot de passe"
#: src/views/Event/Event.vue:205
msgid "Share this event"
msgstr "Partager l'événement"
#: src/views/Event/Event.vue:78
msgid "Show map"
msgstr "Afficher la carte"
#: src/components/NavBar.vue:28
msgid "Sign up"
msgstr "S'enregistrer"
#: src/components/Event/EventFullDate.vue:1
msgid "The %{ date } at %{ time }"
msgstr "Le %{ date } à %{ time }"
#: src/components/Event/EventFullDate.vue:5
msgid "The %{ date } from %{ startTime } to %{ endTime }"
msgstr "Le %{ date } de %{ startTime } à %{ endTime }"
#: src/views/Event/Event.vue:140
msgid "The event organizer didn't add any description."
msgstr "L'organisateur de l'événement n'a pas ajouté de description."
#: src/views/PageNotFound.vue:6
msgid "The page you're looking for doesn't exist."
msgstr "La page que vous recherchez n'existe pas."
#: src/views/Event/Event.vue:223
msgid "These events may interest you"
msgstr "Ces événements peuvent vous intéresser"
#: src/views/Home.vue:11
msgid "This instance isn't opened to registrations, but you can register on other instances."
msgstr "Cette instance n'autorise pas les inscriptions, mais vous pouvez vous enregistrer sur d'autres instances."
#: src/views/Error.vue:6
msgid "Unknown error."
msgstr "Erreur inconnue."
#: src/views/Account/Profile.vue:84
msgid "User logout"
msgstr "Déconnexion"
#: src/views/User/SendPasswordReset.vue:17
msgid "We just sent an email to %{email}"
msgstr "Nous venons d'envoyer un email à %{email}"
#: src/views/Home.vue:18
msgid "Welcome back %{username}"
msgstr "Bon retour %{username}"
#: src/views/User/Login.vue:4
msgid "Welcome back!"
msgstr "Bon retour !"
#: src/views/Event/Event.vue:2
msgid "You announced that you're going to this event."
msgstr "Vous avez annoncé vous rendre à cet événement."
#: src/views/User/Login.vue:58
msgid "You are already logged-in."
msgstr "Vous êtes déjà connecté."
#: src/views/Event/Event.vue:2
msgid "You are an organizer."
msgstr "Vous êtes un organisateur."
#: src/views/Home.vue:45
msgid "You have one event in %{ days } days."
msgid_plural "You have %{ count } events in %{ days } days"
msgstr[0] "Vous avez un événement dans %{ days } jours."
msgstr[1] "Vous avez %{ count } événements dans %{ days } jours"
#: src/views/Home.vue:29
msgid "You have one event today."
msgid_plural "You have %{ count } events today"
msgstr[0] "Vous avez un événement aujourd'hui."
msgstr[1] "Vous avez %{ count } événements aujourd'hui"
#: src/views/Home.vue:37
msgid "You have one event tomorrow."
msgid_plural "You have %{ count } events tomorrow"
msgstr[0] "Vous avez un événement demain."
msgstr[1] "Vous avez %{ count } événements demain"
#: src/views/User/Login.vue:9
msgid "You need to login."
msgstr "Vous devez vous connecter."
#: src/views/Home.vue:64
msgid "You're not going to any event yet"
msgstr "Vous n'allez à aucun événement pour le moment"
#: src/views/User/Validate.vue:12
msgid "Your account has been validated"
msgstr "Votre compte a été validé"
#: src/views/User/Validate.vue:3
msgid "Your account is being validated"
msgstr "Votre compte est en cours de validation"
#: src/views/Account/Register.vue:52
msgid "Your account is nearly ready, %{username}"
msgstr "Votre compte est presque prêt, %{ username }"
#: src/views/User/Register.vue:28
msgid "Your local administrator resumed it's policy:"
msgstr "Votre administrateur local a résumé sa politique ainsi :"
#: src/components/Footer.vue:4
msgid "World map"
msgstr "Carte mondiale"
#: src/views/PageNotFound.vue:42
msgid "Search events, groups, etc."
msgstr "Rechercher des événements, des groupes, etc."

File diff suppressed because one or more lines are too long

View File

@ -2,28 +2,28 @@
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
import Buefy from 'buefy';
import GetTextPlugin from 'vue-gettext';
import VueI18n from 'vue-i18n';
import App from '@/App.vue';
import router from '@/router';
import { apolloProvider } from './vue-apollo';
import { NotifierPlugin } from '@/plugins/notifier';
const translations = require('@/i18n/translations.json');
import filters from '@/filters';
import messages from '@/i18n/index';
Vue.config.productionTip = false;
Vue.use(Buefy);
Vue.use(NotifierPlugin);
Vue.use(filters);
const language = (window.navigator as any).userLanguage || window.navigator.language;
Vue.use(GetTextPlugin, {
translations,
defaultLanguage: 'en_US',
silent: true,
});
Vue.use(VueI18n);
Vue.config.language = language.replace('-', '_');
const i18n = new VueI18n({
locale: language.replace('-', '_'), // set locale
messages, // set locale messages
});
/* eslint-disable no-new */
new Vue({
@ -32,4 +32,5 @@ new Vue({
el: '#app',
template: '<App/>',
components: { App },
i18n,
});

View File

@ -4,6 +4,7 @@ declare module 'vue/types/vue' {
interface Vue {
$notifier: {
success: (message: string) => void;
error: (message: string) => void;
};
}
}
@ -16,7 +17,7 @@ export class Notifier {
}
success(message: string) {
this.vue.prototype.$notification.open({
this.vue.prototype.$buefy.notification.open({
message,
duration: 5000,
position: 'is-bottom-right',
@ -24,6 +25,16 @@ export class Notifier {
hasIcon: true,
});
}
error(message: string) {
this.vue.prototype.$buefy.notification.open({
message,
duration: 5000,
position: 'is-bottom-right',
type: 'is-danger',
hasIcon: true,
});
}
}
// tslint:disable

16
js/src/router/admin.ts Normal file
View File

@ -0,0 +1,16 @@
import { RouteConfig } from 'vue-router';
import Dashboard from '@/views/Admin/Dashboard.vue';
export enum AdminRouteName {
DASHBOARD = 'Dashboard',
}
export const adminRoutes: RouteConfig[] = [
{
path: '/admin',
name: AdminRouteName.DASHBOARD,
component: Dashboard,
props: true,
meta: { requiredAuth: true },
},
];

View File

@ -5,9 +5,11 @@ import Home from '@/views/Home.vue';
import { UserRouteName, userRoutes } from './user';
import { EventRouteName, eventRoutes } from '@/router/event';
import { ActorRouteName, actorRoutes, MyAccountRouteName } from '@/router/actor';
import { AdminRouteName, adminRoutes } from '@/router/admin';
import { ErrorRouteName, errorRoutes } from '@/router/error';
import { authGuardIfNeeded } from '@/router/guards/auth-guard';
import Search from '@/views/Search.vue';
import { ModerationRouteName, moderationRoutes } from '@/router/moderation';
Vue.use(Router);
@ -35,6 +37,8 @@ export const RouteName = {
...EventRouteName,
...ActorRouteName,
...MyAccountRouteName,
...AdminRouteName,
...ModerationRouteName,
...ErrorRouteName,
};
@ -46,6 +50,8 @@ const router = new Router({
...userRoutes,
...eventRoutes,
...actorRoutes,
...adminRoutes,
...moderationRoutes,
...errorRoutes,
{
path: '/search/:searchTerm/:searchType?',

View File

@ -0,0 +1,34 @@
import { RouteConfig } from 'vue-router';
import ReportList from '@/views/Moderation/ReportList.vue';
import Report from '@/views/Moderation/Report.vue';
import Logs from '@/views/Moderation/Logs.vue';
export enum ModerationRouteName {
REPORTS = 'Reports',
REPORT = 'Report',
LOGS = 'Logs',
}
export const moderationRoutes: RouteConfig[] = [
{
path: '/moderation/reports/:filter?',
name: ModerationRouteName.REPORTS,
component: ReportList,
props: true,
meta: { requiredAuth: true },
},
{
path: '/moderation/report/:reportId',
name: ModerationRouteName.REPORT,
component: Report,
props: true,
meta: { requiredAuth: true },
},
{
path: '/moderation/logs',
name: ModerationRouteName.LOGS,
component: Logs,
props: true,
meta: { requiredAuth: true },
},
];

View File

@ -36,7 +36,7 @@ export class Actor implements IActor {
return `@${this.preferredUsername}${domain}`;
}
displayName(): string {
public displayName(): string {
return this.name != null && this.name !== '' ? this.name : this.usernameWithDomain();
}
}

View File

@ -0,0 +1,9 @@
import { IEvent } from '@/types/event.model';
export interface IDashboard {
lastPublicEventPublished: IEvent;
numberOfUsers: number;
numberOfEvents: number;
numberOfComments: number;
numberOfReports: number;
}

View File

@ -1,5 +1,12 @@
export enum ICurrentUserRole {
USER = 'USER',
MODERATOR = 'MODERATOR',
ADMINISTRATOR = 'ADMINISTRATOR',
}
export interface ICurrentUser {
id: number;
email: string;
isLoggedIn: boolean;
role: ICurrentUserRole;
}

View File

@ -90,8 +90,12 @@ export interface IEvent {
picture: IPicture | null;
organizerActor: IActor;
organizerActor?: IActor;
attributedTo: IActor;
participantStats: {
approved: number;
unapproved: number;
};
participants: IParticipant[];
relatedEvents: IEvent[];
@ -117,15 +121,15 @@ export interface IEventOptions {
}
export class EventOptions implements IEventOptions {
maximumAttendeeCapacity: number = 0;
remainingAttendeeCapacity: number = 0;
showRemainingAttendeeCapacity: boolean = false;
maximumAttendeeCapacity = 0;
remainingAttendeeCapacity = 0;
showRemainingAttendeeCapacity = false;
offers: IOffer[] = [];
participationConditions: IParticipationCondition[] = [];
attendees: string[] = [];
program: string = '';
commentModeration: CommentModeration = CommentModeration.ALLOW_ALL;
showParticipationPrice: boolean = false;
program = '';
commentModeration = CommentModeration.ALLOW_ALL;
showParticipationPrice = false;
}
export class EventModel implements IEvent {
@ -154,12 +158,13 @@ export class EventModel implements IEvent {
publishAt = new Date();
participantStats = { approved: 0, unapproved: 0 };
participants: IParticipant[] = [];
relatedEvents: IEvent[] = [];
attributedTo = new Actor();
organizerActor = new Actor();
organizerActor?: IActor;
tags: ITag[] = [];
options: IEventOptions = new EventOptions();
@ -200,6 +205,25 @@ export class EventModel implements IEvent {
this.physicalAddress = hash.physicalAddress;
this.tags = hash.tags;
this.options = hash.options;
if (hash.options) this.options = hash.options;
}
toEditJSON () {
return {
id: this.id,
title: this.title,
description: this.description,
beginsOn: this.beginsOn.toISOString(),
endsOn: this.endsOn ? this.endsOn.toISOString() : null,
status: this.status,
visibility: this.visibility,
tags: this.tags.map(t => t.title),
picture: this.picture,
onlineAddress: this.onlineAddress,
phoneAddress: this.phoneAddress,
category: this.category,
physicalAddress: this.physicalAddress,
options: this.options,
};
}
}

View File

@ -0,0 +1,47 @@
import { IActor, IPerson } from '@/types/actor';
import { IEvent } from '@/types/event.model';
export enum ReportStatusEnum {
OPEN = 'OPEN',
CLOSED = 'CLOSED',
RESOLVED = 'RESOLVED',
}
export interface IReport extends IActionLogObject {
id: string;
reported: IActor;
reporter: IPerson;
event?: IEvent;
content: string;
notes: IReportNote[];
insertedAt: Date;
updatedAt: Date;
status: ReportStatusEnum;
}
export interface IReportNote extends IActionLogObject{
id: string;
content: string;
moderator: IActor;
}
export interface IActionLogObject {
id: string;
}
export enum ActionLogAction {
NOTE_CREATION = 'NOTE_CREATION',
NOTE_DELETION = 'NOTE_DELETION',
REPORT_UPDATE_CLOSED = 'REPORT_UPDATE_CLOSED',
REPORT_UPDATE_OPENED = 'REPORT_UPDATE_OPENED',
REPORT_UPDATE_RESOLVED = 'REPORT_UPDATE_RESOLVED',
EVENT_DELETION = 'EVENT_DELETION',
}
export interface IActionLog {
id: string;
object: IReport|IReportNote|IEvent;
actor: IActor;
action: ActionLogAction;
insertedAt: Date;
}

View File

@ -1,27 +1,50 @@
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID } from '@/constants';
import {
AUTH_ACCESS_TOKEN,
AUTH_REFRESH_TOKEN,
AUTH_USER_ACTOR_ID,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
} from '@/constants';
import { ILogin, IToken } from '@/types/login.model';
import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { onLogout } from '@/vue-apollo';
import ApolloClient from 'apollo-client';
import { ICurrentUserRole } from '@/types/current-user.model';
import { IPerson } from '@/types/actor';
import { UPDATE_CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
export function saveUserData(obj: ILogin) {
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_USER_ROLE, obj.user.role);
saveTokenData(obj);
}
export function saveActorData(obj: IPerson) {
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
}
export function saveTokenData(obj: IToken) {
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
}
export function deleteUserData() {
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN]) {
for (const key of [AUTH_USER_ID, AUTH_USER_EMAIL, AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN, AUTH_USER_ROLE, AUTH_USER_ACTOR_ID]) {
localStorage.removeItem(key);
}
}
export async function changeIdentity(apollo: ApolloClient<any>, identity: IPerson) {
await apollo.mutate({
mutation: UPDATE_CURRENT_ACTOR_CLIENT,
variables: identity,
});
saveActorData(identity);
}
export function logout(apollo: ApolloClient<any>) {
apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT,
@ -29,6 +52,7 @@ export function logout(apollo: ApolloClient<any>) {
id: null,
email: null,
isLoggedIn: false,
role: ICurrentUserRole.USER,
},
});

View File

@ -0,0 +1,61 @@
<template>
<div class="identity-picker">
<img class="image" v-if="currentIdentity.avatar" :src="currentIdentity.avatar.url" :alt="currentIdentity.avatar.alt"/> {{ currentIdentity.name || `@${currentIdentity.preferredUsername}` }}
<b-button type="is-text" @click="isComponentModalActive = true">
{{ $t('Change') }}
</b-button>
<b-modal :active.sync="isComponentModalActive" has-modal-card>
<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" v-if="identity.avatar" :src="identity.avatar.url" />
<div class="media-content">
<h3>@{{ identity.preferredUsername }}</h3>
<small>{{ identity.name }}</small>
</div>
</div>
</a>
</div>
</section>
</div>
</b-modal>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { IActor } from '@/types/actor';
import { IDENTITIES } from '@/graphql/actor';
@Component({
apollo: {
identities: {
query: IDENTITIES,
},
},
})
export default class IdentityPicker extends Vue {
@Prop() value!: IActor;
isComponentModalActive: boolean = false;
identities: IActor[] = [];
currentIdentity: IActor = this.value;
changeCurrentIdentity(identity: IActor) {
this.currentIdentity = identity;
this.$emit('input', identity);
this.isComponentModalActive = false;
}
}
</script>
<style lang="scss">
.identity-picker img.image {
display: inline;
height: 1.5em;
vertical-align: text-bottom;
}
</style>

View File

@ -1,9 +1,9 @@
<template>
<section class="container">
<div v-if="loggedPerson">
<div v-if="currentActor">
<div class="header">
<figure v-if="loggedPerson.banner" class="image is-3by1">
<img :src="loggedPerson.banner.url" alt="banner">
<figure v-if="currentActor.banner" class="image is-3by1">
<img :src="currentActor.banner.url" alt="banner">
</figure>
</div>
@ -31,7 +31,7 @@
</style>
<script lang="ts">
import { LOGGED_PERSON } from '@/graphql/actor';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { Component, Vue, Watch } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue';
import { IPerson } from '@/types/actor';
@ -42,17 +42,18 @@ import Identities from '@/components/Account/Identities.vue';
EventCard,
Identities,
},
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
export default class MyAccount extends Vue {
loggedPerson: IPerson | null = null;
currentActor!: IPerson;
currentIdentityName: string | null = null;
@Watch('$route.params.identityName', { immediate: true })
async onIdentityParamChanged (val: string) {
if (!this.loggedPerson) {
this.loggedPerson = await this.loadLoggedPerson();
}
await this.redirectIfNoIdentitySelected(val);
this.currentIdentityName = val;
@ -61,18 +62,10 @@ export default class MyAccount extends Vue {
private async redirectIfNoIdentitySelected (identityParam?: string) {
if (!!identityParam) return;
if (!!this.loggedPerson) {
this.$router.push({ params: { identityName: this.loggedPerson.preferredUsername } });
if (!!this.currentActor) {
await this.$router.push({ params: { identityName: this.currentActor.preferredUsername } });
}
}
private async loadLoggedPerson () {
const result = await this.$apollo.query({
query: LOGGED_PERSON,
});
return result.data.loggedPerson as IPerson;
}
}
</script>
<style lang="scss">

View File

@ -24,46 +24,46 @@
<b-dropdown hoverable has-link aria-role="list">
<button class="button is-primary" slot="trigger">
<translate>Public feeds</translate>
{{ $t('Public feeds') }}
<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>
{{ $t('Public RSS/Atom Feed') }}
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', true)">
<translate>Public iCal Feed</translate>
{{ $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">
<translate>Private feeds</translate>
{{ $t('Private feeds') }}
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('atom', false)">
<translate>RSS/Atom Feed</translate>
{{ $t('RSS/Atom Feed') }}
</a>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<a :href="feedUrls('ics', false)">
<translate>iCal Feed</translate>
{{ $t('iCal Feed') }}
</a>
</b-dropdown-item>
</b-dropdown>
<a class="button" v-if="loggedPerson.id === person.id" @click="createToken">
<translate>Create token</translate>
<a class="button" v-if="currentActor.id === person.id" @click="createToken">
{{ $t('Create token') }}
</a>
</div>
<section v-if="person.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
{{ $t('Organized') }}
</h2>
<div class="columns">
<EventCard
@ -79,9 +79,9 @@
<a
class="button"
@click="deleteProfile()"
v-if="loggedPerson && loggedPerson.id === person.id"
v-if="currentActor && currentActor.id === person.id"
>
<translate>Delete</translate>
{{ $t('Delete') }}
</a>
</p>
</div>
@ -91,7 +91,7 @@
</template>
<script lang="ts">
import { FETCH_PERSON, LOGGED_PERSON } from '@/graphql/actor';
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';
@ -108,8 +108,8 @@ import { CREATE_FEED_TOKEN_ACTOR } from '@/graphql/feed_tokens';
};
},
},
loggedPerson: {
query: LOGGED_PERSON,
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
components: {
@ -120,6 +120,7 @@ export default class MyAccount extends Vue {
@Prop({ type: String, required: true }) name!: string;
person!: IPerson;
currentActor!: IPerson;
// call again the method if the route changes
@Watch('$route')

View File

@ -3,7 +3,7 @@
<section class="hero">
<div class="hero-body">
<h1 class="title">
<translate>Register an account on Mobilizon!</translate>
{{ $t('Register an account on Mobilizon!') }}
</h1>
</div>
</section>
@ -13,7 +13,7 @@
<div class="column">
<form v-if="!validationSent">
<b-field
:label="$gettext('Username')"
:label="t('Username')"
:type="errors.preferred_username ? 'is-danger' : null"
:message="errors.preferred_username"
>
@ -30,18 +30,18 @@
</b-field>
</b-field>
<b-field :label="$gettext('Displayed name')">
<b-field :label="$t('Displayed name')">
<b-input v-model="person.name"/>
</b-field>
<b-field :label="$gettext('Description')">
<b-field :label="$t('Description')">
<b-input type="textarea" v-model="person.summary"/>
</b-field>
<b-field grouped>
<div class="control">
<button type="button" class="button is-primary" @click="submit()">
<translate>Create my profile</translate>
{{ $t('Create my profile') }}
</button>
</div>
</b-field>
@ -50,15 +50,13 @@
<div v-if="validationSent && !userAlreadyActivated">
<b-message title="Success" type="is-success">
<h2 class="title">
<translate
:translate-params="{ username: person.preferredUsername }"
>Your account is nearly ready, %{username}</translate>
{{ $t('Your account is nearly ready, {username}', { username: person.preferredUsername }) }}
</h2>
<p>
<translate>A validation email was sent to %{email}</translate>
{{ $t('A validation email was sent to {email}', { email }) }}
</p>
<p>
<translate>Before you can login, you need to click on the link inside it to validate your account</translate>
{{ $t('Before you can login, you need to click on the link inside it to validate your account') }}
</p>
</b-message>
</div>

View File

@ -2,16 +2,16 @@
<div class="root">
<h1 class="title">
<span v-if="isUpdate">{{ identity.displayName() }}</span>
<translate v-else>I create an identity</translate>
<span v-else>{{ $t('I create an identity') }}</span>
</h1>
<picture-upload v-model="avatarFile" class="picture-upload"></picture-upload>
<b-field :label="$gettext('Display name')">
<b-field :label="$t('Display name')">
<b-input aria-required="true" required v-model="identity.name" @input="autoUpdateUsername($event)"/>
</b-field>
<b-field :label="$gettext('Username')">
<b-field :label="$t('Username')">
<b-field>
<b-input aria-required="true" required v-model="identity.preferredUsername" :disabled="isUpdate"/>
@ -21,21 +21,31 @@
</b-field>
</b-field>
<b-field :label="$gettext('Description')">
<b-field :label="$t('Description')">
<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"
v-for="error in errors"
>
{{ error }}
</b-notification>
<b-field class="submit">
<div class="control">
<button v-translate type="button" class="button is-primary" @click="submit()">
Save
<button type="button" class="button is-primary" @click="submit()">
{{ $t('Save') }}
</button>
</div>
</b-field>
<div class="delete-identity" v-if="isUpdate">
<span v-translate @click="openDeleteIdentityConfirmation()">
Delete this identity
<span @click="openDeleteIdentityConfirmation()">
{{ $t('Delete this identity') }}
</span>
</div>
</div>
@ -70,19 +80,32 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { CREATE_PERSON, DELETE_PERSON, FETCH_PERSON, IDENTITIES, LOGGED_PERSON, UPDATE_PERSON } from '../../../graphql/actor';
import {
CREATE_PERSON,
CURRENT_ACTOR_CLIENT,
DELETE_PERSON,
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 } from '@/utils/image';
import { changeIdentity, saveActorData } from '@/utils/auth';
@Component({
components: {
PictureUpload,
Dialog,
},
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
export default class EditIdentity extends Vue {
@Prop({ type: Boolean }) isUpdate!: boolean;
@ -94,7 +117,7 @@ export default class EditIdentity extends Vue {
identity = new Person();
private oldDisplayName: string | null = null;
private loggedPerson: IPerson | null = null;
private currentActor: IPerson | null = null;
@Watch('isUpdate')
async isUpdateChanged () {
@ -134,6 +157,9 @@ export default class EditIdentity extends Vue {
this.oldDisplayName = newDisplayName;
}
/**
* Delete an identity
*/
async deleteIdentity() {
try {
await this.$apollo.mutate({
@ -150,17 +176,15 @@ export default class EditIdentity extends Vue {
},
});
this.$notifier.success(
this.$gettextInterpolate('Identity %{displayName} deleted', { displayName: this.identity.displayName() }),
this.$t('Identity {displayName} deleted', { displayName: this.identity.displayName() }) as string,
);
await this.loadLoggedPersonIfNeeded();
// Refresh the loaded person if we deleted the default identity
if (this.loggedPerson && this.identity.id === this.loggedPerson.id) {
this.loggedPerson = null;
await this.loadLoggedPersonIfNeeded(true);
/**
* 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 });
if (data) {
await this.maybeUpdateCurrentActorCache(data.identities[0]);
}
await this.redirectIfNoIdentitySelected();
@ -181,6 +205,7 @@ export default class EditIdentity extends Vue {
const index = data.identities.findIndex(i => i.id === this.identity.id);
this.$set(data.identities, index, updatePerson);
this.maybeUpdateCurrentActorCache(updatePerson);
store.writeQuery({ query: IDENTITIES, data });
}
@ -188,7 +213,7 @@ export default class EditIdentity extends Vue {
});
this.$notifier.success(
this.$gettextInterpolate('Identity %{displayName} updated', { displayName: this.identity.displayName() }),
this.$t('Identity {displayName} updated', { displayName: this.identity.displayName() }) as string,
);
} catch (err) {
this.handleError(err);
@ -211,11 +236,11 @@ export default class EditIdentity extends Vue {
},
});
this.$router.push({ name: RouteName.UPDATE_IDENTITY, params: { identityName: this.identity.preferredUsername } });
this.$notifier.success(
this.$gettextInterpolate('Identity %{displayName} created', { displayName: this.identity.displayName() }),
this.$t('Identity {displayName} created', { displayName: this.identity.displayName() }) as string,
);
await this.$router.push({ name: RouteName.UPDATE_IDENTITY, params: { identityName: this.identity.preferredUsername } });
} catch (err) {
this.handleError(err);
}
@ -228,18 +253,17 @@ export default class EditIdentity extends Vue {
openDeleteIdentityConfirmation() {
this.$buefy.dialog.prompt({
type: 'is-danger',
title: this.$gettext('Delete your identity'),
message: this.$gettextInterpolate(
'This will delete / anonymize all content (events, comments, messages, participation...) created from this identity. <br /><br />' +
'If this identity is the only administrator of some groups, you need to delete them before being able to delete this identity. ' +
'Otherwise this identity will just be removed from the group administrators.<br /><br />' +
'To confirm, type your identity username "%{preferredUsername}"',
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.')}
<br /><br />
${this.$t('To confirm, type your identity username "{preferredUsername}"', { preferredUsername: this.identity.preferredUsername })}`,
confirmText: this.$t(
'Delete {preferredUsername}',
{ preferredUsername: this.identity.preferredUsername },
),
confirmText: this.$gettextInterpolate(
'Delete %{preferredUsername}',
{ preferredUsername: this.identity.preferredUsername },
),
) as string,
inputAttrs: {
placeholder: this.identity.preferredUsername,
pattern: this.identity.preferredUsername,
@ -263,10 +287,12 @@ export default class EditIdentity extends Vue {
private handleError(err: any) {
console.error(err);
if (err.graphQLErrors !== undefined) {
err.graphQLErrors.forEach(({ message }) => {
this.errors.push(message);
this.$notifier.error(message);
});
}
}
private convertToUsername(value: string | null) {
if (!value) return '';
@ -287,20 +313,29 @@ export default class EditIdentity extends Vue {
await this.loadLoggedPersonIfNeeded();
if (!!this.loggedPerson) {
this.$router.push({ params: { identityName: this.loggedPerson.preferredUsername } });
if (!!this.currentActor) {
await this.$router.push({ params: { identityName: this.currentActor.preferredUsername } });
}
}
private async maybeUpdateCurrentActorCache(identity: IPerson) {
if (this.currentActor) {
if (this.currentActor.preferredUsername === this.identity.preferredUsername) {
await changeIdentity(this.$apollo.provider.defaultClient, identity);
}
this.currentActor = identity;
}
}
private async loadLoggedPersonIfNeeded (bypassCache = false) {
if (this.loggedPerson) return;
if (this.currentActor) return;
const result = await this.$apollo.query({
query: LOGGED_PERSON,
query: CURRENT_ACTOR_CLIENT,
fetchPolicy: bypassCache ? 'network-only' : undefined,
});
this.loggedPerson = result.data.loggedPerson;
this.currentActor = result.data.currentActor;
}
private resetFields () {

View File

@ -0,0 +1,73 @@
<template>
<section class="container">
<h1 class="title">{{ $t('Administration') }}</h1>
<div class="tile is-ancestor" v-if="dashboard">
<div class="tile is-vertical is-4">
<div class="tile">
<div class="tile is-parent is-vertical is-6">
<article class="tile is-child box">
<p class="title">{{ dashboard.numberOfEvents }}</p>
<p class="subtitle">{{ $t('Published events')}}</p>
</article>
<article class="tile is-child box">
<p class="title">{{ dashboard.numberOfComments}}</p>
<p class="subtitle">{{ $t('Comments')}}</p>
</article>
</div>
<div class="tile is-parent is-vertical">
<article class="tile is-child box">
<p class="title">{{ dashboard.numberOfUsers }}</p>
<p class="subtitle">{{ $t('Users')}}</p>
</article>
<router-link :to="{ name: ModerationRouteName.REPORTS}">
<article class="tile is-child box">
<p class="title">{{ dashboard.numberOfReports }}</p>
<p class="subtitle">{{ $t('Opened reports')}}</p>
</article>
</router-link>
</div>
</div>
<div class="tile is-parent" v-if="dashboard.lastPublicEventPublished">
<article class="tile is-child box">
<p class="title">{{ $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>
</div>
</div>
<div class="tile is-parent">
<article class="tile is-child box">
<div class="content">
<p class="title">{{ $t('Welcome on your administration panel') }}</p>
<p class="subtitle">With even more content</p>
<div class="content">
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam semper diam at erat pulvinar, at pulvinar felis blandit. Vestibulum volutpat tellus diam, consequat gravida libero rhoncus ut. Morbi maximus, leo sit amet vehicula eleifend, nunc dui porta orci, quis semper odio felis ut quam.</p>
<p>Suspendisse varius ligula in molestie lacinia. Maecenas varius eget ligula a sagittis. Pellentesque interdum, nisl nec interdum maximus, augue diam porttitor lorem, et sollicitudin felis neque sit amet erat. Maecenas imperdiet felis nisi, fringilla luctus felis hendrerit sit amet. Aenean vitae gravida diam, finibus dignissim turpis. Sed eget varius ligula, at volutpat tortor.</p>
<p>Integer sollicitudin, tortor a mattis commodo, velit urna rhoncus erat, vitae congue lectus dolor consequat libero. Donec leo ligula, maximus et pellentesque sed, gravida a metus. Cras ullamcorper a nunc ac porta. Aliquam ut aliquet lacus, quis faucibus libero. Quisque non semper leo.</p>
</div>
</div>
</article>
</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 { ModerationRouteName } from '@/router/moderation';
@Component({
apollo: {
dashboard: {
query: DASHBOARD,
},
},
})
export default class Dashboard extends Vue {
dashboard!: IDashboard;
ModerationRouteName = ModerationRouteName;
}
</script>

View File

@ -1,10 +1,12 @@
<template>
<div v-if="code === ErrorCode.REGISTRATION_CLOSED">
<translate>Registration is currently closed.</translate>
</div>
<div>
<span v-if="code === ErrorCode.REGISTRATION_CLOSED">
{{ $t('Registration is currently closed.') }}
</span>
<div v-else>
<translate>Unknown error.</translate>
<span v-else>
{{ $t('Unknown error.') }}
</span>
</div>
</template>

View File

@ -1,129 +1,129 @@
<template>
<section class="container">
<h1 class="title">
<translate v-if="isUpdate === false">Create a new event</translate>
<translate v-else>Update event {{ event.name }}</translate>
<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>
<div v-if="$apollo.loading">Loading...</div>
<div v-if="$apollo.loading">{{ $t('Loading') }}</div>
<div class="columns is-centered" v-else>
<form class="column is-two-thirds-desktop" @submit="createOrUpdate">
<h2 class="subtitle">
<translate>
General information
</translate>
{{ $t('General information') }}
</h2>
<picture-upload v-model="pictureFile" />
<b-field :label="$gettext('Title')">
<b-field :label="$t('Title')">
<b-input aria-required="true" required v-model="event.title" maxlength="64" />
</b-field>
<tag-input v-model="event.tags" :data="tags" path="title" />
<date-time-picker v-model="event.beginsOn" :label="$t('Starts on…')" :step="15"/>
<date-time-picker v-model="event.endsOn" :label="$t('Ends on…')" :step="15" />
<address-auto-complete v-model="event.physicalAddress" />
<date-time-picker v-model="event.beginsOn" :label="$gettext('Starts on…')" :step="15"/>
<date-time-picker v-model="event.endsOn" :label="$gettext('Ends on…')" :step="15" />
<b-field :label="$t('Organizer')">
<identity-picker v-model="event.organizerActor"></identity-picker>
</b-field>
<div class="field">
<label class="label">{{ $gettext('Description') }}</label>
<label class="label">{{ $t('Description') }}</label>
<editor v-model="event.description" />
</div>
<b-field :label="$gettext('Website / URL')">
<b-field :label="$t('Website / URL')">
<b-input v-model="event.onlineAddress" placeholder="URL" />
</b-field>
<!--<b-field :label="$gettext('Category')">
<!--<b-field :label="$t('Category')">
<b-select placeholder="Select a category" v-model="event.category">
<option
v-for="category in categories"
:value="category"
:key="category"
>{{ $gettext(category) }}</option>
>{{ $t(category) }}</option>
</b-select>
</b-field>-->
<h2 class="subtitle">
<translate>
Who can view this event and participate
</translate>
{{ $t('Who can view this event and participate') }}
</h2>
<div class="field">
<b-radio v-model="eventVisibilityJoinOptions"
name="eventVisibilityJoinOptions"
:native-value="EventVisibilityJoinOptions.PUBLIC">
<translate>Visible everywhere on the web (public)</translate>
{{ $t('Visible everywhere on the web (public)') }}
</b-radio>
</div>
<div class="field">
<b-radio v-model="eventVisibilityJoinOptions"
name="eventVisibilityJoinOptions"
:native-value="EventVisibilityJoinOptions.LINK">
<translate>Only accessible through link and search (private)</translate>
{{ $t('Only accessible through link and search (private)') }}
</b-radio>
</div>
<div class="field">
<b-radio v-model="eventVisibilityJoinOptions"
name="eventVisibilityJoinOptions"
:native-value="EventVisibilityJoinOptions.LIMITED">
<translate>Page limited to my group (asks for auth)</translate>
{{ $t('Page limited to my group (asks for auth)') }}
</b-radio>
</div>
<div class="field">
<label class="label">Approbation des participations</label>
<label class="label">{{ $t('Participation approval') }}</label>
<b-switch v-model="needsApproval">
Je veux approuver chaque demande de participation
{{ $t('I want to approve every participation request') }}
</b-switch>
</div>
<div class="field">
<label class="label">Mise en avant</label>
<label class="label">{{ $t('Promotion') }}</label>
<b-switch v-model="doNotPromote" :disabled="canPromote === false">
Ne pas autoriser la mise en avant sur sur Mobilizon
{{ $t('Disallow promoting on Mobilizon')}}
</b-switch>
</div>
<div class="field">
<b-switch v-model="limitedPlaces">
Places limitées
{{ $t('Limited places') }}
</b-switch>
</div>
<div class="box" v-if="limitedPlaces">
<b-field label="Number of places">
<b-field :label="$t('Number of places')">
<b-numberinput v-model="event.options.maximumAttendeeCapacity"></b-numberinput>
</b-field>
<b-field>
<b-switch v-model="event.options.showRemainingAttendeeCapacity">
Show remaining number of places
{{ $t('Show remaining number of places') }}
</b-switch>
</b-field>
<b-field>
<b-switch v-model="event.options.showParticipationPrice">
Display participation price
{{ $t('Display participation price') }}
</b-switch>
</b-field>
</div>
<h2 class="subtitle">
<translate>
Modération des commentaires publics
</translate>
{{ $t('Public comment moderation') }}
</h2>
<label>Comments on the event page</label>
<label>{{ $t('Comments on the event page') }}</label>
<div class="field">
<b-radio v-model="event.options.commentModeration"
name="commentModeration"
:native-value="CommentModeration.ALLOW_ALL">
<translate>Allow all comments</translate>
{{ $t('Allow all comments') }}
</b-radio>
</div>
@ -131,7 +131,7 @@
<b-radio v-model="event.options.commentModeration"
name="commentModeration"
:native-value="CommentModeration.MODERATED">
<translate>Moderated comments (shown after approval)</translate>
{{ $t('Moderated comments (shown after approval)') }}
</b-radio>
</div>
@ -139,21 +139,19 @@
<b-radio v-model="event.options.commentModeration"
name="commentModeration"
:native-value="CommentModeration.CLOSED">
<translate>Close all comments (except for admins)</translate>
{{ $t('Close comments for all (except for admins)') }}
</b-radio>
</div>
<h2 class="subtitle">
<translate>
Status
</translate>
{{ $t('Status') }}
</h2>
<div class="field">
<b-radio v-model="event.status"
name="status"
:native-value="EventStatus.TENTATIVE">
<translate>Tentative: Will be confirmed later</translate>
{{ $t('Tentative: Will be confirmed later') }}
</b-radio>
</div>
@ -161,24 +159,38 @@
<b-radio v-model="event.status"
name="status"
:native-value="EventStatus.CONFIRMED">
<translate>Confirmed: Will happen</translate>
{{ $t('Confirmed: Will happen') }}
</b-radio>
</div>
<button class="button is-primary">
<translate v-if="isUpdate === false">Create my event</translate>
<translate v-else>Update my event</translate>
<span v-if="isUpdate === false">{{ $t('Create my event') }}</span>
<span v-else> {{ $t('Update my event') }}</span>
</button>
</form>
</div>
</section>
</template>
<style lang="scss">
@import "@/variables.scss";
h2.subtitle {
margin: 10px 0;
span {
padding: 5px 7px;
display: inline;
background: $secondary;
}
}
</style>
<script lang="ts">
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT } from '@/graphql/event';
import { CREATE_EVENT, EDIT_EVENT, FETCH_EVENT, FETCH_EVENTS } from '@/graphql/event';
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration } from '@/types/event.model';
import { LOGGED_PERSON } from '@/graphql/actor';
import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration, IEvent } from '@/types/event.model';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue';
import Editor from '@/components/Editor.vue';
@ -188,12 +200,13 @@ import { TAGS } from '@/graphql/tags';
import { ITag } from '@/types/tag.model';
import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
import IdentityPicker from '@/views/Account/IdentityPicker.vue';
@Component({
components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor },
components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor, IdentityPicker },
apollo: {
loggedPerson: {
query: LOGGED_PERSON,
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
tags: {
query: TAGS,
@ -206,7 +219,7 @@ export default class EditEvent extends Vue {
eventId!: string | undefined;
loggedPerson = new Person();
currentActor = new Person();
tags: ITag[] = [];
event = new EventModel();
pictureFile: File | null = null;
@ -243,6 +256,7 @@ export default class EditEvent extends Vue {
this.event.beginsOn = now;
this.event.endsOn = end;
this.event.organizerActor = this.event.organizerActor || this.currentActor;
}
createOrUpdate(e: Event) {
@ -291,12 +305,10 @@ export default class EditEvent extends Vue {
* Build variables for Event GraphQL creation query
*/
private buildVariables() {
const obj = {
organizerActorId: this.loggedPerson.id,
beginsOn: this.event.beginsOn.toISOString(),
tags: this.event.tags.map((tag: ITag) => tag.title),
};
const res = Object.assign({}, this.event, obj);
let res = this.event.toEditJSON();
if (this.event.organizerActor) {
res = Object.assign(res, { organizerActorId: this.event.organizerActor.id });
}
delete this.event.options['__typename'];
@ -360,16 +372,4 @@ export default class EditEvent extends Vue {
// }
}
</script>
<style lang="scss">
@import "@/variables.scss";
h2.subtitle {
margin: 10px 0;
span {
padding: 5px 7px;
display: inline;
background: $secondary;
}
}
</style>

View File

@ -18,14 +18,20 @@
</div>
<h1 class="title">{{ event.title }}</h1>
</div>
<span v-if="event.participantStats.approved > 0 && !actorIsParticipant()">
{{ $tc('One person is going', event.participantStats.approved, {approved: event.participantStats.approved}) }}
</span>
<span v-else>
{{ $tc('You and one other person are going to this event', event.participantStats.approved - 1, {approved: event.participantStats.approved - 1}) }}
</span>
<div v-if="!actorIsOrganizer()" class="participate-button has-text-centered">
<a v-if="!actorIsParticipant()" @click="joinEvent" class="button is-large is-primary is-rounded">
<a v-if="!actorIsParticipant()" @click="isJoinModalActive = true" class="button is-large is-primary is-rounded">
<b-icon icon="circle-outline"></b-icon>
<translate>Join</translate>
{{ $t('Join') }}
</a>
<a v-if="actorIsParticipant()" @click="leaveEvent" class="button is-large is-primary is-rounded">
<a v-if="actorIsParticipant()" @click="confirmLeave()" class="button is-large is-primary is-rounded">
<b-icon icon="check-circle"></b-icon>
<translate>Leave</translate>
{{ $t('Leave') }}
</a>
</div>
</div>
@ -35,7 +41,7 @@
<span class="tag" v-if="event.category">{{ event.category }}</span>
<span class="tag" v-if="event.tags" v-for="tag in event.tags">{{ tag.title }}</span>
<span class="visibility">
<translate v-if="event.visibility === EventVisibility.PUBLIC">public event</translate>
<span v-if="event.visibility === EventVisibility.PUBLIC">{{ $t('public event') }}</span>
</span>
</p>
<div class="date-and-add-to-calendar">
@ -45,7 +51,7 @@
</div>
<a class="add-to-calendar" @click="downloadIcsEvent()">
<b-icon icon="calendar-plus" />
<translate>Add to my calendar</translate>
{{ $t('Add to my calendar') }}
</a>
</div>
<p class="slug">
@ -53,24 +59,29 @@
</p>
</div>
<div class="column sidebar">
<div class="field has-addons" v-if="actorIsOrganizer()">
<p class="control">
<div class="field has-addons">
<p class="control" v-if="actorIsOrganizer()">
<router-link
class="button"
:to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
>
<translate>Edit</translate>
{{ $t('Edit') }}
</router-link>
</p>
<p class="control" v-if="actorIsOrganizer()">
<a class="button is-danger" @click="openDeleteEventModal()">
{{ $t('Delete') }}
</a>
</p>
<p class="control">
<a class="button is-danger" @click="deleteEvent()">
<translate>Delete</translate>
<a class="button is-danger" @click="isReportModalActive = true">
{{ $t('Report') }}
</a>
</p>
</div>
<div class="address-wrapper">
<b-icon icon="map" />
<translate v-if="!event.physicalAddress">No address defined</translate>
<span v-if="!event.physicalAddress">{{ $t('No address defined') }}</span>
<div class="address" v-if="event.physicalAddress">
<address>
<span class="addressDescription">{{ event.physicalAddress.description }}</span>
@ -79,7 +90,7 @@
<!-- <span>{{ event.physicalAddress.region }} {{ event.physicalAddress.country }}</span>-->
</address>
<span class="map-show-button" @click="showMap = !showMap" v-if="event.physicalAddress && event.physicalAddress.geom">
<translate>Show map</translate>
{{ $t('Show map') }}
</span>
</div>
<b-modal v-if="event.physicalAddress && event.physicalAddress.geom" :active.sync="showMap" :width="800" scroll="keep">
@ -93,14 +104,14 @@
</div>
<div class="organizer">
<actor-link :actor="event.organizerActor">
<translate
:translate-params="{name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}"
v-if="event.organizerActor">By %{ name }</translate>
<span v-if="event.organizerActor">
{{ $t('By {name}', {name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}) }}
</span>
<figure v-if="event.organizerActor.avatar" class="image is-48x48">
<img
class="is-rounded"
:src="event.organizerActor.avatar.url"
:alt="$gettextInterpolate('%{actor}\'s avatar', {actor: event.organizerActor.preferredUsername})" />
:alt="$t("{actor}'s avatar", {actor: event.organizerActor.preferredUsername})" />
</figure>
</actor-link>
</div>
@ -120,7 +131,7 @@
<!-- <span>-->
<!-- <translate-->
<!-- :translate-n="event.participants.length"-->
<!-- translate-plural="%{event.participants.length} persons are going"-->
<!-- translate-plural="{event.participants.length} persons are going"-->
<!-- >-->
<!-- One person is going.-->
<!-- </translate>-->
@ -130,10 +141,10 @@
<div class="description">
<div class="description-container container">
<h3 class="title">
<translate>About this event</translate>
{{ $t('About this event') }}
</h3>
<p v-if="!event.description">
<translate>The event organizer didn't add any description.</translate>
{{ $t("The event organizer didn't add any description.") }}
</p>
<div class="columns" v-else>
<div class="column is-half">
@ -197,7 +208,7 @@
<div class="container">
<div class="columns">
<div class="column is-half has-text-centered">
<h3 class="title"><translate>Share this event</translate></h3>
<h3 class="title">{{ $t('Share this event') }}</h3>
<div>
<b-icon icon="mastodon" size="is-large" type="is-primary" />
<a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="facebook" size="is-large" type="is-primary" /></a>
@ -210,20 +221,26 @@
<hr />
<div class="column is-half has-text-right add-to-calendar">
<h3 @click="downloadIcsEvent()">
<translate>Add to my calendar</translate>
{{ $t('Add to my calendar') }}
</h3>
</div>
</div>
</div>
</section>
<section class="more-events container" v-if="event.relatedEvents.length > 0">
<h3 class="title has-text-centered"><translate>These events may interest you</translate></h3>
<h3 class="title has-text-centered">{{ $t('These events may interest you') }}</h3>
<div class="columns">
<div class="column is-one-third-desktop" v-for="relatedEvent in event.relatedEvents" :key="relatedEvent.uuid">
<EventCard :event="relatedEvent" />
</div>
</div>
</section>
<b-modal :active.sync="isReportModalActive" has-modal-card ref="reportModal">
<report-modal :on-confirm="reportEvent" title="Report this event" :outside-domain="event.organizerActor.domain" @close="$refs.reportModal.close()" />
</b-modal>
<b-modal :active.sync="isJoinModalActive" has-modal-card ref="participationModal">
<participation-modal :on-confirm="joinEvent" :event="event" :defaultIdentity="currentActor" @close="$refs.participationModal.close()" />
</b-modal>
</div>
</div>
</template>
@ -231,7 +248,7 @@
<script lang="ts">
import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
import { Component, Prop, Vue } from 'vue-property-decorator';
import { LOGGED_PERSON } from '@/graphql/actor';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { EventVisibility, IEvent, IParticipant } from '@/types/event.model';
import { IPerson } from '@/types/actor';
import { RouteName } from '@/router';
@ -241,6 +258,10 @@ import BIcon from 'buefy/src/components/icon/Icon.vue';
import EventCard from '@/components/Event/EventCard.vue';
import EventFullDate from '@/components/Event/EventFullDate.vue';
import ActorLink from '@/components/Account/ActorLink.vue';
import ReportModal from '@/components/Report/ReportModal.vue';
import ParticipationModal from '@/components/Event/ParticipationModal.vue';
import { IReport } from '@/types/report.model';
import { CREATE_REPORT } from '@/graphql/report';
@Component({
components: {
@ -249,6 +270,8 @@ import ActorLink from '@/components/Account/ActorLink.vue';
EventCard,
BIcon,
DateCalendarIcon,
ReportModal,
ParticipationModal,
// tslint:disable:space-in-parens
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
// tslint:enable
@ -262,8 +285,8 @@ import ActorLink from '@/components/Account/ActorLink.vue';
};
},
},
loggedPerson: {
query: LOGGED_PERSON,
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
@ -271,28 +294,57 @@ export default class Event extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
event!: IEvent;
loggedPerson!: IPerson;
currentActor!: IPerson;
validationSent: boolean = false;
showMap: boolean = false;
isReportModalActive: boolean = false;
isJoinModalActive: boolean = false;
EventVisibility = EventVisibility;
async deleteEvent() {
const router = this.$router;
const eventTitle = this.event.title;
async openDeleteEventModal () {
const participantsLength = this.event.participants.length;
const prefix = participantsLength
? this.$tc('There are {participants} participants.', this.event.participants.length, {
participants: this.event.participants.length,
})
: '';
this.$buefy.dialog.prompt({
type: 'is-danger',
title: this.$t('Delete event') as string,
message: `${prefix}
${this.$t('Are you sure you want to delete this event? This action cannot be reverted.')}
<br><br>
${this.$t('To confirm, type your event title "{eventTitle}"', { eventTitle: this.event.title })}`,
confirmText: this.$t(
'Delete {eventTitle}',
{ eventTitle: this.event.title },
) as string,
inputAttrs: {
placeholder: this.event.title,
pattern: this.event.title,
},
onConfirm: () => this.deleteEvent(),
});
}
async reportEvent(content: string, forward: boolean) {
this.isReportModalActive = false;
if (!this.event.organizerActor) return;
const eventTitle = this.event.title;
try {
await this.$apollo.mutate<IParticipant>({
mutation: DELETE_EVENT,
await this.$apollo.mutate<IReport>({
mutation: CREATE_REPORT,
variables: {
id: this.event.id,
actorId: this.loggedPerson.id,
eventId: this.event.id,
reporterActorId: this.currentActor.id,
reportedActorId: this.event.organizerActor.id,
content,
},
});
await router.push({ name: RouteName.HOME });
this.$buefy.notification.open({
message: this.$gettextInterpolate('Event %{eventTitle} deleted', { eventTitle }),
message: this.$t('Event {eventTitle} reported', { eventTitle }) as string,
type: 'is-success',
position: 'is-bottom-right',
duration: 5000,
@ -302,13 +354,14 @@ export default class Event extends Vue {
}
}
async joinEvent() {
async joinEvent(identity: IPerson) {
this.isJoinModalActive = false;
try {
await this.$apollo.mutate<{ joinEvent: IParticipant }>({
mutation: JOIN_EVENT,
variables: {
eventId: this.event.id,
actorId: this.loggedPerson.id,
actorId: identity.id,
},
update: (store, { data }) => {
if (data == null) return;
@ -330,13 +383,24 @@ export default class Event extends Vue {
}
}
confirmLeave() {
this.$buefy.dialog.confirm({
title: `Leaving event « ${this.event.title} »`,
message: `Are you sure you want to leave event « ${this.event.title} »`,
confirmText: 'Leave event',
type: 'is-danger',
hasIcon: true,
onConfirm: () => this.leaveEvent(),
});
}
async leaveEvent() {
try {
await this.$apollo.mutate<{ leaveEvent: IParticipant }>({
mutation: LEAVE_EVENT,
variables: {
eventId: this.event.id,
actorId: this.loggedPerson.id,
actorId: this.currentActor.id,
},
update: (store, { data }) => {
if (data == null) return;
@ -350,6 +414,7 @@ export default class Event extends Vue {
event.participants = event.participants
.filter(p => p.actor.id !== data.leaveEvent.actor.id);
event.participantStats.approved = event.participantStats.approved - 1;
store.writeQuery({ query: FETCH_EVENT, data: { event } });
},
@ -373,14 +438,14 @@ export default class Event extends Vue {
actorIsParticipant() {
if (this.actorIsOrganizer()) return true;
return this.loggedPerson &&
return this.currentActor &&
this.event.participants
.some(participant => participant.actor.id === this.loggedPerson.id);
.some(participant => participant.actor.id === this.currentActor.id);
}
actorIsOrganizer() {
return this.loggedPerson &&
this.loggedPerson.id === this.event.organizerActor.id;
return this.currentActor && this.event.organizerActor &&
this.currentActor.id === this.event.organizerActor.id;
}
get twitterShareUrl(): string {
@ -398,6 +463,32 @@ export default class Event extends Vue {
get emailShareUrl(): string {
return `mailto:?to=&body=${this.event.url}${encodeURIComponent('\n\n')}${this.event.description}&subject=${this.event.title}`;
}
private async deleteEvent() {
const router = this.$router;
const eventTitle = this.event.title;
try {
await this.$apollo.mutate<IParticipant>({
mutation: DELETE_EVENT,
variables: {
eventId: this.event.id,
actorId: this.currentActor.id,
},
});
await router.push({ name: RouteName.HOME });
this.$buefy.notification.open({
message: this.$t('Event {eventTitle} deleted', { eventTitle }) as string,
type: 'is-success',
position: 'is-bottom-right',
duration: 5000,
});
} catch (error) {
console.error(error);
}
}
}
</script>
<style lang="scss" scoped>

View File

@ -1,7 +1,7 @@
<template>
<section>
<h1>
<translate>Event list</translate>
{{ $t('Event list') }}
</h1>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="events.length > 0" class="columns is-multiline">
@ -13,7 +13,7 @@
/>
</div>
<b-message v-if-else="events.length === 0 && $apollo.loading === false" type="is-danger">
<translate>No events found</translate>
{{ $t('No events found') }}
</b-message>
</section>
</template>

View File

@ -1,17 +1,17 @@
<template>
<div class="root">
<h1 v-translate>Create a new group</h1>
<h1>{{ $t('Create a new group') }}</h1>
<div>
<b-field :label="$gettext('Group name')">
<b-field :label="$t('Group name')">
<b-input aria-required="true" required v-model="group.preferred_username"/>
</b-field>
<b-field :label="$gettext('Group full name')">
<b-field :label="$t('Group full name')">
<b-input aria-required="true" required v-model="group.name"/>
</b-field>
<b-field :label="$gettext('Description')">
<b-field :label="$t('Description')">
<b-input aria-required="true" required v-model="group.description" type="textarea"/>
</b-field>
@ -26,7 +26,7 @@
</div>
<button class="button is-primary" @click="createGroup()">
<translate>Create my group</translate>
{{ $t('Create my group') }}
</button>
</div>
</div>
@ -42,7 +42,7 @@
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { Group, IPerson } from '@/types/actor';
import { CREATE_GROUP, LOGGED_PERSON } from '@/graphql/actor';
import { CREATE_GROUP, CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { RouteName } from '@/router';
import PictureUpload from '@/components/PictureUpload.vue';
@ -51,13 +51,13 @@ import PictureUpload from '@/components/PictureUpload.vue';
PictureUpload,
},
apollo: {
loggedPerson: {
query: LOGGED_PERSON,
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
export default class CreateGroup extends Vue {
loggedPerson!: IPerson;
currentActor!: IPerson;
group = new Group();
@ -74,10 +74,10 @@ export default class CreateGroup extends Vue {
},
});
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.$gettextInterpolate('Group %{displayName} created', { displayName: this.group.displayName() }),
this.$t('Group {displayName} created', { displayName: this.group.displayName() }) as string,
);
} catch (err) {
this.handleError(err);
@ -111,7 +111,7 @@ export default class CreateGroup extends Vue {
}
const currentActor = {
creatorActorId: this.loggedPerson.id,
creatorActorId: this.currentActor.id,
};
return Object.assign({}, this.group, avatarObj, bannerObj, currentActor);

View File

@ -25,7 +25,7 @@
</div>
<section class="box" v-if="group.organizedEvents.length > 0">
<h2 class="subtitle">
<translate>Organized</translate>
{{ $t('Organized') }}
</h2>
<div class="columns">
<EventCard
@ -39,7 +39,7 @@
</section>
<section v-if="group.members.length > 0">
<h2 class="subtitle">
<translate>Members</translate>
{{ $t('Members') }}
</h2>
<div class="columns">
<span
@ -50,7 +50,7 @@
</section>
</div>
<b-message v-else-if="!group && $apollo.loading === false" type="is-danger">
<translate>No group found</translate>
{{ $t('No group found') }}
</b-message>
</section>
</template>
@ -58,7 +58,7 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue';
import { FETCH_GROUP, LOGGED_PERSON } from '@/graphql/actor';
import { FETCH_GROUP, CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IGroup } from '@/types/actor';
@Component({
@ -71,8 +71,8 @@ import { IGroup } from '@/types/actor';
};
},
},
loggedPerson: {
query: LOGGED_PERSON,
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
components: {

View File

@ -1,7 +1,7 @@
<template>
<section>
<h1>
<translate>Group List</translate>
{{ $t('Group List') }}
</h1>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div class="columns">
@ -13,7 +13,7 @@
/>
</div>
<router-link class="button" :to="{ name: RouteName.CREATE_GROUP }">
<translate>Create group</translate>
{{ $t('Create group') }}
</router-link>
</section>
</template>

View File

@ -6,64 +6,51 @@
<h1 class="title">{{ config.name }}</h1>
<h2 class="subtitle">{{ config.description }}</h2>
<router-link class="button" :to="{ name: 'Register' }" v-if="config.registrationsOpen">
<translate>Register</translate>
{{ $t('Register') }}
</router-link>
<p v-else>
<translate>This instance isn't opened to registrations, but you can register on other instances.</translate>
{{ $t("This instance isn't opened to registrations, but you can register on other instances.") }}
</p>
</div>
</div>
</section>
<section v-else>
<h1>
<translate
:translate-params="{username: loggedPerson.preferredUsername}"
>Welcome back %{username}</translate>
{{ $t('Welcome back {username}', {username: loggedPerson.preferredUsername}) }}
</h1>
</section>
<b-dropdown aria-role="list">
<button class="button is-primary" slot="trigger">
<span>Create</span>
<span>{{ $t('Create') }}</span>
<b-icon icon="menu-down"></b-icon>
</button>
<b-dropdown-item aria-role="listitem">
<router-link :to="{ name: RouteName.CREATE_EVENT }">Event</router-link>
<router-link :to="{ name: RouteName.CREATE_EVENT }">{{ $t('Event') }}</router-link>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">
<router-link :to="{ name: RouteName.CREATE_GROUP }">Group</router-link>
<router-link :to="{ name: RouteName.CREATE_GROUP }">{{ $t('Group') }}</router-link>
</b-dropdown-item>
<b-dropdown-item aria-role="listitem">Something else</b-dropdown-item>
</b-dropdown>
<section v-if="loggedPerson" class="container">
<span class="events-nearby title"><translate>Events you're going at</translate></span>
<span class="events-nearby title">
{{ $t("Events you're going at") }}
</span>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="goingToEvents.size > 0" v-for="row in Array.from(goingToEvents.entries())">
<!-- Iterators will be supported in v-for with VueJS 3 -->
<date-component :date="row[0]"></date-component>
<h3 class="subtitle"
v-if="isToday(row[0])"
v-translate="{count: row[1].length}"
:translate-n="row[1].length"
translate-plural="You have %{ count } events today"
>
You have one event today.
v-if="isToday(row[0])">
{{ $tc('You have one event today.', row[1].length, {count: row[1].length}) }}
</h3>
<h3 class="subtitle"
v-else-if="isTomorrow(row[0])"
v-translate="{count: row[1].length}"
:translate-n="row[1].length"
translate-plural="You have %{ count } events tomorrow"
>
You have one event tomorrow.
v-else-if="isTomorrow(row[0])">
{{ $tc('You have one event tomorrow.', row[1].length, {count: row[1].length}) }}
</h3>
<h3 class="subtitle"
v-else
v-translate="{count: row[1].length, days: calculateDiffDays(row[0])}"
:translate-n="row[1].length"
translate-plural="You have %{ count } events in %{ days } days"
>
You have one event in %{ days } days.
v-else>
{{ $tc('You have one event in {days} days.', row[1].length, {count: row[1].length, days: calculateDiffDays(row[0])}) }}
</h3>
<div class="columns">
<EventCard
@ -76,11 +63,11 @@
</div>
</div>
<b-message v-else type="is-danger">
<translate>You're not going to any event yet</translate>
{{ $t("You're not going to any event yet") }}
</b-message>
</section>
<section class="container">
<h3 class="events-nearby title"><translate>Events nearby you</translate></h3>
<h3 class="events-nearby title">{{ $t('Events nearby you') }}</h3>
<b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="events.length > 0" class="columns is-multiline">
<div class="column is-one-third-desktop" v-for="event in events.slice(0, 6)" :key="event.uuid">
@ -90,7 +77,7 @@
</div>
</div>
<b-message v-else type="is-danger">
<translate>No events found</translate>
{{ $t('No events found') }}
</b-message>
</section>
</div>

View File

@ -0,0 +1,80 @@
import {ReportStatusEnum} from "@/types/report.model";
<template>
<section class="container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: AdminRouteName.DASHBOARD }">Dashboard</router-link></li>
<li class="is-active"><router-link :to="{ name: ModerationRouteName.LOGS }" aria-current="page">Logs</router-link></li>
</ul>
</nav>
<ul v-if="actionLogs.length > 0">
<li v-for="log in actionLogs">
<div class="box">
<img class="image" :src="log.actor.avatar.url" />
<span>@{{ log.actor.preferredUsername }}</span>
<span v-if="log.action === ActionLogAction.REPORT_UPDATE_CLOSED">
closed <router-link :to="{ name: ModerationRouteName.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: ModerationRouteName.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: ModerationRouteName.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: ModerationRouteName.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>
</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 { AdminRouteName } from '@/router/admin';
import { ModerationRouteName } from '@/router/moderation';
@Component({
components: {
ReportCard,
},
apollo: {
actionLogs: {
query: LOGS,
},
},
})
export default class ReportList extends Vue {
actionLogs?: IActionLog[] = [];
ActionLogAction = ActionLogAction;
AdminRouteName = AdminRouteName;
ModerationRouteName = ModerationRouteName;
}
</script>
<style lang="scss">
.container li {
margin: 10px auto;
}
img.image {
display: inline;
height: 1.5em;
vertical-align: text-bottom;
}
</style>

View File

@ -0,0 +1,271 @@
<template>
<section class="container">
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<div class="container" v-if="report">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: AdminRouteName.DASHBOARD }">Dashboard</router-link></li>
<li><router-link :to="{ name: ModerationRouteName.REPORTS }">Reports</router-link></li>
<li class="is-active"><router-link :to="{ name: ModerationRouteName.REPORT, params: { reportId: this.report.id} }" aria-current="page">Report</router-link></li>
</ul>
</nav>
<div class="buttons">
<b-button v-if="report.status !== ReportStatusEnum.RESOLVED" @click="updateReport(ReportStatusEnum.RESOLVED)" type="is-primary">Mark as resolved</b-button>
<b-button v-if="report.status !== ReportStatusEnum.OPEN" @click="updateReport(ReportStatusEnum.OPEN)" type="is-success">Reopen</b-button>
<b-button v-if="report.status !== ReportStatusEnum.CLOSED" @click="updateReport(ReportStatusEnum.CLOSED)" type="is-danger">Close</b-button>
</div>
<div class="columns">
<div class="column">
<div class="table-container">
<table class="box table is-striped">
<tbody>
<tr>
<td>Compte signalé</td>
<td>
<router-link :to="{ name: ActorRouteName.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>Signalé par</td>
<td>
<router-link :to="{ name: ActorRouteName.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>Signalé</td>
<td>{{ report.insertedAt | formatDateTimeString }}</td>
</tr>
<tr v-if="report.updatedAt !== report.insertedAt">
<td>Mis à jour</td>
<td>{{ report.updatedAt | formatDateTimeString }}</td>
</tr>
<tr>
<td>Statut</td>
<td>
<span v-if="report.status === ReportStatusEnum.OPEN">Ouvert</span>
<span v-else-if="report.status === ReportStatusEnum.CLOSED">Fermé</span>
<span v-else-if="report.status === ReportStatusEnum.RESOLVED">Résolu</span>
<span v-else>Inconnu</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="column">
<div class="box">
<p v-if="report.content">{{ report.content }}</p>
<p v-else>Pas de commentaire</p>
</div>
</div>
</div>
<div class="box" v-if="report.event">
<router-link :to="{ name: EventRouteName.EVENT, params: { uuid: report.event.uuid }}">
<h3 class="title">{{ report.event.title }}</h3>
<p v-html="report.event.description"></p>
</router-link>
<b-button
tag="router-link"
type="is-primary"
:to="{ name: EventRouteName.EDIT_EVENT, params: {eventId: report.event.uuid } }"
icon-left="pencil"
size="is-small">Edit</b-button>
<b-button
type="is-danger"
@click="confirmDelete()"
icon-left="delete"
size="is-small">Delete</b-button>
</div>
<h2 class="title" v-if="report.notes.length > 0">Notes</h2>
<div class="box note" v-for="note in report.notes" :id="`note-${note.id}`">
<p>{{ note.content }}</p>
<router-link :to="{ name: ActorRouteName.PROFILE, params: { name: note.moderator.preferredUsername } }">
<img class="image" :src="note.moderator.avatar.url" /> @{{ 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="Nouvelle note">
<b-input type="textarea" v-model="noteContent"></b-input>
</b-field>
<b-button type="submit" @click="addNote">Ajouter une note</b-button>
</form>
</div>
</section>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import { CREATE_REPORT_NOTE, REPORT, REPORTS, UPDATE_REPORT } from '@/graphql/report';
import { IReport, IReportNote, ReportStatusEnum } from '@/types/report.model';
import { EventRouteName } from '@/router/event';
import { ActorRouteName } from '@/router/actor';
import { AdminRouteName } from '@/router/admin';
import { ModerationRouteName } from '@/router/moderation';
import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson } from '@/types/actor';
import { DELETE_EVENT } from '@/graphql/event';
import { uniq } from 'lodash';
@Component({
apollo: {
report: {
query: REPORT,
variables() {
return {
id: this.reportId,
};
},
error({ graphQLErrors }) {
this.errors = uniq(graphQLErrors.map(({ message }) => message));
},
},
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
})
export default class Report extends Vue {
@Prop({ required: true }) reportId!: number;
report!: IReport;
currentActor!: IPerson;
errors: string[] = [];
ReportStatusEnum = ReportStatusEnum;
EventRouteName = EventRouteName;
ActorRouteName = ActorRouteName;
AdminRouteName = AdminRouteName;
ModerationRouteName = ModerationRouteName;
noteContent: string = '';
addNote() {
try {
this.$apollo.mutate<{ createReportNote: IReportNote }>({
mutation: CREATE_REPORT_NOTE,
variables: {
reportId: this.report.id,
moderatorId: this.currentActor.id,
content: this.noteContent,
},
update: (store, { data }) => {
if (data == null) return;
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.');
return;
}
const note = data.createReportNote;
note.moderator = this.currentActor;
report.notes = report.notes.concat([note]);
store.writeQuery({ query: REPORT, data: { report } });
},
});
this.noteContent = '';
} catch (error) {
console.error(error);
}
}
confirmDelete() {
this.$buefy.dialog.confirm({
title: 'Deleting event',
message: '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.',
confirmText: 'Delete Event',
type: 'is-danger',
hasIcon: true,
onConfirm: () => this.deleteEvent(),
});
}
async deleteEvent() {
if (!this.report.event || !this.report.event.id) return;
const eventTitle = this.report.event.title;
try {
await this.$apollo.mutate({
mutation: DELETE_EVENT,
variables: {
eventId: this.report.event.id.toString(),
actorId: this.currentActor.id,
},
});
this.$buefy.notification.open({
message: this.$t('Event {eventTitle} deleted', { eventTitle }) as string,
type: 'is-success',
position: 'is-bottom-right',
duration: 5000,
});
} catch (error) {
console.error(error);
}
}
async updateReport(status: ReportStatusEnum) {
try {
await this.$apollo.mutate({
mutation: UPDATE_REPORT,
variables: {
reportId: this.report.id,
moderatorId: this.currentActor.id,
status,
},
update: (store, { data }) => {
if (data == null) return;
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.');
return;
}
const updatedReport = data.updateReportStatus;
report.status = updatedReport.status;
store.writeQuery({ query: REPORT, data: { report } });
},
});
} catch (error) {
console.error(error);
}
}
// TODO make me a global function
formatDate(value) {
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;
}
}
</script>
<style lang="scss">
.container li {
margin: 10px auto;
}
tbody td img.image, .note img.image {
display: inline;
height: 1.5em;
vertical-align: text-bottom;
}
.dialog .modal-card-foot {
justify-content: flex-end;
}
</style>

View File

@ -0,0 +1,90 @@
import {ReportStatusEnum} from "@/types/report.model";
<template>
<section class="container">
<nav class="breadcrumb" aria-label="breadcrumbs">
<ul>
<li><router-link :to="{ name: AdminRouteName.DASHBOARD }">Dashboard</router-link></li>
<li class="is-active"><router-link :to="{ name: ModerationRouteName.REPORTS }" aria-current="page">Reports</router-link></li>
</ul>
</nav>
<b-field>
<b-radio-button v-model="filterReports"
:native-value="ReportStatusEnum.OPEN">
Ouvert
</b-radio-button>
<b-radio-button v-model="filterReports"
:native-value="ReportStatusEnum.RESOLVED">
Résolus
</b-radio-button>
<b-radio-button v-model="filterReports"
:native-value="ReportStatusEnum.CLOSED">
Fermés
</b-radio-button>
</b-field>
<ul v-if="reports.length > 0">
<li v-for="report in reports">
<router-link :to="{ name: ModerationRouteName.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">No open reports yet</b-message>
<b-message v-if="filterReports === ReportStatusEnum.RESOLVED" type="is-info">No resolved reports yet</b-message>
<b-message v-if="filterReports === ReportStatusEnum.CLOSED" type="is-info">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 { AdminRouteName } from '@/router/admin';
import { ModerationRouteName } from '@/router/moderation';
@Component({
components: {
ReportCard,
},
apollo: {
reports: {
query: REPORTS,
fetchPolicy: 'no-cache',
variables() {
return {
status: this.filterReports,
};
},
pollInterval: 120000, // 2 minutes
},
},
})
export default class ReportList extends Vue {
reports?: IReport[] = [];
AdminRouteName = AdminRouteName;
ModerationRouteName = ModerationRouteName;
ReportStatusEnum = ReportStatusEnum;
filterReports: ReportStatusEnum = ReportStatusEnum.OPEN;
@Watch('$route.params.filter', { immediate: true })
onRouteFilterChanged (val: string) {
if (!val) return;
const filter = val.toUpperCase();
if (filter in ReportStatusEnum) {
this.filterReports = filter as ReportStatusEnum;
}
}
@Watch('filterReports', { immediate: true })
async onFilterChanged (val: string) {
await this.$router.push({ name: ModerationRouteName.REPORTS, params: { filter: val.toLowerCase() } });
}
}
</script>
<style lang="scss">
.container li {
margin: 10px auto;
}
</style>

View File

@ -4,20 +4,20 @@
<div class="column is-centered">
<img src="../assets/oh_no.jpg" alt="Not found 'oh no' picture">
<h1 class="title">
<translate>The page you're looking for doesn't exist.</translate>
{{ $t("The page you're looking for doesn't exist.") }}
</h1>
<p>
<translate>Please make sure the address is correct and that the page hasn't been moved.</translate>
{{ $t("Please make sure the address is correct and that the page hasn't been moved.") }}
</p>
<p>
<translate>Please contact this instance's Mobilizon admin if you think this is a mistake.</translate>
{{ $t("Please contact this instance's Mobilizon admin if you think this is a mistake.") }}
</p>
<!-- 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" />
<p class="control">
<button type="submit" class="button is-primary"><translate>Search</translate></button>
<button type="submit" class="button is-primary">{{ $t('Search') }}</button>
</p>
</b-field>
</form>
@ -39,7 +39,7 @@ export default class PageNotFound extends Vue {
searchText: string = '';
get searchPlaceHolder(): string {
return this.$gettext('Search events, groups, etc.');
return this.$t('Search events, groups, etc.') as string;
}
enter() {

View File

@ -1,14 +1,16 @@
<template>
<section class="container">
<h1>
<translate :translate-params="{ search: this.searchTerm }">Search results: « %{ search } »</translate>
{{ $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><translate>Events</translate> <b-tag rounded>{{ searchEvents.total }}</b-tag> </span>
<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"
@ -20,13 +22,15 @@
</div>
</div>
<b-message v-else-if="$apollo.loading === false" type="is-danger">
<translate>No events found</translate>
{{ $t('No events found') }}
</b-message>
</b-tab-item>
<b-tab-item>
<template slot="header">
<b-icon icon="account-multiple"></b-icon>
<span><translate>Groups</translate> <b-tag rounded>{{ searchGroups.total }}</b-tag> </span>
<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"
@ -36,7 +40,7 @@
</div>
</div>
<b-message v-else-if="$apollo.loading === false" type="is-danger">
<translate>No groups found</translate>
{{ $t('No groups found') }}
</b-message>
</b-tab-item>
</b-tabs>

View File

@ -2,12 +2,12 @@
<div class="container">
<section class="hero">
<h1 class="title">
<translate>Welcome back!</translate>
{{ $t('Welcome back!') }}
</h1>
</section>
<b-message v-if="errorCode === LoginErrorCode.NEED_TO_LOGIN" title="Info" type="is-info">
<translate>You need to login.</translate>
{{ $t('You need to login.') }}
</b-message>
<section v-if="!currentUser.isLoggedIn">
@ -15,11 +15,11 @@
<div class="column is-half">
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<form @submit="loginAction">
<b-field :label="$gettext('Email')">
<b-field :label="$t('Email')">
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
</b-field>
<b-field :label="$gettext('Password')">
<b-field :label="$t('Password')">
<b-input
aria-required="true"
required
@ -31,7 +31,7 @@
<div class="control has-text-centered">
<button class="button is-primary is-large">
<translate>Login</translate>
{{ $t('Login') }}
</button>
</div>
<div class="control">
@ -39,7 +39,7 @@
class="button is-text"
:to="{ name: 'SendPasswordReset', params: { email: credentials.email }}"
>
<translate>Forgot your password ?</translate>
{{ $t('Forgot your password ?') }}
</router-link>
</div>
<div class="control" v-if="config && config.registrationsOpen">
@ -47,7 +47,7 @@
class="button is-text"
:to="{ name: 'Register', params: { default_email: credentials.email, default_password: credentials.password }}"
>
<translate>Register</translate>
{{ $t('Register') }}
</router-link>
</div>
</form>
@ -56,7 +56,7 @@
</section>
<b-message v-else title="Error" type="is-error">
<translate>You are already logged-in.</translate>
{{ $t('You are already logged-in.') }}
</b-message>
</div>
</template>
@ -143,6 +143,7 @@ export default class Login extends Vue {
id: data.login.user.id,
email: this.credentials.email,
isLoggedIn: true,
role: data.login.user.role,
},
});

View File

@ -2,7 +2,7 @@
<section class="columns is-mobile is-centered">
<div class="card column is-half-desktop">
<h1>
<translate>Password reset</translate>
{{ $t('Password reset') }}
</h1>
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<form @submit="resetAction">
@ -27,7 +27,7 @@
/>
</b-field>
<button class="button is-primary">
<translate>Reset my password</translate>
{{ $t('Reset my password') }}
</button>
</form>
</div>

View File

@ -3,7 +3,7 @@
<section class="hero">
<div class="hero-body">
<h1 class="title">
<translate>Register an account on Mobilizon!</translate>
{{ $t('Register an account on Mobilizon!') }}
</h1>
</div>
</section>
@ -12,28 +12,27 @@
<div class="columns is-mobile">
<div class="column">
<div class="content">
<h3 class="title" v-translate>Features</h3>
<h3 class="title">{{ $t('Features') }}</h3>
<ul>
<li v-translate>Create your communities and your events</li>
<li v-translate>Other stuff</li>
<li>{{ $t('Create your communities and your events') }}</li>
<li>{{ $t('Other stuff…') }}</li>
</ul>
</div>
<p v-translate>
Learn more on
<i18n path="Learn more on" tag="p">
<a target="_blank" href="https://joinmobilizon.org">joinmobilizon.org</a>
</p>
</i18n>
<hr>
<div class="content">
<h3 class="title" v-translate>About this instance</h3>
<h3 class="title">{{ $t('About this instance') }}</h3>
<p>
<translate>Your local administrator resumed it's policy:</translate>
{{ $t("Your local administrator resumed it's policy:") }}
</p>
<ul>
<li v-translate>Please be nice to each other</li>
<li v-translate>meditate a bit</li>
<li>{{ $t('Please be nice to each other') }}</li>
<li>{{ $t('meditate a bit') }}</li>
</ul>
<p>
<translate>Please read the full rules</translate>
{{ $t('Please read the full rules') }}
</p>
</div>
</div>
@ -72,7 +71,7 @@
<b-field grouped>
<div class="control">
<button type="button" class="button is-primary" @click="submit()">
<translate>Register</translate>
{{ $t('Register') }}
</button>
</div>
<div class="control">
@ -80,7 +79,7 @@
class="button is-text"
:to="{ name: 'ResendConfirmation', params: { email: credentials.email }}"
>
<translate>Didn't receive the instructions ?</translate>
{{ $t("Didn't receive the instructions ?") }}
</router-link>
</div>
<div class="control">
@ -89,7 +88,7 @@
:to="{ name: 'Login', params: { email: credentials.email, password: credentials.password }}"
:disabled="sendingValidation"
>
<translate>Login</translate>
{{ $t('Login') }}
</router-link>
</div>
</b-field>

View File

@ -2,24 +2,22 @@
<section class="container">
<div class="column">
<h1 class="title">
<translate>Resend confirmation email</translate>
{{ $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-field>
<button class="button is-primary">
<translate>Send confirmation email again</translate>
{{ $t('Send confirmation email again') }}
</button>
</form>
<div v-else>
<b-message type="is-success" :closable="false" title="Success">
<translate
:translate-params="{email: credentials.email}"
>If an account with this email exists, we just sent another confirmation email to %{email}</translate>
{{ $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">
<translate>Please check you spam folder if you didn't receive the email.</translate>
{{ $t("Please check you spam folder if you didn't receive the email.") }}
</b-message>
</div>
</div>

View File

@ -2,7 +2,7 @@
<section class="container">
<div class="column">
<h1 class="title">
<translate>Password reset</translate>
{{ $t('Password reset') }}
</h1>
<b-message title="Error" type="is-danger" v-for="error in errors" :key="error">{{ error }}</b-message>
<form @submit="sendResetPasswordTokenAction" v-if="!validationSent">
@ -10,17 +10,15 @@
<b-input aria-required="true" required type="email" v-model="credentials.email"/>
</b-field>
<button class="button is-primary">
<translate>Send email to reset my password</translate>
{{ $t('Send email to reset my password') }}
</button>
</form>
<div v-else>
<b-message type="is-success" :closable="false" title="Success">
<translate
:translate-params="{email: credentials.email}"
>We just sent an email to %{email}</translate>
{{ $t('We just sent an email to {email}', {email: credentials.email}) }}
</b-message>
<b-message type="is-info">
<translate>Please check you spam folder if you didn't receive the email.</translate>
{{ $t("Please check you spam folder if you didn't receive the email.") }}
</b-message>
</div>
</div>

View File

@ -1,16 +1,16 @@
<template>
<section>
<h1 class="title" v-if="loading">
<translate>Your account is being validated</translate>
{{ $t('Your account is being validated') }}
</h1>
<div v-else>
<div v-if="failed">
<b-message :title="$gettext('Error while validating account')" type="is-danger">
<translate>Either the account is already validated, either the validation token is incorrect.</translate>
<b-message :title="$t('Error while validating account')" type="is-danger">
{{ $t('Either the account is already validated, either the validation token is incorrect.') }}
</b-message>
</div>
<h1 class="title" v-else>
<translate>Your account has been validated</translate>
{{ $t('Your account has been validated') }}
</h1>
</div>
</section>

View File

@ -99,7 +99,9 @@ const link = authMiddleware
.concat(errorLink)
.concat(uploadLink);
const cache = new InMemoryCache({ fragmentMatcher });
const cache = new InMemoryCache({
fragmentMatcher,
});
const apolloClient = new ApolloClient({
cache,

View File

@ -2,6 +2,7 @@
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"resolveJsonModule": true,
"strict": true,
"jsx": "preserve",
"importHelpers": true,

View File

@ -795,18 +795,6 @@
error-stack-parser "^2.0.0"
string-width "^2.0.0"
"@types/babel-types@*", "@types/babel-types@^7.0.0":
version "7.0.7"
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
integrity sha512-dBtBbrc+qTHy1WdfHYjBwRln4+LWqASWakLHsWHR2NWHIFkv4W3O070IGoGLEBrJBvct3r0L1BUPuvURi7kYUQ==
"@types/babylon@^6.16.2":
version "6.16.5"
resolved "https://registry.yarnpkg.com/@types/babylon/-/babylon-6.16.5.tgz#1c5641db69eb8cdf378edd25b4be7754beeb48b4"
integrity sha512-xH2e58elpj1X4ynnKp9qSnWlsRTIs6n3tgLGNfwAGHwePw0mulHQllV34n0T25uYSu1k0hRKkWXF890B1yS47w==
dependencies:
"@types/babel-types" "*"
"@types/chai@^4.1.7":
version "4.2.0"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.0.tgz#2478260021408dec32c123a7cad3414beb811a07"
@ -1156,21 +1144,6 @@
semver "^6.0.0"
string.prototype.padstart "^3.0.0"
"@vue/component-compiler-utils@^1.2.1":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.3.1.tgz#686f0b913d59590ae327b2a1cb4b6d9b931bbe0e"
integrity sha512-IyjJW6ToMitgAhp3xh22QiEW8JvHfLyzlyY/J+GjJ71miod9tNsy6xT2ckm/VirlhPMfeM43kgYZe34jhmmzpw==
dependencies:
consolidate "^0.15.1"
hash-sum "^1.0.2"
lru-cache "^4.1.2"
merge-source-map "^1.1.0"
postcss "^6.0.20"
postcss-selector-parser "^3.1.1"
prettier "^1.13.0"
source-map "^0.5.6"
vue-template-es2015-compiler "^1.6.0"
"@vue/component-compiler-utils@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.0.0.tgz#d16fa26b836c06df5baaeb45f3d80afc47e35634"
@ -1383,11 +1356,6 @@
resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==
"@yarnpkg/lockfile@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31"
integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==
abab@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.1.tgz#3fa17797032b71410ec372e11668f4b4ffc86a82"
@ -1406,34 +1374,6 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7:
mime-types "~2.1.24"
negotiator "0.6.2"
acorn-bigint@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/acorn-bigint/-/acorn-bigint-0.2.0.tgz#0f45a5290537799a3b07085689a186881cb53784"
integrity sha1-D0WlKQU3eZo7BwhWiaGGiBy1N4Q=
dependencies:
acorn "^5.2.1"
acorn-class-fields@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/acorn-class-fields/-/acorn-class-fields-0.1.2.tgz#20782f304af42257feff5bd4a5c335291473bf58"
integrity sha1-IHgvMEr0Ilf+/1vUpcM1KRRzv1g=
dependencies:
acorn "^5.3.0"
acorn-dynamic-import@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/acorn-dynamic-import/-/acorn-dynamic-import-3.0.0.tgz#901ceee4c7faaef7e07ad2a47e890675da50a278"
integrity sha512-zVWV8Z8lislJoOKKqdNMOB+s6+XV5WERty8MnKBeFgwA+19XJjJHs2RP5dzM57FftIs+jQnRToLiWazKr6sSWg==
dependencies:
acorn "^5.0.0"
acorn-globals@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-3.1.0.tgz#fd8270f71fbb4996b004fa880ee5d46573a731bf"
integrity sha1-/YJw9x+7SZawBPqIDuXUZXOnMb8=
dependencies:
acorn "^4.0.4"
acorn-globals@^4.3.0:
version "4.3.3"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.3.tgz#a86f75b69680b8780d30edd21eee4e0ea170c05e"
@ -1442,81 +1382,16 @@ acorn-globals@^4.3.0:
acorn "^6.0.1"
acorn-walk "^6.0.1"
acorn-import-meta@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/acorn-import-meta/-/acorn-import-meta-0.2.1.tgz#ac91e06e00facece7e96ff76a0fe9ec7b1cb5b5c"
integrity sha512-+KB5Q0P0Q/XpsPHgnLx4XbCGqMogw4yiJJjYsbzPCNrE/IoX+c6J4C+BFcwdWh3CD1zLzMxPITN1jzHd+NiS3w==
dependencies:
acorn "^5.4.1"
acorn-json-superset@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/acorn-json-superset/-/acorn-json-superset-0.1.1.tgz#61222bfdb6bd0a825c05d5550135729076c2cb5a"
integrity sha512-fhvg6mWlulil3spkNL0UQtym0pLAaKsKWmDGuTKlP5PVQwv9DlR1avvnnwl2YT9A61AH5j0idgv5/h9Rdkaqyg==
dependencies:
acorn "^5.4.1"
acorn-jsx@^5.0.2:
version "5.0.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f"
integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==
acorn-numeric-separator@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/acorn-numeric-separator/-/acorn-numeric-separator-0.1.1.tgz#aa455a1d95ae887231de97e0681abbe28b065e8d"
integrity sha1-qkVaHZWuiHIx3pfgaBq74osGXo0=
dependencies:
acorn "^5.2.1"
acorn-optional-catch-binding@^0.1.0:
version "0.1.1"
resolved "https://registry.yarnpkg.com/acorn-optional-catch-binding/-/acorn-optional-catch-binding-0.1.1.tgz#593d8c0a51ae3a3404b3bb84ee40180b808e7548"
integrity sha512-LJn5iDpAU1Zah1sdG2pY4rwv7kSe7ykbKpYrwbw5Igfn3OgPyjSD5f0JPboA1xITYpENS9rtNgN7PaAtTsvI/g==
dependencies:
acorn "^5.2.1"
acorn-private-methods@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/acorn-private-methods/-/acorn-private-methods-0.1.1.tgz#32c13cf24d05bf1c9be04914b41491c59d75a195"
integrity sha1-MsE88k0Fvxyb4EkUtBSRxZ11oZU=
dependencies:
acorn "^5.4.0"
acorn-stage3@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/acorn-stage3/-/acorn-stage3-0.6.0.tgz#d2814cec8e2f8bcb0407ba657fbe0cfb118f9bc2"
integrity sha512-/CZrHonJfg5OSTkZ71w4L4JnpsqZyDIXaSot5gUpQriTUavjiuAjkJBxxNGtxTlGBVtOBtYwzqxLDUSOD3amDQ==
dependencies:
acorn "^5.5.0"
acorn-bigint "^0.2.0"
acorn-class-fields "^0.1.1"
acorn-dynamic-import "^3.0.0"
acorn-import-meta "^0.2.1"
acorn-json-superset "^0.1.0"
acorn-numeric-separator "^0.1.1"
acorn-optional-catch-binding "^0.1.0"
acorn-private-methods "^0.1.1"
acorn-walk@^6.0.1, acorn-walk@^6.1.1:
version "6.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==
acorn@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
integrity sha1-ReN/s56No/JbruP/U2niu18iAXo=
acorn@^4.0.4, acorn@~4.0.2:
version "4.0.13"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.13.tgz#105495ae5361d697bd195c825192e1ad7f253787"
integrity sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=
acorn@^5.0.0, acorn@^5.2.1, acorn@^5.3.0, acorn@^5.4.0, acorn@^5.4.1, acorn@^5.5.0, acorn@^5.5.3:
version "5.7.3"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279"
integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==
acorn@^6.0.1, acorn@^6.0.4, acorn@^6.0.7, acorn@^6.1.1, acorn@^6.2.1:
version "6.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e"
@ -1577,15 +1452,6 @@ ajv@^6.1.0, ajv@^6.10.0, ajv@^6.10.2, ajv@^6.5.5:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"
align-text@^0.1.1, align-text@^0.1.3:
version "0.1.4"
resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
integrity sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=
dependencies:
kind-of "^3.0.2"
longest "^1.0.1"
repeat-string "^1.5.2"
alphanum-sort@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3"
@ -1907,11 +1773,6 @@ array-unique@^0.3.2:
resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=
asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
asn1.js@^4.0.0:
version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0"
@ -2091,16 +1952,6 @@ babel-runtime@^6.18.0, babel-runtime@^6.25.0, babel-runtime@^6.26.0:
core-js "^2.4.0"
regenerator-runtime "^0.11.0"
babel-types@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
integrity sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=
dependencies:
babel-runtime "^6.26.0"
esutils "^2.0.2"
lodash "^4.17.4"
to-fast-properties "^1.0.3"
babylon@^6.18.0:
version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
@ -2513,11 +2364,6 @@ camelcase-keys@^2.0.0:
camelcase "^2.0.0"
map-obj "^1.0.0"
camelcase@^1.0.2:
version "1.2.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=
camelcase@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
@ -2568,14 +2414,6 @@ caseless@~0.12.0:
resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=
center-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/center-align/-/center-align-0.1.3.tgz#aa0d32629b6ee972200411cbd4461c907bc2b7ad"
integrity sha1-qg0yYptu6XIgBBHL1EYckHvCt60=
dependencies:
align-text "^0.1.3"
lazy-cache "^1.0.3"
chai-nightwatch@~0.1.x:
version "0.1.1"
resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9"
@ -2649,13 +2487,6 @@ change-case@^3.0.1:
upper-case "^1.1.1"
upper-case-first "^1.1.0"
character-parser@^2.1.1:
version "2.2.0"
resolved "https://registry.yarnpkg.com/character-parser/-/character-parser-2.2.0.tgz#c7ce28f36d4bcd9744e5ffc2c5fcde1c73261fc0"
integrity sha1-x84o821LzZdE5f/CxfzeHHMmH8A=
dependencies:
is-regex "^1.0.3"
chardet@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
@ -2671,18 +2502,6 @@ check-types@^8.0.3:
resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ==
cheerio@^1.0.0-rc.2:
version "1.0.0-rc.3"
resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6"
integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA==
dependencies:
css-select "~1.2.0"
dom-serializer "~0.1.1"
entities "~1.1.1"
htmlparser2 "^3.9.1"
lodash "^4.15.0"
parse5 "^3.0.1"
chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.6:
version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917"
@ -2730,11 +2549,6 @@ ci-info@^1.5.0:
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==
ci-info@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-2.0.0.tgz#67a9e964be31a51e15e5010d58e6f12834002f46"
integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==
cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de"
@ -2753,7 +2567,7 @@ class-utils@^0.3.5:
isobject "^3.0.0"
static-extend "^0.1.1"
clean-css@4.2.x, clean-css@^4.1.11:
clean-css@4.2.x:
version "4.2.1"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g==
@ -2788,6 +2602,16 @@ cli-spinners@^2.0.0:
resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77"
integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ==
cli-table3@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/cli-table3/-/cli-table3-0.5.1.tgz#0252372d94dfc40dbd8df06005f48f31f656f202"
integrity sha512-7Qg2Jrep1S/+Q3EceiZtQcDPWxhAvBw+ERf1162v4sikJrvojMHFqXt8QIVha8UlH9rgU0BeWPytZ9/TzYqlUw==
dependencies:
object-assign "^4.1.0"
string-width "^2.1.1"
optionalDependencies:
colors "^1.1.2"
cli-width@^2.0.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
@ -2801,15 +2625,6 @@ clipboardy@^2.0.0:
arch "^2.1.1"
execa "^1.0.0"
cliui@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1"
integrity sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=
dependencies:
center-align "^0.1.1"
right-align "^0.1.1"
wordwrap "0.0.2"
cliui@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
@ -2916,6 +2731,11 @@ color@^3.0.0:
color-convert "^1.9.1"
color-string "^1.5.2"
colors@^1.1.2:
version "1.3.3"
resolved "https://registry.yarnpkg.com/colors/-/colors-1.3.3.tgz#39e005d546afe01e01f9c4ca8fa50f686a01205d"
integrity sha512-mmGt/1pZqYRjMxB1axhTo16/snVZ5krrKkcmMeVKxzECMMXoCgnvTPp10QgHfcbQZw8Dq2jMNG6je4JlWU0gWg==
columnify@^1.5.4:
version "1.5.4"
resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb"
@ -3057,16 +2877,6 @@ constant-case@^2.0.0:
snake-case "^2.1.0"
upper-case "^1.1.1"
constantinople@^3.0.1, constantinople@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/constantinople/-/constantinople-3.1.2.tgz#d45ed724f57d3d10500017a7d3a889c1381ae647"
integrity sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==
dependencies:
"@types/babel-types" "^7.0.0"
"@types/babylon" "^6.16.2"
babel-types "^6.26.0"
babylon "^6.18.0"
constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@ -3299,7 +3109,7 @@ css-select-base-adapter@^0.1.1:
resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
css-select@^1.1.0, css-select@~1.2.0:
css-select@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
@ -3555,7 +3365,7 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
dependencies:
ms "^2.1.1"
decamelize@^1.0.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@ -3803,11 +3613,6 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
doctypes@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/doctypes/-/doctypes-1.1.0.tgz#ea80b106a87538774e8a3a4a5afe293de489e0a9"
integrity sha1-6oCxBqh1OHdOijpKWv4pPeSJ4Kk=
dom-converter@^0.2:
version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
@ -3828,20 +3633,12 @@ dom-serializer@0:
domelementtype "^2.0.1"
entities "^2.0.0"
dom-serializer@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0"
integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==
dependencies:
domelementtype "^1.3.0"
entities "^1.1.1"
domain-browser@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==
domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1:
domelementtype@1, domelementtype@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@ -3888,6 +3685,14 @@ dot-case@^2.1.0:
dependencies:
no-case "^2.2.0"
dot-object@^1.7.1:
version "1.9.0"
resolved "https://registry.yarnpkg.com/dot-object/-/dot-object-1.9.0.tgz#6e3d6d8379f794c5174599ddf05528f5990f076e"
integrity sha512-7MPN6y7XhAO4vM4eguj5+5HNKLjJYfkVG1ZR1Aput4Q4TR6SYeSjhpVQ77IzJHoSHffKbDxBC+48aCiiRurDPw==
dependencies:
commander "^2.20.0"
glob "^7.1.4"
dot-prop@^4.1.0, dot-prop@^4.1.1:
version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57"
@ -3954,20 +3759,6 @@ easy-stack@^1.0.0:
resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.0.tgz#12c91b3085a37f0baa336e9486eac4bf94e3e788"
integrity sha1-EskbMIWjfwuqM26UhurEv5Tj54g=
easygettext@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/easygettext/-/easygettext-2.7.0.tgz#35eecf687f817baa10d2fd5dc66ef47caade56d5"
integrity sha512-BaoyxsZtre7Ndvgz3utjrE/6Yo8Txsc4m33ehQ0pBNX3HjcjGQozDhnpqSRhaeD8PQAk0Rgq3vhI+YJvQu0vUQ==
dependencies:
"@vue/component-compiler-utils" "^1.2.1"
acorn "^5.5.3"
acorn-stage3 "^0.6.0"
cheerio "^1.0.0-rc.2"
minimist "^1.2.0"
pofile "^1.0.10"
pug "^2.0.3"
vue-template-compiler "^2.5.16"
ecc-jsbn@~0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9"
@ -4054,7 +3845,7 @@ enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0:
memory-fs "^0.4.0"
tapable "^1.0.0"
entities@^1.1.1, entities@~1.1.1:
entities@^1.1.1:
version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
@ -4211,6 +4002,11 @@ eslint@^6.0.1:
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
esm@^3.2.13:
version "3.2.25"
resolved "https://registry.yarnpkg.com/esm/-/esm-3.2.25.tgz#342c18c29d56157688ba5ce31f8431fbb795cc10"
integrity sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==
espree@^6.1.1:
version "6.1.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de"
@ -4658,14 +4454,6 @@ find-up@^3.0.0:
dependencies:
locate-path "^3.0.0"
find-yarn-workspace-root@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-1.2.1.tgz#40eb8e6e7c2502ddfaa2577c176f221422f860db"
integrity sha512-dVtfb0WuQG+8Ag2uWkbG79hOUzEsRrhBzgfn86g2sJPkzmcpGdghbNTfUKGTxymFrY/tLIodDzLoW9nOJ4FY8Q==
dependencies:
fs-extra "^4.0.3"
micromatch "^3.1.4"
find@^0.2.7:
version "0.2.9"
resolved "https://registry.yarnpkg.com/find/-/find-0.2.9.tgz#4b73f1ff9e56ad91b76e716407fe5ffe6554bb8c"
@ -5540,7 +5328,7 @@ html-webpack-plugin@^3.2.0:
toposort "^1.0.0"
util.promisify "1.0.0"
htmlparser2@^3.3.0, htmlparser2@^3.9.1:
htmlparser2@^3.3.0:
version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@ -5976,13 +5764,6 @@ is-ci@^1.0.10:
dependencies:
ci-info "^1.5.0"
is-ci@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-2.0.0.tgz#6bc6334181810e04b5c22b3d589fdca55026404c"
integrity sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==
dependencies:
ci-info "^2.0.0"
is-color-stop@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345"
@ -6037,14 +5818,6 @@ is-directory@^0.3.1:
resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=
is-expression@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/is-expression/-/is-expression-3.0.0.tgz#39acaa6be7fd1f3471dc42c7416e61c24317ac9f"
integrity sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=
dependencies:
acorn "~4.0.2"
object-assign "^4.0.1"
is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89"
@ -6177,7 +5950,7 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4:
dependencies:
isobject "^3.0.1"
is-promise@^2.0.0, is-promise@^2.1.0:
is-promise@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=
@ -6187,7 +5960,7 @@ is-redirect@^1.0.0:
resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
is-regex@^1.0.3, is-regex@^1.0.4:
is-regex@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=
@ -6262,6 +6035,11 @@ is-utf8@^0.2.0:
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=
is-valid-glob@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-valid-glob/-/is-valid-glob-1.0.0.tgz#29bf3eff701be2d4d315dbacc39bc39fe8f601aa"
integrity sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=
is-windows@^1.0.1, is-windows@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -6369,11 +6147,6 @@ js-queue@2.0.0:
dependencies:
easy-stack "^1.0.0"
js-stringify@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/js-stringify/-/js-stringify-1.0.2.tgz#1736fddfd9724f28a3682adc6230ae7e4e9679db"
integrity sha1-Fzb939lyTyijaCrcYjCufk6Weds=
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -6567,14 +6340,6 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
jstransformer@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/jstransformer/-/jstransformer-1.0.0.tgz#ed8bf0921e2f3f1ed4d5c1a44f68709ed24722c3"
integrity sha1-7Yvwkh4vPx7U1cGkT2hwntJHIsM=
dependencies:
is-promise "^2.0.0"
promise "^7.0.1"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
@ -6621,13 +6386,6 @@ kind-of@^6.0.0, kind-of@^6.0.2:
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==
klaw-sync@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/klaw-sync/-/klaw-sync-6.0.0.tgz#1fd2cfd56ebb6250181114f0a581167099c2b28c"
integrity sha512-nIeuVSzdCCs6TDPTqI8w1Yre34sSq7AkZ4B3sfOBbI2CgVSB4Du4aLQijFU2+lhAFCwt9+42Hel6lQNIv6AntQ==
dependencies:
graceful-fs "^4.1.11"
latest-version@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
@ -6650,11 +6408,6 @@ launch-editor@^2.2.1:
chalk "^2.3.0"
shell-quote "^1.6.1"
lazy-cache@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/lazy-cache/-/lazy-cache-1.0.4.tgz#a1d78fc3a50474cb80845d3b3b6e1da49a446e8e"
integrity sha1-odePw6UEdMuAhF07O24dpJpEbo4=
lcid@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835"
@ -6997,7 +6750,7 @@ lodash@4.17.5:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==
lodash@^4.0.0, lodash@^4.15.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10:
lodash@^4.0.0, lodash@^4.16.4, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.2, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.2.0, lodash@^4.3.0, lodash@~4.17.10:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
@ -7014,11 +6767,6 @@ loglevel@^1.6.3:
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280"
integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA==
longest@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
loose-envify@^1.0.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
@ -8315,13 +8063,6 @@ parse5@5.1.0:
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==
parse5@^3.0.1:
version "3.0.3"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c"
integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==
dependencies:
"@types/node" "*"
parse5@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608"
@ -8345,25 +8086,6 @@ pascalcase@^0.1.1:
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=
patch-package@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/patch-package/-/patch-package-6.1.2.tgz#9ed0b3defb5c34ecbef3f334ddfb13e01b3d3ff6"
integrity sha512-5GnzR8lEyeleeariG+hGabUnD2b1yL7AIGFjlLo95zMGRWhZCel58IpeKD46wwPb7i+uNhUI8unV56ogk8Bgqg==
dependencies:
"@yarnpkg/lockfile" "^1.1.0"
chalk "^2.4.2"
cross-spawn "^6.0.5"
find-yarn-workspace-root "^1.2.1"
fs-extra "^7.0.1"
is-ci "^2.0.0"
klaw-sync "^6.0.0"
minimist "^1.2.0"
rimraf "^2.6.3"
semver "^5.6.0"
slash "^2.0.0"
tmp "^0.0.33"
update-notifier "^2.5.0"
path-browserify@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a"
@ -8532,11 +8254,6 @@ pn@^1.1.0:
resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==
pofile@^1.0.10:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.0.tgz#9ce84bbef5043ceb4f19bdc3520d85778fad4f94"
integrity sha512-6XYcNkXWGiJ2CVXogTP7uJ6ZXQCldYLZc16wgRp8tqRaBTTyIfF+TUT3EQJPXTLAT7OTPpTAoaFdoXKfaTRU1w==
popper.js@^1.14.7:
version "1.15.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2"
@ -8834,7 +8551,7 @@ postcss-reduce-transforms@^4.0.2:
postcss "^7.0.0"
postcss-value-parser "^3.0.0"
postcss-selector-parser@^3.0.0, postcss-selector-parser@^3.1.1:
postcss-selector-parser@^3.0.0:
version "3.1.1"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU=
@ -8881,7 +8598,7 @@ postcss-value-parser@^4.0.0:
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9"
integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ==
postcss@^6.0.1, postcss@^6.0.20, postcss@^6.0.23:
postcss@^6.0.1, postcss@^6.0.23:
version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==
@ -8919,11 +8636,6 @@ prettier@1.16.3:
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==
prettier@^1.13.0:
version "1.18.2"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea"
integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==
pretty-bytes@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
@ -8991,13 +8703,6 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
promise@^7.0.1:
version "7.3.1"
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
dependencies:
asap "~2.0.3"
prosemirror-collab@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.1.2.tgz#622fdc52692a83045ba6914c01a0416ff35f646a"
@ -9167,111 +8872,6 @@ public-encrypt@^4.0.0:
randombytes "^2.0.1"
safe-buffer "^5.1.2"
pug-attrs@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pug-attrs/-/pug-attrs-2.0.4.tgz#b2f44c439e4eb4ad5d4ef25cac20d18ad28cc336"
integrity sha512-TaZ4Z2TWUPDJcV3wjU3RtUXMrd3kM4Wzjbe3EWnSsZPsJ3LDI0F3yCnf2/W7PPFF+edUFQ0HgDL1IoxSz5K8EQ==
dependencies:
constantinople "^3.0.1"
js-stringify "^1.0.1"
pug-runtime "^2.0.5"
pug-code-gen@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/pug-code-gen/-/pug-code-gen-2.0.2.tgz#ad0967162aea077dcf787838d94ed14acb0217c2"
integrity sha512-kROFWv/AHx/9CRgoGJeRSm+4mLWchbgpRzTEn8XCiwwOy6Vh0gAClS8Vh5TEJ9DBjaP8wCjS3J6HKsEsYdvaCw==
dependencies:
constantinople "^3.1.2"
doctypes "^1.1.0"
js-stringify "^1.0.1"
pug-attrs "^2.0.4"
pug-error "^1.3.3"
pug-runtime "^2.0.5"
void-elements "^2.0.1"
with "^5.0.0"
pug-error@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/pug-error/-/pug-error-1.3.3.tgz#f342fb008752d58034c185de03602dd9ffe15fa6"
integrity sha512-qE3YhESP2mRAWMFJgKdtT5D7ckThRScXRwkfo+Erqga7dyJdY3ZquspprMCj/9sJ2ijm5hXFWQE/A3l4poMWiQ==
pug-filters@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/pug-filters/-/pug-filters-3.1.1.tgz#ab2cc82db9eeccf578bda89130e252a0db026aa7"
integrity sha512-lFfjNyGEyVWC4BwX0WyvkoWLapI5xHSM3xZJFUhx4JM4XyyRdO8Aucc6pCygnqV2uSgJFaJWW3Ft1wCWSoQkQg==
dependencies:
clean-css "^4.1.11"
constantinople "^3.0.1"
jstransformer "1.0.0"
pug-error "^1.3.3"
pug-walk "^1.1.8"
resolve "^1.1.6"
uglify-js "^2.6.1"
pug-lexer@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/pug-lexer/-/pug-lexer-4.1.0.tgz#531cde48c7c0b1fcbbc2b85485c8665e31489cfd"
integrity sha512-i55yzEBtjm0mlplW4LoANq7k3S8gDdfC6+LThGEvsK4FuobcKfDAwt6V4jKPH9RtiE3a2Akfg5UpafZ1OksaPA==
dependencies:
character-parser "^2.1.1"
is-expression "^3.0.0"
pug-error "^1.3.3"
pug-linker@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/pug-linker/-/pug-linker-3.0.6.tgz#f5bf218b0efd65ce6670f7afc51658d0f82989fb"
integrity sha512-bagfuHttfQOpANGy1Y6NJ+0mNb7dD2MswFG2ZKj22s8g0wVsojpRlqveEQHmgXXcfROB2RT6oqbPYr9EN2ZWzg==
dependencies:
pug-error "^1.3.3"
pug-walk "^1.1.8"
pug-load@^2.0.12:
version "2.0.12"
resolved "https://registry.yarnpkg.com/pug-load/-/pug-load-2.0.12.tgz#d38c85eb85f6e2f704dea14dcca94144d35d3e7b"
integrity sha512-UqpgGpyyXRYgJs/X60sE6SIf8UBsmcHYKNaOccyVLEuT6OPBIMo6xMPhoJnqtB3Q3BbO4Z3Bjz5qDsUWh4rXsg==
dependencies:
object-assign "^4.1.0"
pug-walk "^1.1.8"
pug-parser@^5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/pug-parser/-/pug-parser-5.0.1.tgz#03e7ada48b6840bd3822f867d7d90f842d0ffdc9"
integrity sha512-nGHqK+w07p5/PsPIyzkTQfzlYfuqoiGjaoqHv1LjOv2ZLXmGX1O+4Vcvps+P4LhxZ3drYSljjq4b+Naid126wA==
dependencies:
pug-error "^1.3.3"
token-stream "0.0.1"
pug-runtime@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/pug-runtime/-/pug-runtime-2.0.5.tgz#6da7976c36bf22f68e733c359240d8ae7a32953a"
integrity sha512-P+rXKn9un4fQY77wtpcuFyvFaBww7/91f3jHa154qU26qFAnOe6SW1CbIDcxiG5lLK9HazYrMCCuDvNgDQNptw==
pug-strip-comments@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/pug-strip-comments/-/pug-strip-comments-1.0.4.tgz#cc1b6de1f6e8f5931cf02ec66cdffd3f50eaf8a8"
integrity sha512-i5j/9CS4yFhSxHp5iKPHwigaig/VV9g+FgReLJWWHEHbvKsbqL0oP/K5ubuLco6Wu3Kan5p7u7qk8A4oLLh6vw==
dependencies:
pug-error "^1.3.3"
pug-walk@^1.1.8:
version "1.1.8"
resolved "https://registry.yarnpkg.com/pug-walk/-/pug-walk-1.1.8.tgz#b408f67f27912f8c21da2f45b7230c4bd2a5ea7a"
integrity sha512-GMu3M5nUL3fju4/egXwZO0XLi6fW/K3T3VTgFQ14GxNi8btlxgT5qZL//JwZFm/2Fa64J/PNS8AZeys3wiMkVA==
pug@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/pug/-/pug-2.0.4.tgz#ee7682ec0a60494b38d48a88f05f3b0ac931377d"
integrity sha512-XhoaDlvi6NIzL49nu094R2NA6P37ijtgMDuWE+ofekDChvfKnzFal60bhSdiy8y2PBO6fmz3oMEIcfpBVRUdvw==
dependencies:
pug-code-gen "^2.0.2"
pug-filters "^3.1.1"
pug-lexer "^4.1.0"
pug-linker "^3.0.6"
pug-load "^2.0.12"
pug-parser "^5.0.1"
pug-runtime "^2.0.5"
pug-strip-comments "^1.0.4"
pump@^2.0.0, pump@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909"
@ -9649,7 +9249,7 @@ repeat-element@^1.1.2:
resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
repeat-string@^1.5.2, repeat-string@^1.6.1:
repeat-string@^1.6.1:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
@ -9773,7 +9373,7 @@ resolve-url@^0.2.1:
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=
resolve@^1.1.6, resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1:
resolve@^1.10.0, resolve@^1.3.2, resolve@^1.4.0, resolve@^1.8.1:
version "1.12.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
@ -9808,13 +9408,6 @@ rgba-regex@^1.0.0:
resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=
right-align@^0.1.1:
version "0.1.3"
resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
integrity sha1-YTObci/mo1FWiSENJOFMlhSGE+8=
dependencies:
align-text "^0.1.1"
rimraf@2, rimraf@2.7.1, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@ -10288,7 +9881,7 @@ source-map@^0.4.2:
dependencies:
amdefine ">=0.0.4"
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
@ -10942,11 +10535,6 @@ to-arraybuffer@^1.0.0:
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
to-fast-properties@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
integrity sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=
to-fast-properties@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e"
@ -10982,11 +10570,6 @@ toidentifier@1.0.0:
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
token-stream@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/token-stream/-/token-stream-0.0.1.tgz#ceeefc717a76c4316f126d0b9dbaa55d7e7df01a"
integrity sha1-zu78cXp2xDFvEm0LnbqlXX598Bo=
topo@2.x.x:
version "2.0.2"
resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182"
@ -11221,21 +10804,6 @@ uglify-js@3.4.x:
commander "~2.19.0"
source-map "~0.6.1"
uglify-js@^2.6.1:
version "2.8.29"
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
integrity sha1-KcVzMUgFe7Th913zW3qcty5qWd0=
dependencies:
source-map "~0.5.1"
yargs "~3.10.0"
optionalDependencies:
uglify-to-browserify "~1.0.0"
uglify-to-browserify@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7"
integrity sha1-bgkk1r2mta/jSeOabWMoUKD4grc=
unicode-canonical-property-names-ecmascript@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz#2619800c4c825800efdd8343af7dd9933cbe2818"
@ -11506,11 +11074,6 @@ vm-browserify@^1.0.1:
resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw==
void-elements@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
integrity sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=
vue-apollo@^3.0.0-rc.1:
version "3.0.0-rc.2"
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.0-rc.2.tgz#3b59a93084cb37c19be46b9f18cd63214a782718"
@ -11532,16 +11095,28 @@ vue-cli-plugin-webpack-bundle-analyzer@^1.3.0:
dependencies:
webpack-bundle-analyzer "^3.3.2"
vue-gettext@^2.1.3:
version "2.1.5"
resolved "https://registry.yarnpkg.com/vue-gettext/-/vue-gettext-2.1.5.tgz#6bede1091c25ca657c591532a154b7e5680abd84"
integrity sha512-QAfPupLNthQLDVSIoLKOSiDeBqXja3qfT48Civhuxwjdee0fdi0MHTgsvnseMsHDfMJq6GBxIMBMhoRp6NJdjg==
vue-hot-reload-api@^2.3.0:
version "2.3.3"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf"
integrity sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==
vue-i18n-extract@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/vue-i18n-extract/-/vue-i18n-extract-1.0.2.tgz#0a136e12d1634d6799e187aad81a7003d02f67a5"
integrity sha512-+zwDKvle4KcfloXZnj5hF01ViKDiFr5RMx5507D7oyDXpSleRpekF5YHgZa/+Ra6Go68//z0Nya58J9tKFsCjw==
dependencies:
cli-table3 "^0.5.1"
dot-object "^1.7.1"
esm "^3.2.13"
glob "^7.1.3"
is-valid-glob "^1.0.0"
yargs "^13.2.2"
vue-i18n@^8.14.0:
version "8.14.0"
resolved "https://registry.yarnpkg.com/vue-i18n/-/vue-i18n-8.14.0.tgz#613cbbc21d71dc608cd085f8a94ea3a40badcd33"
integrity sha512-utI1Rvc8i+fmmUkkKRmHaf4QQ87s7rGVL5ZZLsKvvRzmgaIr1l+GfGxxxRmsZxHpPlgeB8OxoUZ4noqZgDL6xg==
vue-loader@^15.7.0:
version "15.7.1"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.1.tgz#6ccacd4122aa80f69baaac08ff295a62e3aefcfd"
@ -11584,7 +11159,7 @@ vue-svg-inline-loader@^1.2.15:
loader-utils "^1.2.3"
svgo "^1.3.0"
vue-template-compiler@^2.5.16, vue-template-compiler@^2.6.10:
vue-template-compiler@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
@ -11592,7 +11167,7 @@ vue-template-compiler@^2.5.16, vue-template-compiler@^2.6.10:
de-indent "^1.0.2"
he "^1.1.0"
vue-template-es2015-compiler@^1.6.0, vue-template-es2015-compiler@^1.9.0:
vue-template-es2015-compiler@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
@ -11865,24 +11440,6 @@ widest-line@^2.0.0:
dependencies:
string-width "^2.1.1"
window-size@0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
integrity sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=
with@^5.0.0:
version "5.1.1"
resolved "https://registry.yarnpkg.com/with/-/with-5.1.1.tgz#fa4daa92daf32c4ea94ed453c81f04686b575dfe"
integrity sha1-+k2qktrzLE6pTtRTyB8EaGtXXf4=
dependencies:
acorn "^3.1.0"
acorn-globals "^3.0.0"
wordwrap@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
integrity sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=
wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
@ -12220,7 +11777,7 @@ yargs@^11.0.0:
y18n "^3.2.1"
yargs-parser "^9.0.2"
yargs@^13.0.0:
yargs@^13.0.0, yargs@^13.2.2:
version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
@ -12274,16 +11831,6 @@ yargs@^8.0.2:
y18n "^3.2.1"
yargs-parser "^7.0.0"
yargs@~3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
integrity sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=
dependencies:
camelcase "^1.0.2"
cliui "^2.1.0"
decamelize "^1.0.0"
window-size "0.1.0"
yauzl@2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"

View File

@ -42,6 +42,7 @@ defmodule Mobilizon do
Mobilizon.Service.Federator,
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1),
cachex_spec(:statistics, 10, 60, 60),
cachex_spec(:activity_pub, 2500, 3, 15)
]

View File

@ -65,12 +65,8 @@ defmodule Mobilizon.Addresses.Address do
@spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
url =
Map.get(
changes,
:url,
"#{MobilizonWeb.Endpoint.url()}/address/#{Ecto.UUID.generate()}"
)
uuid = Ecto.UUID.generate()
url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{uuid}")
put_change(changeset, :url, url)
end

View File

@ -1,3 +1,11 @@
import EctoEnum
defenum(Mobilizon.Admin.ActionLogAction, [
"update",
"create",
"delete"
])
defmodule Mobilizon.Admin.ActionLog do
@moduledoc """
Represents an action log entity.
@ -8,6 +16,7 @@ defmodule Mobilizon.Admin.ActionLog do
import Ecto.Changeset
alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.ActionLogAction
@type t :: %__MODULE__{
action: String.t(),
@ -17,12 +26,13 @@ defmodule Mobilizon.Admin.ActionLog do
actor: Actor.t()
}
@required_attrs [:action, :target_type, :target_id, :actor_id]
@optional_attrs [:changes]
@attrs @required_attrs ++ @optional_attrs
@required_attrs [:action, :target_type, :target_id, :changes, :actor_id]
@attrs @required_attrs
@timestamps_opts [type: :utc_datetime]
schema "admin_action_logs" do
field(:action, :string)
field(:action, ActionLogAction)
field(:target_type, :string)
field(:target_id, :integer)
field(:changes, :map)

View File

@ -30,6 +30,6 @@ defmodule Mobilizon.Admin do
@spec list_action_logs_query :: Ecto.Query.t()
defp list_action_logs_query do
from(r in ActionLog, preload: [:actor])
from(r in ActionLog, preload: [:actor], order_by: [desc: :id])
end
end

View File

@ -36,6 +36,7 @@ defmodule Mobilizon.Events.EventOptions do
]
@primary_key false
@derive Jason.Encoder
embedded_schema do
field(:maximum_attendee_capacity, :integer)
field(:remaining_attendee_capacity, :integer)

View File

@ -657,6 +657,28 @@ defmodule Mobilizon.Events do
|> Repo.all()
end
@doc """
Counts approved participants.
"""
@spec count_approved_participants(integer | String.t()) :: integer
def count_approved_participants(event_id) do
event_id
|> count_participants_query()
|> filter_approved_role()
|> Repo.aggregate(:count, :id)
end
@doc """
Counts unapproved participants.
"""
@spec count_unapproved_participants(integer | String.t()) :: integer
def count_unapproved_participants(event_id) do
event_id
|> count_participants_query()
|> filter_unapproved_role()
|> Repo.aggregate(:count, :id)
end
@doc """
Gets a single session.
Raises `Ecto.NoResultsError` if the session does not exist.
@ -1145,6 +1167,11 @@ defmodule Mobilizon.Events do
from(p in Participant, where: p.actor_id == ^actor_id and p.role == ^:not_approved)
end
@spec count_participants_query(integer) :: Ecto.Query.t()
defp count_participants_query(event_id) do
from(p in Participant, where: p.event_id == ^event_id)
end
@spec event_participations_for_actor_query(integer) :: Ecto.Query.t()
def event_participations_for_actor_query(actor_id) do
from(
@ -1244,11 +1271,18 @@ defmodule Mobilizon.Events do
from(q in query, where: q.visibility == ^:public)
end
@spec filter_role(Ecto.Query.t(), boolean) :: Ecto.Query.t()
defp filter_role(query, false) do
@spec filter_approved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_approved_role(query) do
from(p in query, where: p.role != ^:not_approved)
end
@spec filter_unapproved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_unapproved_role(query) do
from(p in query, where: p.role == ^:not_approved)
end
@spec filter_role(Ecto.Query.t(), boolean) :: Ecto.Query.t()
defp filter_role(query, false), do: filter_approved_role(query)
defp filter_role(query, true), do: query
@spec preload_for_event(Ecto.Query.t()) :: Ecto.Query.t()

View File

@ -13,6 +13,8 @@ defmodule Mobilizon.Reports.Note do
@required_attrs [:content, :moderator_id, :report_id]
@attrs @required_attrs
@timestamps_opts [type: :utc_datetime]
@type t :: %__MODULE__{
content: String.t(),
report: Report.t(),

View File

@ -23,10 +23,12 @@ defmodule Mobilizon.Reports.Report do
notes: [Note.t()]
}
@required_attrs [:content, :uri, :reported_id, :reporter_id]
@optional_attrs [:status, :manager_id, :event_id]
@required_attrs [:uri, :reported_id, :reporter_id]
@optional_attrs [:content, :status, :manager_id, :event_id]
@attrs @required_attrs ++ @optional_attrs
@timestamps_opts [type: :utc_datetime]
@derive {Jason.Encoder, only: [:status, :uri]}
schema "reports" do
field(:content, :string)

View File

@ -76,14 +76,29 @@ defmodule Mobilizon.Reports do
@doc """
Returns the list of reports.
"""
@spec list_reports(integer | nil, integer | nil, atom, atom) :: [Report.t()]
def list_reports(page \\ nil, limit \\ nil, sort \\ :updated_at, direction \\ :asc) do
list_reports_query()
@spec list_reports(integer | nil, integer | nil, atom, atom, ReportStatus) :: [Report.t()]
def list_reports(
page \\ nil,
limit \\ nil,
sort \\ :updated_at,
direction \\ :asc,
status \\ :open
) do
status
|> list_reports_query()
|> Page.paginate(page, limit)
|> sort(sort, direction)
|> Repo.all()
end
@doc """
Counts opened reports.
"""
@spec count_opened_reports :: integer
def count_opened_reports do
Repo.aggregate(count_reports_query(), :count, :id)
end
@doc """
Gets a single note.
"""
@ -131,14 +146,20 @@ defmodule Mobilizon.Reports do
from(r in Report, where: r.uri == ^url)
end
@spec list_reports_query :: Ecto.Query.t()
defp list_reports_query do
@spec list_reports_query(ReportStatus.t()) :: Ecto.Query.t()
defp list_reports_query(status) do
from(
r in Report,
preload: [:reported, :reporter, :manager, :event, :comments, :notes]
preload: [:reported, :reporter, :manager, :event, :comments, :notes],
where: r.status == ^status
)
end
@spec count_reports_query :: Ecto.Query.t()
defp count_reports_query do
from(r in Report, where: r.status == ^:open)
end
@spec list_notes_for_report_query(integer | String.t()) :: Ecto.Query.t()
defp list_notes_for_report_query(report_id) do
from(

View File

@ -135,4 +135,13 @@ defmodule MobilizonWeb.API.Events do
}
end
end
@doc """
Trigger the deletion of an event
If the event is deleted by
"""
def delete_event(%Event{} = event, federate \\ true) do
ActivityPub.delete(event, federate)
end
end

View File

@ -22,21 +22,17 @@ defmodule MobilizonWeb.API.Reports do
def report(
%{
reporter_actor_id: reporter_actor_id,
reported_actor_id: reported_actor_id,
event_id: event_id,
comments_ids: comments_ids,
report_content: report_content
reported_actor_id: reported_actor_id
} = args
) do
with {:reporter, %Actor{url: reporter_url} = _reporter_actor} <-
{:reporter, Actors.get_actor!(reporter_actor_id)},
{:reported, %Actor{url: reported_actor_url} = reported_actor} <-
{:reported, Actors.get_actor!(reported_actor_id)},
{:ok, content} <- make_report_content_html(report_content),
{:ok, event} <-
if(event_id, do: Events.get_event(event_id), else: {:ok, nil}),
{:ok, content} <- args |> Map.get(:content, nil) |> make_report_content_text(),
{:ok, event} <- args |> Map.get(:event_id, nil) |> get_event(),
{:get_report_comments, comments_urls} <-
get_report_comments(reported_actor, comments_ids),
get_report_comments(reported_actor, Map.get(args, :comments_ids, [])),
{:make_activity, {:ok, %Activity{} = activity, %Report{} = report}} <-
{:make_activity,
ActivityPub.flag(%{
@ -50,6 +46,7 @@ defmodule MobilizonWeb.API.Reports do
})} do
{:ok, activity, report}
else
{:make_activity, err} -> {:error, err}
{:error, err} -> {:error, err}
{:actor_id, %{}} -> {:error, "Valid `actor_id` required"}
{:reporter, nil} -> {:error, "Reporter Actor not found"}
@ -57,6 +54,9 @@ defmodule MobilizonWeb.API.Reports do
end
end
defp get_event(nil), do: {:ok, nil}
defp get_event(event_id), do: Events.get_event(event_id)
@doc """
Update the state of a report
"""

View File

@ -124,9 +124,9 @@ defmodule MobilizonWeb.API.Utils do
# |> Formatter.html_escape("text/html")
# end
def make_report_content_html(nil), do: {:ok, {nil, [], []}}
def make_report_content_text(nil), do: {:ok, nil}
def make_report_content_html(comment) do
def make_report_content_text(comment) do
max_size = Config.get([:instance, :max_report_comment_size], 1000)
if String.length(comment) <= max_size do
@ -137,7 +137,7 @@ defmodule MobilizonWeb.API.Utils do
end
def prepare_content(actor, content, visibility, tags, in_reply_to) do
with content <- String.trim(content),
with content <- String.trim(content || ""),
{content_html, mentions, tags} <-
make_content_html(
content,

View File

@ -6,7 +6,8 @@
defmodule MobilizonWeb.NodeInfoController do
use MobilizonWeb, :controller
alias Mobilizon.{Config, Events, Users}
alias Mobilizon.Config
alias Mobilizon.Service.Statistics
@node_info_supported_versions ["2.0", "2.1"]
@node_info_schema_uri "http://nodeinfo.diaspora.software/ns/schema/"
@ -32,7 +33,7 @@ defmodule MobilizonWeb.NodeInfoController do
response = %{
version: version,
software: %{
name: "mobilizon",
name: "Mobilizon",
version: Config.instance_version()
},
protocols: ["activitypub"],
@ -43,10 +44,10 @@ defmodule MobilizonWeb.NodeInfoController do
openRegistrations: Config.instance_registrations_open?(),
usage: %{
users: %{
total: Users.count_users()
total: Statistics.get_cached_value(:local_users)
},
localPosts: Events.count_local_events(),
localComments: Events.count_local_comments()
localPosts: Statistics.get_cached_value(:local_events),
localComments: Statistics.get_cached_value(:local_comments)
},
metadata: %{
nodeName: Config.instance_name(),

View File

@ -2,10 +2,13 @@ defmodule MobilizonWeb.Resolvers.Admin do
@moduledoc """
Handles the report-related GraphQL calls
"""
alias Mobilizon.Events
alias Mobilizon.Users.User
import Mobilizon.Users.Guards
alias Mobilizon.Admin.ActionLog
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Events.Event
alias Mobilizon.Service.Statistics
def list_action_logs(_parent, %{page: page, limit: limit}, %{
context: %{current_user: %User{role: role}}
@ -17,14 +20,19 @@ defmodule MobilizonWeb.Resolvers.Admin do
target_type: target_type,
action: action,
actor: actor,
id: id
id: id,
inserted_at: inserted_at
} = action_log ->
transform_action_log(target_type, action, action_log)
|> Map.merge(%{
with data when is_map(data) <-
transform_action_log(String.to_existing_atom(target_type), action, action_log) do
Map.merge(data, %{
actor: actor,
id: id
id: id,
inserted_at: inserted_at
})
end
end)
|> Enum.filter(& &1)
{:ok, action_logs}
end
@ -35,38 +43,87 @@ defmodule MobilizonWeb.Resolvers.Admin do
end
defp transform_action_log(
"Elixir.Mobilizon.Reports.Report",
"update",
Report,
:update,
%ActionLog{} = action_log
) do
with %Report{status: status} = report <- Mobilizon.Reports.get_report(action_log.target_id) do
with %Report{} = report <- Mobilizon.Reports.get_report(action_log.target_id) do
action =
case action_log do
%ActionLog{changes: %{"status" => "closed"}} -> :report_update_closed
%ActionLog{changes: %{"status" => "open"}} -> :report_update_opened
%ActionLog{changes: %{"status" => "resolved"}} -> :report_update_resolved
end
%{
action: "report_update_" <> to_string(status),
action: action,
object: report
}
end
end
defp transform_action_log("Elixir.Mobilizon.Reports.Note", "create", %ActionLog{
defp transform_action_log(Note, :create, %ActionLog{
changes: changes
}) do
%{
action: "note_creation",
action: :note_creation,
object: convert_changes_to_struct(Note, changes)
}
end
defp transform_action_log("Elixir.Mobilizon.Reports.Note", "delete", %ActionLog{
defp transform_action_log(Note, :delete, %ActionLog{
changes: changes
}) do
%{
action: "note_deletion",
action: :note_deletion,
object: convert_changes_to_struct(Note, changes)
}
end
defp transform_action_log(Event, :delete, %ActionLog{
changes: changes
}) do
%{
action: :event_deletion,
object: convert_changes_to_struct(Event, changes)
}
end
# Changes are stored as %{"key" => "value"} so we need to convert them back as struct
defp convert_changes_to_struct(struct, %{"report_id" => _report_id} = changes) do
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}),
data <- Map.put(data, :report, Mobilizon.Reports.get_report(data.report_id)) do
struct(struct, data)
end
end
defp convert_changes_to_struct(struct, changes) do
struct(struct, for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}))
with data <- for({key, val} <- changes, into: %{}, do: {String.to_atom(key), val}) do
struct(struct, data)
end
end
def get_dashboard(_parent, _args, %{
context: %{current_user: %User{role: role}}
})
when is_admin(role) do
last_public_event_published =
case Events.list_events(1, 1, :inserted_at, :desc) do
[event | _] -> event
_ -> nil
end
{:ok,
%{
number_of_users: Statistics.get_cached_value(:local_users),
number_of_events: Statistics.get_cached_value(:local_events),
number_of_comments: Statistics.get_cached_value(:local_comments),
number_of_reports: Mobilizon.Reports.count_opened_reports(),
last_public_event_published: last_public_event_published
}}
end
def get_dashboard(_parent, _args, _resolution) do
{:error, "You need to be logged-in and an administrator to access dashboard statistics"}
end
end

View File

@ -9,7 +9,10 @@ defmodule MobilizonWeb.Resolvers.Event do
alias Mobilizon.Events.{Activity, Event, Participant}
alias Mobilizon.Media.Picture
alias Mobilizon.Users.User
alias Mobilizon.Actors
alias Mobilizon.Actors.Actor
alias MobilizonWeb.Resolvers.Person
import Mobilizon.Service.Admin.ActionLogService
# We limit the max number of events that can be retrieved
@event_max_limit 100
@ -48,6 +51,14 @@ defmodule MobilizonWeb.Resolvers.Event do
{:ok, Mobilizon.Events.list_participants_for_event(uuid, 1, 10)}
end
def stats_participants_for_event(%Event{id: id}, _args, _resolution) do
{:ok,
%{
approved: Mobilizon.Events.count_approved_participants(id),
unapproved: Mobilizon.Events.count_unapproved_participants(id)
}}
end
@doc """
List related events
"""
@ -174,10 +185,10 @@ defmodule MobilizonWeb.Resolvers.Event do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
{:ok, args} <- save_attached_picture(args),
{:ok, args} <- save_physical_address(args),
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <-
{:ok, args_with_organizer} <- save_attached_picture(args_with_organizer),
{:ok, args_with_organizer} <- save_physical_address(args_with_organizer),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.create_event(args_with_organizer) do
{:ok, event}
else
@ -200,13 +211,13 @@ defmodule MobilizonWeb.Resolvers.Event do
) do
# See https://github.com/absinthe-graphql/absinthe/issues/490
with args <- Map.put(args, :options, args[:options] || %{}),
{:ok, %Event{} = event} <- Mobilizon.Events.get_event_with_preload(event_id),
{:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
{:is_owned, %Actor{} = organizer_actor} <-
User.owns_actor(user, event.organizer_actor_id),
args <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, args} <- save_attached_picture(args),
{:ok, args} <- save_physical_address(args),
args <- Map.put(args, :organizer_actor, organizer_actor),
{:ok, %Activity{data: %{"object" => %{"type" => "Event"} = _object}}, %Event{} = event} <-
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
MobilizonWeb.API.Events.update_event(args, event) do
{:ok, event}
else
@ -229,7 +240,7 @@ defmodule MobilizonWeb.Resolvers.Event do
defp save_attached_picture(
%{picture: %{picture: %{file: %Plug.Upload{} = _picture} = all_pic}} = args
) do
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor_id))}
{:ok, Map.put(args, :picture, Map.put(all_pic, :actor_id, args.organizer_actor.id))}
end
# Otherwise if we use a previously uploaded picture we need to fetch it from database
@ -253,7 +264,7 @@ defmodule MobilizonWeb.Resolvers.Event do
end
@spec save_physical_address(map()) :: {:ok, map()}
defp save_physical_address(%{physical_address: address} = args) do
defp save_physical_address(%{physical_address: address} = args) when address != nil do
with {:ok, %Address{} = address} <- Addresses.create_address(address),
args <- Map.put(args, :physical_address, address.url) do
{:ok, args}
@ -269,26 +280,42 @@ defmodule MobilizonWeb.Resolvers.Event do
def delete_event(
_parent,
%{event_id: event_id, actor_id: actor_id},
%{context: %{current_user: user}}
%{context: %{current_user: %User{role: role} = user}}
) do
with {:ok, %Event{} = event} <- Mobilizon.Events.get_event(event_id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:event_can_be_managed, true} <- Event.can_be_managed_by(event, actor_id),
event <- Mobilizon.Events.delete_event!(event) do
{:ok, %{id: event.id}}
with {:ok, %Event{local: is_local} = event} <- Events.get_event_with_preload(event_id),
{actor_id, ""} <- Integer.parse(actor_id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id) do
cond do
{:event_can_be_managed, true} == Event.can_be_managed_by(event, actor_id) ->
do_delete_event(event)
role in [:moderator, :administrator] ->
with {:ok, res} <- do_delete_event(event, !is_local),
%Actor{} = actor <- Actors.get_actor(actor_id) do
log_action(actor, "delete", event)
{:ok, res}
end
true ->
{:error, "You cannot delete this event"}
end
else
{:error, :event_not_found} ->
{:error, "Event not found"}
{:is_owned, nil} ->
{:error, "Actor id is not owned by authenticated user"}
{:event_can_be_managed, false} ->
{:error, "You cannot delete this event"}
end
end
def delete_event(_parent, _args, _resolution) do
{:error, "You need to be logged-in to delete an event"}
end
defp do_delete_event(event, federate \\ true) when is_boolean(federate) do
with {:ok, _activity, event} <- MobilizonWeb.API.Events.delete_event(event) do
{:ok, %{id: event.id}}
end
end
end

View File

@ -76,7 +76,9 @@ defmodule MobilizonWeb.Resolvers.Group do
%{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}}
) do
with {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- Member.is_administrator(member),
@ -109,7 +111,9 @@ defmodule MobilizonWeb.Resolvers.Group do
%{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:error, :member_not_found} <- Actors.get_member(actor.id, group.id),
{:is_able_to_join, true} <- {:is_able_to_join, Member.can_be_joined(group)},
@ -150,7 +154,9 @@ defmodule MobilizonWeb.Resolvers.Group do
%{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}}
) do
with {:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
with {actor_id, ""} <- Integer.parse(actor_id),
{group_id, ""} <- Integer.parse(group_id),
{:is_owned, %Actor{} = actor} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id),
{:only_administrator, false} <-
{:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)},

View File

@ -12,11 +12,11 @@ defmodule MobilizonWeb.Resolvers.Report do
def list_reports(
_parent,
%{page: page, limit: limit},
%{page: page, limit: limit, status: status},
%{context: %{current_user: %User{role: role}}}
)
when is_moderator(role) do
{:ok, Mobilizon.Reports.list_reports(page, limit)}
{:ok, Mobilizon.Reports.list_reports(page, limit, :updated_at, :desc, status)}
end
def list_reports(_parent, _args, _resolution) do
@ -25,7 +25,13 @@ defmodule MobilizonWeb.Resolvers.Report do
def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do
{:ok, Mobilizon.Reports.get_report(id)}
case Mobilizon.Reports.get_report(id) do
%Report{} = report ->
{:ok, report}
nil ->
{:error, "Report not found"}
end
end
def get_report(_parent, _args, _resolution) do

View File

@ -78,6 +78,9 @@ defmodule MobilizonWeb.Router do
get("/events/create", PageController, :index)
get("/events/list", PageController, :index)
get("/events/:uuid/edit", PageController, :index)
# This is a hack to ease link generation into emails
get("/moderation/reports/:id", PageController, :index, as: "moderation_report")
end
scope "/", MobilizonWeb do

View File

@ -4,7 +4,7 @@ defmodule MobilizonWeb.Schema do
"""
use Absinthe.Schema
alias Mobilizon.{Actors, Events, Users, Addresses, Media}
alias Mobilizon.{Actors, Events, Users, Addresses, Media, Reports}
alias Mobilizon.Actors.{Actor, Follower, Member}
alias Mobilizon.Events.{Event, Comment, Participant}
alias Mobilizon.Storage.Repo
@ -27,7 +27,7 @@ defmodule MobilizonWeb.Schema do
@desc "A struct containing the id of the deleted object"
object :deleted_object do
field(:id, :integer)
field(:id, :id)
end
@desc "A JWT and the associated user ID"
@ -45,7 +45,7 @@ defmodule MobilizonWeb.Schema do
Represents a notification for an user
"""
object :notification do
field(:id, :integer, description: "The notification ID")
field(:id, :id, description: "The notification ID")
field(:user, :user, description: "The user to transmit the notification to")
field(:actor, :actor, description: "The notification target profile")
@ -98,6 +98,7 @@ defmodule MobilizonWeb.Schema do
|> Dataloader.add_source(Events, default_source)
|> Dataloader.add_source(Addresses, default_source)
|> Dataloader.add_source(Media, default_source)
|> Dataloader.add_source(Reports, default_source)
Map.put(ctx, :loader, loader)
end

View File

@ -13,7 +13,7 @@ defmodule MobilizonWeb.Schema.ActorInterface do
@desc "An ActivityPub actor"
interface :actor do
field(:id, :integer, description: "Internal ID for this actor")
field(:id, :id, description: "Internal ID for this actor")
field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
field(:name, :string, description: "The actor's displayed name")

View File

@ -14,7 +14,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
object :group do
interfaces([:actor])
field(:id, :integer, description: "Internal ID for this group")
field(:id, :id, description: "Internal ID for this group")
field(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
field(:name, :string, description: "The actor's displayed name")
@ -96,9 +96,7 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
field :create_group, :group do
arg(:preferred_username, non_null(:string), description: "The name for the group")
arg(:creator_actor_id, non_null(:integer),
description: "The identity that creates the group"
)
arg(:creator_actor_id, non_null(:id), description: "The identity that creates the group")
arg(:name, :string, description: "The displayed name for the group")
arg(:summary, :string, description: "The summary for the group", default_value: "")
@ -118,8 +116,8 @@ defmodule MobilizonWeb.Schema.Actors.GroupType do
@desc "Delete a group"
field :delete_group, :deleted_object do
arg(:group_id, non_null(:integer))
arg(:actor_id, non_null(:integer))
arg(:group_id, non_null(:id))
arg(:actor_id, non_null(:id))
resolve(&Group.delete_group/3)
end

View File

@ -24,16 +24,16 @@ defmodule MobilizonWeb.Schema.Actors.MemberType do
object :member_mutations do
@desc "Join a group"
field :join_group, :member do
arg(:group_id, non_null(:integer))
arg(:actor_id, non_null(:integer))
arg(:group_id, non_null(:id))
arg(:actor_id, non_null(:id))
resolve(&Resolvers.Group.join_group/3)
end
@desc "Leave an event"
field :leave_group, :deleted_member do
arg(:group_id, non_null(:integer))
arg(:actor_id, non_null(:integer))
arg(:group_id, non_null(:id))
arg(:actor_id, non_null(:id))
resolve(&Resolvers.Group.leave_group/3)
end

View File

@ -15,7 +15,7 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
"""
object :person do
interfaces([:actor])
field(:id, :integer, description: "Internal ID for this person")
field(:id, :id, description: "Internal ID for this person")
field(:user, :user, description: "The user this actor is associated to")
field(:member_of, list_of(:member), description: "The list of groups this person is member of")

View File

@ -15,7 +15,7 @@ defmodule MobilizonWeb.Schema.AddressType do
field(:country, :string)
field(:description, :string)
field(:url, :string)
field(:id, :integer)
field(:id, :id)
field(:origin_id, :string)
end
@ -40,7 +40,7 @@ defmodule MobilizonWeb.Schema.AddressType do
field(:country, :string)
field(:description, :string)
field(:url, :string)
field(:id, :integer)
field(:id, :id)
field(:origin_id, :string)
end

View File

@ -5,13 +5,25 @@ defmodule MobilizonWeb.Schema.AdminType do
use Absinthe.Schema.Notation
alias MobilizonWeb.Resolvers.Admin
alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Events.Event
@desc "An action log"
object :action_log do
field(:id, :id, description: "Internal ID for this comment")
field(:actor, :actor, description: "The actor that acted")
field(:object, :action_log_object, description: "The object that was acted upon")
field(:action, :string, description: "The action that was done")
field(:action, :action_log_action, description: "The action that was done")
field(:inserted_at, :datetime, description: "The time when the action was performed")
end
enum :action_log_action do
value(:report_update_closed)
value(:report_update_opened)
value(:report_update_resolved)
value(:note_creation)
value(:note_deletion)
value(:event_deletion)
value(:event_update)
end
@desc "The objects that can be in an action log"
@ -25,11 +37,22 @@ defmodule MobilizonWeb.Schema.AdminType do
%Note{}, _ ->
:report_note
%Event{}, _ ->
:event
_, _ ->
nil
end)
end
object :dashboard do
field(:last_public_event_published, :event, description: "Last public event publish")
field(:number_of_users, :integer, description: "The number of local users")
field(:number_of_events, :integer, description: "The number of local events")
field(:number_of_comments, :integer, description: "The number of local comments")
field(:number_of_reports, :integer, description: "The number of current opened reports")
end
object :admin_queries do
@desc "Get the list of action logs"
field :action_logs, type: list_of(:action_log) do
@ -37,5 +60,9 @@ defmodule MobilizonWeb.Schema.AdminType do
arg(:limit, :integer, default_value: 10)
resolve(&Admin.list_action_logs/3)
end
field :dashboard, type: :dashboard do
resolve(&Admin.get_dashboard/3)
end
end
end

Some files were not shown because too many files have changed in this diff Show More