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: [ url: [
host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local", host: System.get_env("MOBILIZON_INSTANCE_HOST") || "mobilizon.local",
port: 80, port: System.get_env("MOBILIZON_INSTANCE_PORT") || 4000,
scheme: "http" scheme: "http"
], ],
debug_errors: true, 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", "dev": "vue-cli-service build --watch",
"test:e2e": "vue-cli-service test:e2e", "test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit", "test:unit": "vue-cli-service test:unit",
"prepare": "patch-package" "vue-i18n-extract": "vue-i18n-extract"
}, },
"dependencies": { "dependencies": {
"apollo-absinthe-upload-link": "^1.5.0", "apollo-absinthe-upload-link": "^1.5.0",
@ -18,7 +18,6 @@
"apollo-link": "^1.2.11", "apollo-link": "^1.2.11",
"apollo-link-http": "^1.5.14", "apollo-link-http": "^1.5.14",
"buefy": "^0.8.2", "buefy": "^0.8.2",
"easygettext": "^2.7.0",
"graphql": "^14.2.1", "graphql": "^14.2.1",
"graphql-tag": "^2.10.1", "graphql-tag": "^2.10.1",
"leaflet": "^1.4.0", "leaflet": "^1.4.0",
@ -33,7 +32,7 @@
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-apollo": "^3.0.0-rc.1", "vue-apollo": "^3.0.0-rc.1",
"vue-class-component": "^7.0.2", "vue-class-component": "^7.0.2",
"vue-gettext": "^2.1.3", "vue-i18n": "^8.14.0",
"vue-property-decorator": "^8.1.0", "vue-property-decorator": "^8.1.0",
"vue-router": "^3.0.6", "vue-router": "^3.0.6",
"vue2-leaflet": "^2.0.3", "vue2-leaflet": "^2.0.3",
@ -58,12 +57,12 @@
"eslint": "^6.0.1", "eslint": "^6.0.1",
"graphql-cli": "^3.0.12", "graphql-cli": "^3.0.12",
"node-sass": "^4.11.0", "node-sass": "^4.11.0",
"patch-package": "^6.1.2",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"tslint": "^5.16.0", "tslint": "^5.16.0",
"tslint-config-airbnb": "^5.11.1", "tslint-config-airbnb": "^5.11.1",
"typescript": "^3.4.3", "typescript": "^3.4.3",
"vue-cli-plugin-webpack-bundle-analyzer": "^1.3.0", "vue-cli-plugin-webpack-bundle-analyzer": "^1.3.0",
"vue-i18n-extract": "^1.0.2",
"vue-svg-inline-loader": "^1.2.15", "vue-svg-inline-loader": "^1.2.15",
"vue-template-compiler": "^2.6.10", "vue-template-compiler": "^2.6.10",
"webpack": "^4.30.0" "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"> <script lang="ts">
import NavBar from '@/components/NavBar.vue'; import NavBar from '@/components/NavBar.vue';
import { Component, Vue } from 'vue-property-decorator'; 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 { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { ICurrentUser } from '@/types/current-user.model'; import { ICurrentUser } from '@/types/current-user.model';
import Footer from '@/components/Footer.vue'; import Footer from '@/components/Footer.vue';
import Logo from '@/components/Logo.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({ @Component({
apollo: { apollo: {
@ -30,34 +39,49 @@ import Logo from '@/components/Logo.vue';
}, },
}) })
export default class App extends Vue { export default class App extends Vue {
currentUser!: ICurrentUser; async created() {
actor = localStorage.getItem(AUTH_USER_ACTOR);
async mounted() {
await this.initializeCurrentUser(); await this.initializeCurrentUser();
} await this.initializeCurrentActor();
getUser(): ICurrentUser | false {
return this.currentUser.id ? this.currentUser : false;
} }
private initializeCurrentUser() { private initializeCurrentUser() {
const userId = localStorage.getItem(AUTH_USER_ID); const userId = localStorage.getItem(AUTH_USER_ID);
const userEmail = localStorage.getItem(AUTH_USER_EMAIL); const userEmail = localStorage.getItem(AUTH_USER_EMAIL);
const accessToken = localStorage.getItem(AUTH_ACCESS_TOKEN); 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({ return this.$apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT, mutation: UPDATE_CURRENT_USER_CLIENT,
variables: { variables: {
id: userId, id: userId,
email: userEmail, email: userEmail,
isLoggedIn: true, 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> </script>
@ -74,6 +98,8 @@ export default class App extends Vue {
@import "~bulma/sass/components/navbar.sass"; @import "~bulma/sass/components/navbar.sass";
@import "~bulma/sass/components/pagination.sass"; @import "~bulma/sass/components/pagination.sass";
@import "~bulma/sass/components/dropdown.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/box.sass";
@import "~bulma/sass/elements/button.sass"; @import "~bulma/sass/elements/button.sass";
@import "~bulma/sass/elements/container.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/tag.sass";
@import "~bulma/sass/elements/title.sass"; @import "~bulma/sass/elements/title.sass";
@import "~bulma/sass/elements/notification"; @import "~bulma/sass/elements/notification";
@import "~bulma/sass/elements/table";
@import "~bulma/sass/grid/_all.sass"; @import "~bulma/sass/grid/_all.sass";
@import "~bulma/sass/layout/_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/upload";
@import "~buefy/src/scss/components/radio"; @import "~buefy/src/scss/components/radio";
@import "~buefy/src/scss/components/switch"; @import "~buefy/src/scss/components/switch";
@import "~buefy/src/scss/components/table";
.router-enter-active, .router-enter-active,
.router-leave-active { .router-leave-active {

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
<template> <template>
<b-field grouped horizontal :label="label"> <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-input expanded type="time" required v-model="time" />
</b-field> </b-field>
</template> </template>

View file

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

View file

@ -1,16 +1,12 @@
<template> <template>
<translate <span v-if="!endsOn">{{ beginsOn | formatDateTimeString }}</span>
v-if="!endsOn" <span v-else-if="isSameDay()">
:translate-params="{date: formatDate(beginsOn), time: formatTime(beginsOn)}" {{ $t('The {date} from {startTime} to {endTime}', {date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}) }}
>The %{ date } at %{ time }</translate> </span>
<translate <span v-else-if="endsOn">
v-else-if="isSameDay()" {{ $t('From the {startDate} at {startTime} to the {endDate} at {endTime}',
:translate-params="{date: formatDate(beginsOn), startTime: formatTime(beginsOn), endTime: formatTime(endsOn)}" {startDate: formatDate(beginsOn), startTime: formatTime(beginsOn), endDate: formatDate(endsOn), endTime: formatTime(endsOn)}) }}
>The %{ date } from %{ startTime } to %{ endTime }</translate> </span>
<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>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'; import { Component, Prop, Vue } from 'vue-property-decorator';
@ -21,11 +17,13 @@ export default class EventFullDate extends Vue {
@Prop({ required: false }) endsOn!: string; @Prop({ required: false }) endsOn!: string;
formatDate(value) { 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) { 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() { 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> <template>
<b-field label="Enter some tags"> <b-field :label="$t('Enter some tags')">
<b-taginput <b-taginput
v-model="tagsStrings" v-model="tagsStrings"
:data="filteredTags" :data="filteredTags"
@ -7,7 +7,7 @@
:allow-new="true" :allow-new="true"
:field="path" :field="path"
icon="label" icon="label"
placeholder="Add a tag" :placeholder="$t('Add a tag')"
@typing="getFilteredTags" @typing="getFilteredTags"
> >
</b-taginput> </b-taginput>

View file

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

View file

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

View file

@ -8,7 +8,7 @@
<b-upload @input="onFileChanged"> <b-upload @input="onFileChanged">
<a class="button is-primary"> <a class="button is-primary">
<b-icon icon="upload"></b-icon> <b-icon icon="upload"></b-icon>
<span>Click to upload</span> <span>{{ $t('Click to upload') }}</span>
</a> </a>
</b-upload> </b-upload>
</div> </div>
@ -41,6 +41,10 @@ export default class PictureUpload extends Vue {
imageSrc: string | null = null; imageSrc: string | null = null;
mounted() {
this.updatePreview(this.pictureFile);
}
@Watch('pictureFile') @Watch('pictureFile')
onPictureFileChanged(val: File) { onPictureFileChanged(val: File) {
this.updatePreview(val); 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 { get defaultPlaceHolder(): string {
// We can't use "this" inside @Prop's default value. // 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> </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_REFRESH_TOKEN = 'auth-refresh-token';
export const AUTH_USER_ID = 'auth-user-id'; export const AUTH_USER_ID = 'auth-user-id';
export const AUTH_USER_EMAIL = 'auth-user-email'; 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` export const LOGGED_PERSON_WITH_GOING_TO_EVENTS = gql`
query { query {
loggedPerson { loggedPerson {
@ -177,7 +196,7 @@ query($name:String!) {
export const CREATE_GROUP = gql` export const CREATE_GROUP = gql`
mutation CreateGroup( mutation CreateGroup(
$creatorActorId: Int!, $creatorActorId: ID!,
$preferredUsername: String!, $preferredUsername: String!,
$name: String!, $name: String!,
$summary: 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, refreshToken,
user { user {
id, 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` export const FETCH_EVENT = gql`
query($uuid:UUID!) { query($uuid:UUID!) {
event(uuid: $uuid) { event(uuid: $uuid) {
@ -29,20 +66,14 @@ export const FETCH_EVENT = gql`
picture { picture {
id id
url url
name
}, },
publishAt, publishAt,
category, category,
# online_address, onlineAddress,
# phone_address, phoneAddress,
physicalAddress { physicalAddress {
description, ${physicalAddressQuery}
floor,
street,
locality,
postalCode,
region,
country,
geom
} }
organizerActor { organizerActor {
avatar { avatar {
@ -64,10 +95,12 @@ export const FETCH_EVENT = gql`
participants { participants {
${participantQuery} ${participantQuery}
}, },
participantStats {
approved,
unapproved
},
tags { tags {
id, ${tagsQuery}
slug,
title
}, },
relatedEvents { relatedEvents {
uuid, uuid,
@ -86,23 +119,7 @@ export const FETCH_EVENT = gql`
} }
}, },
options { options {
maximumAttendeeCapacity, ${optionsQuery}
remainingAttendeeCapacity,
showRemainingAttendeeCapacity,
offers {
price,
priceCurrency,
url
},
participationConditions {
title,
content,
url
},
attendees,
program,
commentModeration,
showParticipationPrice
} }
} }
} }
@ -159,37 +176,62 @@ export const FETCH_EVENTS = gql`
`; `;
export const CREATE_EVENT = gql` export const CREATE_EVENT = gql`
mutation CreateEvent( mutation createEvent(
$organizerActorId: ID!,
$title: String!, $title: String!,
$description: String!, $description: String!,
$organizerActorId: ID!,
$category: String,
$beginsOn: DateTime!, $beginsOn: DateTime!,
$endsOn: DateTime, $endsOn: DateTime,
$picture: PictureInput, $status: EventStatus,
$tags: [String],
$options: EventOptionsInput,
$physicalAddress: AddressInput,
$visibility: EventVisibility $visibility: EventVisibility
$tags: [String],
$picture: PictureInput,
$onlineAddress: String,
$phoneAddress: String,
$category: String,
$physicalAddress: AddressInput,
$options: EventOptionsInput,
) { ) {
createEvent( createEvent(
organizerActorId: $organizerActorId,
title: $title, title: $title,
description: $description, description: $description,
beginsOn: $beginsOn, beginsOn: $beginsOn,
endsOn: $endsOn, endsOn: $endsOn,
organizerActorId: $organizerActorId, status: $status,
category: $category, visibility: $visibility,
options: $options,
picture: $picture,
tags: $tags, tags: $tags,
physicalAddress: $physicalAddress, picture: $picture,
visibility: $visibility onlineAddress: $onlineAddress,
phoneAddress: $phoneAddress,
category: $category,
physicalAddress: $physicalAddress
options: $options,
) { ) {
id, id,
uuid, uuid,
title, title,
description,
beginsOn,
endsOn,
status,
visibility,
picture { picture {
id
url 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` export const EDIT_EVENT = gql`
mutation updateEvent( mutation updateEvent(
$id: ID!, $id: ID!,
$title: String!, $title: String,
$description: String!, $description: String,
$organizerActorId: ID!, $beginsOn: DateTime,
$category: String,
$beginsOn: DateTime!,
$endsOn: DateTime, $endsOn: DateTime,
$picture: PictureInput, $status: EventStatus,
$tags: [String],
$options: EventOptionsInput,
$physicalAddress: AddressInput,
$visibility: EventVisibility $visibility: EventVisibility
$tags: [String],
$picture: PictureInput,
$onlineAddress: String,
$phoneAddress: String,
$category: String,
$physicalAddress: AddressInput,
$options: EventOptionsInput,
) { ) {
updateEvent(eventId: $id, updateEvent(
eventId: $id,
title: $title, title: $title,
description: $description, description: $description,
beginsOn: $beginsOn, beginsOn: $beginsOn,
endsOn: $endsOn, endsOn: $endsOn,
organizerActorId: $organizerActorId, status: $status,
category: $category, visibility: $visibility,
options: $options,
picture: $picture,
tags: $tags, tags: $tags,
physicalAddress: $physicalAddress, picture: $picture,
visibility: $visibility) { onlineAddress: $onlineAddress,
uuid 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` export const JOIN_EVENT = gql`
mutation JoinEvent($eventId: Int!, $actorId: Int!) { mutation JoinEvent($eventId: ID!, $actorId: ID!) {
joinEvent( joinEvent(
eventId: $eventId, eventId: $eventId,
actorId: $actorId actorId: $actorId
@ -239,7 +311,7 @@ export const JOIN_EVENT = gql`
`; `;
export const LEAVE_EVENT = gql` export const LEAVE_EVENT = gql`
mutation LeaveEvent($eventId: Int!, $actorId: Int!) { mutation LeaveEvent($eventId: ID!, $actorId: ID!) {
leaveEvent( leaveEvent(
eventId: $eventId, eventId: $eventId,
actorId: $actorId actorId: $actorId
@ -252,9 +324,9 @@ export const LEAVE_EVENT = gql`
`; `;
export const DELETE_EVENT = gql` export const DELETE_EVENT = gql`
mutation DeleteEvent($id: Int!, $actorId: Int!) { mutation DeleteEvent($eventId: ID!, $actorId: ID!) {
deleteEvent( deleteEvent(
eventId: $id, eventId: $eventId,
actorId: $actorId actorId: $actorId
) { ) {
id id

View file

@ -12,7 +12,7 @@ query {
}`; }`;
export const CREATE_FEED_TOKEN_ACTOR = gql` export const CREATE_FEED_TOKEN_ACTOR = gql`
mutation createFeedToken($actor_id: Int!) { mutation createFeedToken($actor_id: ID!) {
createFeedToken(actorId: $actor_id) { createFeedToken(actorId: $actor_id) {
token, token,
actor { 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, id,
email, email,
isLoggedIn, isLoggedIn,
role
} }
} }
`; `;
export const UPDATE_CURRENT_USER_CLIENT = gql` export const UPDATE_CURRENT_USER_CLIENT = gql`
mutation UpdateCurrentUser($id: Int!, $email: String!, $isLoggedIn: Boolean!) { mutation UpdateCurrentUser($id: String!, $email: String!, $isLoggedIn: Boolean!, $role: UserRole!) {
updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn) @client 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. // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'; import Vue from 'vue';
import Buefy from 'buefy'; import Buefy from 'buefy';
import GetTextPlugin from 'vue-gettext'; import VueI18n from 'vue-i18n';
import App from '@/App.vue'; import App from '@/App.vue';
import router from '@/router'; import router from '@/router';
import { apolloProvider } from './vue-apollo'; import { apolloProvider } from './vue-apollo';
import { NotifierPlugin } from '@/plugins/notifier'; import { NotifierPlugin } from '@/plugins/notifier';
import filters from '@/filters';
const translations = require('@/i18n/translations.json'); import messages from '@/i18n/index';
Vue.config.productionTip = false; Vue.config.productionTip = false;
Vue.use(Buefy); Vue.use(Buefy);
Vue.use(NotifierPlugin); Vue.use(NotifierPlugin);
Vue.use(filters);
const language = (window.navigator as any).userLanguage || window.navigator.language; const language = (window.navigator as any).userLanguage || window.navigator.language;
Vue.use(GetTextPlugin, { Vue.use(VueI18n);
translations,
defaultLanguage: 'en_US',
silent: true,
});
Vue.config.language = language.replace('-', '_'); const i18n = new VueI18n({
locale: language.replace('-', '_'), // set locale
messages, // set locale messages
});
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({
@ -32,4 +32,5 @@ new Vue({
el: '#app', el: '#app',
template: '<App/>', template: '<App/>',
components: { App }, components: { App },
i18n,
}); });

View file

@ -4,6 +4,7 @@ declare module 'vue/types/vue' {
interface Vue { interface Vue {
$notifier: { $notifier: {
success: (message: string) => void; success: (message: string) => void;
error: (message: string) => void;
}; };
} }
} }
@ -16,7 +17,7 @@ export class Notifier {
} }
success(message: string) { success(message: string) {
this.vue.prototype.$notification.open({ this.vue.prototype.$buefy.notification.open({
message, message,
duration: 5000, duration: 5000,
position: 'is-bottom-right', position: 'is-bottom-right',
@ -24,6 +25,16 @@ export class Notifier {
hasIcon: true, 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 // 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 { UserRouteName, userRoutes } from './user';
import { EventRouteName, eventRoutes } from '@/router/event'; import { EventRouteName, eventRoutes } from '@/router/event';
import { ActorRouteName, actorRoutes, MyAccountRouteName } from '@/router/actor'; import { ActorRouteName, actorRoutes, MyAccountRouteName } from '@/router/actor';
import { AdminRouteName, adminRoutes } from '@/router/admin';
import { ErrorRouteName, errorRoutes } from '@/router/error'; import { ErrorRouteName, errorRoutes } from '@/router/error';
import { authGuardIfNeeded } from '@/router/guards/auth-guard'; import { authGuardIfNeeded } from '@/router/guards/auth-guard';
import Search from '@/views/Search.vue'; import Search from '@/views/Search.vue';
import { ModerationRouteName, moderationRoutes } from '@/router/moderation';
Vue.use(Router); Vue.use(Router);
@ -35,6 +37,8 @@ export const RouteName = {
...EventRouteName, ...EventRouteName,
...ActorRouteName, ...ActorRouteName,
...MyAccountRouteName, ...MyAccountRouteName,
...AdminRouteName,
...ModerationRouteName,
...ErrorRouteName, ...ErrorRouteName,
}; };
@ -46,6 +50,8 @@ const router = new Router({
...userRoutes, ...userRoutes,
...eventRoutes, ...eventRoutes,
...actorRoutes, ...actorRoutes,
...adminRoutes,
...moderationRoutes,
...errorRoutes, ...errorRoutes,
{ {
path: '/search/:searchTerm/:searchType?', 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}`; return `@${this.preferredUsername}${domain}`;
} }
displayName(): string { public displayName(): string {
return this.name != null && this.name !== '' ? this.name : this.usernameWithDomain(); 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 { export interface ICurrentUser {
id: number; id: number;
email: string; email: string;
isLoggedIn: boolean; isLoggedIn: boolean;
role: ICurrentUserRole;
} }

View file

@ -90,8 +90,12 @@ export interface IEvent {
picture: IPicture | null; picture: IPicture | null;
organizerActor: IActor; organizerActor?: IActor;
attributedTo: IActor; attributedTo: IActor;
participantStats: {
approved: number;
unapproved: number;
};
participants: IParticipant[]; participants: IParticipant[];
relatedEvents: IEvent[]; relatedEvents: IEvent[];
@ -117,15 +121,15 @@ export interface IEventOptions {
} }
export class EventOptions implements IEventOptions { export class EventOptions implements IEventOptions {
maximumAttendeeCapacity: number = 0; maximumAttendeeCapacity = 0;
remainingAttendeeCapacity: number = 0; remainingAttendeeCapacity = 0;
showRemainingAttendeeCapacity: boolean = false; showRemainingAttendeeCapacity = false;
offers: IOffer[] = []; offers: IOffer[] = [];
participationConditions: IParticipationCondition[] = []; participationConditions: IParticipationCondition[] = [];
attendees: string[] = []; attendees: string[] = [];
program: string = ''; program = '';
commentModeration: CommentModeration = CommentModeration.ALLOW_ALL; commentModeration = CommentModeration.ALLOW_ALL;
showParticipationPrice: boolean = false; showParticipationPrice = false;
} }
export class EventModel implements IEvent { export class EventModel implements IEvent {
@ -154,12 +158,13 @@ export class EventModel implements IEvent {
publishAt = new Date(); publishAt = new Date();
participantStats = { approved: 0, unapproved: 0 };
participants: IParticipant[] = []; participants: IParticipant[] = [];
relatedEvents: IEvent[] = []; relatedEvents: IEvent[] = [];
attributedTo = new Actor(); attributedTo = new Actor();
organizerActor = new Actor(); organizerActor?: IActor;
tags: ITag[] = []; tags: ITag[] = [];
options: IEventOptions = new EventOptions(); options: IEventOptions = new EventOptions();
@ -200,6 +205,25 @@ export class EventModel implements IEvent {
this.physicalAddress = hash.physicalAddress; this.physicalAddress = hash.physicalAddress;
this.tags = hash.tags; 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 { ILogin, IToken } from '@/types/login.model';
import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user'; import { UPDATE_CURRENT_USER_CLIENT } from '@/graphql/user';
import { onLogout } from '@/vue-apollo'; import { onLogout } from '@/vue-apollo';
import ApolloClient from 'apollo-client'; 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) { export function saveUserData(obj: ILogin) {
localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`); localStorage.setItem(AUTH_USER_ID, `${obj.user.id}`);
localStorage.setItem(AUTH_USER_EMAIL, obj.user.email); localStorage.setItem(AUTH_USER_EMAIL, obj.user.email);
localStorage.setItem(AUTH_USER_ROLE, obj.user.role);
saveTokenData(obj); saveTokenData(obj);
} }
export function saveActorData(obj: IPerson) {
localStorage.setItem(AUTH_USER_ACTOR_ID, `${obj.id}`);
}
export function saveTokenData(obj: IToken) { export function saveTokenData(obj: IToken) {
localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken); localStorage.setItem(AUTH_ACCESS_TOKEN, obj.accessToken);
localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken); localStorage.setItem(AUTH_REFRESH_TOKEN, obj.refreshToken);
} }
export function deleteUserData() { 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); 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>) { export function logout(apollo: ApolloClient<any>) {
apollo.mutate({ apollo.mutate({
mutation: UPDATE_CURRENT_USER_CLIENT, mutation: UPDATE_CURRENT_USER_CLIENT,
@ -29,6 +52,7 @@ export function logout(apollo: ApolloClient<any>) {
id: null, id: null,
email: null, email: null,
isLoggedIn: false, 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> <template>
<section class="container"> <section class="container">
<div v-if="loggedPerson"> <div v-if="currentActor">
<div class="header"> <div class="header">
<figure v-if="loggedPerson.banner" class="image is-3by1"> <figure v-if="currentActor.banner" class="image is-3by1">
<img :src="loggedPerson.banner.url" alt="banner"> <img :src="currentActor.banner.url" alt="banner">
</figure> </figure>
</div> </div>
@ -31,7 +31,7 @@
</style> </style>
<script lang="ts"> <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 { Component, Vue, Watch } from 'vue-property-decorator';
import EventCard from '@/components/Event/EventCard.vue'; import EventCard from '@/components/Event/EventCard.vue';
import { IPerson } from '@/types/actor'; import { IPerson } from '@/types/actor';
@ -42,17 +42,18 @@ import Identities from '@/components/Account/Identities.vue';
EventCard, EventCard,
Identities, Identities,
}, },
apollo: {
currentActor: {
query: CURRENT_ACTOR_CLIENT,
},
},
}) })
export default class MyAccount extends Vue { export default class MyAccount extends Vue {
loggedPerson: IPerson | null = null; currentActor!: IPerson;
currentIdentityName: string | null = null; currentIdentityName: string | null = null;
@Watch('$route.params.identityName', { immediate: true }) @Watch('$route.params.identityName', { immediate: true })
async onIdentityParamChanged (val: string) { async onIdentityParamChanged (val: string) {
if (!this.loggedPerson) {
this.loggedPerson = await this.loadLoggedPerson();
}
await this.redirectIfNoIdentitySelected(val); await this.redirectIfNoIdentitySelected(val);
this.currentIdentityName = val; this.currentIdentityName = val;
@ -61,18 +62,10 @@ export default class MyAccount extends Vue {
private async redirectIfNoIdentitySelected (identityParam?: string) { private async redirectIfNoIdentitySelected (identityParam?: string) {
if (!!identityParam) return; if (!!identityParam) return;
if (!!this.loggedPerson) { if (!!this.currentActor) {
this.$router.push({ params: { identityName: this.loggedPerson.preferredUsername } }); 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> </script>
<style lang="scss"> <style lang="scss">

View file

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

View file

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

View file

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

View file

@ -1,129 +1,129 @@
<template> <template>
<section class="container"> <section class="container">
<h1 class="title"> <h1 class="title" v-if="isUpdate === false">
<translate v-if="isUpdate === false">Create a new event</translate> {{ $t('Create a new event') }}
<translate v-else>Update event {{ event.name }}</translate> </h1>
<h1 class="title" v-else>
{{ $t('Update event {name}', { name: event.title }) }}
</h1> </h1>
<div v-if="$apollo.loading">Loading...</div> <div v-if="$apollo.loading">{{ $t('Loading') }}</div>
<div class="columns is-centered" v-else> <div class="columns is-centered" v-else>
<form class="column is-two-thirds-desktop" @submit="createOrUpdate"> <form class="column is-two-thirds-desktop" @submit="createOrUpdate">
<h2 class="subtitle"> <h2 class="subtitle">
<translate> {{ $t('General information') }}
General information
</translate>
</h2> </h2>
<picture-upload v-model="pictureFile" /> <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-input aria-required="true" required v-model="event.title" maxlength="64" />
</b-field> </b-field>
<tag-input v-model="event.tags" :data="tags" path="title" /> <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" /> <address-auto-complete v-model="event.physicalAddress" />
<date-time-picker v-model="event.beginsOn" :label="$gettext('Starts on…')" :step="15"/> <b-field :label="$t('Organizer')">
<date-time-picker v-model="event.endsOn" :label="$gettext('Ends on…')" :step="15" /> <identity-picker v-model="event.organizerActor"></identity-picker>
</b-field>
<div class="field"> <div class="field">
<label class="label">{{ $gettext('Description') }}</label> <label class="label">{{ $t('Description') }}</label>
<editor v-model="event.description" /> <editor v-model="event.description" />
</div> </div>
<b-field :label="$gettext('Website / URL')"> <b-field :label="$t('Website / URL')">
<b-input v-model="event.onlineAddress" placeholder="URL" /> <b-input v-model="event.onlineAddress" placeholder="URL" />
</b-field> </b-field>
<!--<b-field :label="$gettext('Category')"> <!--<b-field :label="$t('Category')">
<b-select placeholder="Select a category" v-model="event.category"> <b-select placeholder="Select a category" v-model="event.category">
<option <option
v-for="category in categories" v-for="category in categories"
:value="category" :value="category"
:key="category" :key="category"
>{{ $gettext(category) }}</option> >{{ $t(category) }}</option>
</b-select> </b-select>
</b-field>--> </b-field>-->
<h2 class="subtitle"> <h2 class="subtitle">
<translate> {{ $t('Who can view this event and participate') }}
Who can view this event and participate
</translate>
</h2> </h2>
<div class="field"> <div class="field">
<b-radio v-model="eventVisibilityJoinOptions" <b-radio v-model="eventVisibilityJoinOptions"
name="eventVisibilityJoinOptions" name="eventVisibilityJoinOptions"
:native-value="EventVisibilityJoinOptions.PUBLIC"> :native-value="EventVisibilityJoinOptions.PUBLIC">
<translate>Visible everywhere on the web (public)</translate> {{ $t('Visible everywhere on the web (public)') }}
</b-radio> </b-radio>
</div> </div>
<div class="field"> <div class="field">
<b-radio v-model="eventVisibilityJoinOptions" <b-radio v-model="eventVisibilityJoinOptions"
name="eventVisibilityJoinOptions" name="eventVisibilityJoinOptions"
:native-value="EventVisibilityJoinOptions.LINK"> :native-value="EventVisibilityJoinOptions.LINK">
<translate>Only accessible through link and search (private)</translate> {{ $t('Only accessible through link and search (private)') }}
</b-radio> </b-radio>
</div> </div>
<div class="field"> <div class="field">
<b-radio v-model="eventVisibilityJoinOptions" <b-radio v-model="eventVisibilityJoinOptions"
name="eventVisibilityJoinOptions" name="eventVisibilityJoinOptions"
:native-value="EventVisibilityJoinOptions.LIMITED"> :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> </b-radio>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Approbation des participations</label> <label class="label">{{ $t('Participation approval') }}</label>
<b-switch v-model="needsApproval"> <b-switch v-model="needsApproval">
Je veux approuver chaque demande de participation {{ $t('I want to approve every participation request') }}
</b-switch> </b-switch>
</div> </div>
<div class="field"> <div class="field">
<label class="label">Mise en avant</label> <label class="label">{{ $t('Promotion') }}</label>
<b-switch v-model="doNotPromote" :disabled="canPromote === false"> <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> </b-switch>
</div> </div>
<div class="field"> <div class="field">
<b-switch v-model="limitedPlaces"> <b-switch v-model="limitedPlaces">
Places limitées {{ $t('Limited places') }}
</b-switch> </b-switch>
</div> </div>
<div class="box" v-if="limitedPlaces"> <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-numberinput v-model="event.options.maximumAttendeeCapacity"></b-numberinput>
</b-field> </b-field>
<b-field> <b-field>
<b-switch v-model="event.options.showRemainingAttendeeCapacity"> <b-switch v-model="event.options.showRemainingAttendeeCapacity">
Show remaining number of places {{ $t('Show remaining number of places') }}
</b-switch> </b-switch>
</b-field> </b-field>
<b-field> <b-field>
<b-switch v-model="event.options.showParticipationPrice"> <b-switch v-model="event.options.showParticipationPrice">
Display participation price {{ $t('Display participation price') }}
</b-switch> </b-switch>
</b-field> </b-field>
</div> </div>
<h2 class="subtitle"> <h2 class="subtitle">
<translate> {{ $t('Public comment moderation') }}
Modération des commentaires publics
</translate>
</h2> </h2>
<label>Comments on the event page</label> <label>{{ $t('Comments on the event page') }}</label>
<div class="field"> <div class="field">
<b-radio v-model="event.options.commentModeration" <b-radio v-model="event.options.commentModeration"
name="commentModeration" name="commentModeration"
:native-value="CommentModeration.ALLOW_ALL"> :native-value="CommentModeration.ALLOW_ALL">
<translate>Allow all comments</translate> {{ $t('Allow all comments') }}
</b-radio> </b-radio>
</div> </div>
@ -131,7 +131,7 @@
<b-radio v-model="event.options.commentModeration" <b-radio v-model="event.options.commentModeration"
name="commentModeration" name="commentModeration"
:native-value="CommentModeration.MODERATED"> :native-value="CommentModeration.MODERATED">
<translate>Moderated comments (shown after approval)</translate> {{ $t('Moderated comments (shown after approval)') }}
</b-radio> </b-radio>
</div> </div>
@ -139,21 +139,19 @@
<b-radio v-model="event.options.commentModeration" <b-radio v-model="event.options.commentModeration"
name="commentModeration" name="commentModeration"
:native-value="CommentModeration.CLOSED"> :native-value="CommentModeration.CLOSED">
<translate>Close all comments (except for admins)</translate> {{ $t('Close comments for all (except for admins)') }}
</b-radio> </b-radio>
</div> </div>
<h2 class="subtitle"> <h2 class="subtitle">
<translate> {{ $t('Status') }}
Status
</translate>
</h2> </h2>
<div class="field"> <div class="field">
<b-radio v-model="event.status" <b-radio v-model="event.status"
name="status" name="status"
:native-value="EventStatus.TENTATIVE"> :native-value="EventStatus.TENTATIVE">
<translate>Tentative: Will be confirmed later</translate> {{ $t('Tentative: Will be confirmed later') }}
</b-radio> </b-radio>
</div> </div>
@ -161,24 +159,38 @@
<b-radio v-model="event.status" <b-radio v-model="event.status"
name="status" name="status"
:native-value="EventStatus.CONFIRMED"> :native-value="EventStatus.CONFIRMED">
<translate>Confirmed: Will happen</translate> {{ $t('Confirmed: Will happen') }}
</b-radio> </b-radio>
</div> </div>
<button class="button is-primary"> <button class="button is-primary">
<translate v-if="isUpdate === false">Create my event</translate> <span v-if="isUpdate === false">{{ $t('Create my event') }}</span>
<translate v-else>Update my event</translate> <span v-else> {{ $t('Update my event') }}</span>
</button> </button>
</form> </form>
</div> </div>
</section> </section>
</template> </template>
<style lang="scss">
@import "@/variables.scss";
h2.subtitle {
margin: 10px 0;
span {
padding: 5px 7px;
display: inline;
background: $secondary;
}
}
</style>
<script lang="ts"> <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 { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration } from '@/types/event.model'; import { EventModel, EventStatus, EventVisibility, EventVisibilityJoinOptions, CommentModeration, IEvent } from '@/types/event.model';
import { LOGGED_PERSON } from '@/graphql/actor'; import { CURRENT_ACTOR_CLIENT } from '@/graphql/actor';
import { IPerson, Person } from '@/types/actor'; import { IPerson, Person } from '@/types/actor';
import PictureUpload from '@/components/PictureUpload.vue'; import PictureUpload from '@/components/PictureUpload.vue';
import Editor from '@/components/Editor.vue'; import Editor from '@/components/Editor.vue';
@ -188,12 +200,13 @@ import { TAGS } from '@/graphql/tags';
import { ITag } from '@/types/tag.model'; import { ITag } from '@/types/tag.model';
import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue'; import AddressAutoComplete from '@/components/Event/AddressAutoComplete.vue';
import { buildFileFromIPicture, buildFileVariable } from '@/utils/image'; import { buildFileFromIPicture, buildFileVariable } from '@/utils/image';
import IdentityPicker from '@/views/Account/IdentityPicker.vue';
@Component({ @Component({
components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor }, components: { AddressAutoComplete, TagInput, DateTimePicker, PictureUpload, Editor, IdentityPicker },
apollo: { apollo: {
loggedPerson: { currentActor: {
query: LOGGED_PERSON, query: CURRENT_ACTOR_CLIENT,
}, },
tags: { tags: {
query: TAGS, query: TAGS,
@ -206,7 +219,7 @@ export default class EditEvent extends Vue {
eventId!: string | undefined; eventId!: string | undefined;
loggedPerson = new Person(); currentActor = new Person();
tags: ITag[] = []; tags: ITag[] = [];
event = new EventModel(); event = new EventModel();
pictureFile: File | null = null; pictureFile: File | null = null;
@ -243,6 +256,7 @@ export default class EditEvent extends Vue {
this.event.beginsOn = now; this.event.beginsOn = now;
this.event.endsOn = end; this.event.endsOn = end;
this.event.organizerActor = this.event.organizerActor || this.currentActor;
} }
createOrUpdate(e: Event) { createOrUpdate(e: Event) {
@ -291,12 +305,10 @@ export default class EditEvent extends Vue {
* Build variables for Event GraphQL creation query * Build variables for Event GraphQL creation query
*/ */
private buildVariables() { private buildVariables() {
const obj = { let res = this.event.toEditJSON();
organizerActorId: this.loggedPerson.id, if (this.event.organizerActor) {
beginsOn: this.event.beginsOn.toISOString(), res = Object.assign(res, { organizerActorId: this.event.organizerActor.id });
tags: this.event.tags.map((tag: ITag) => tag.title), }
};
const res = Object.assign({}, this.event, obj);
delete this.event.options['__typename']; delete this.event.options['__typename'];
@ -360,16 +372,4 @@ export default class EditEvent extends Vue {
// } // }
} }
</script> </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> </div>
<h1 class="title">{{ event.title }}</h1> <h1 class="title">{{ event.title }}</h1>
</div> </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"> <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> <b-icon icon="circle-outline"></b-icon>
<translate>Join</translate> {{ $t('Join') }}
</a> </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> <b-icon icon="check-circle"></b-icon>
<translate>Leave</translate> {{ $t('Leave') }}
</a> </a>
</div> </div>
</div> </div>
@ -35,7 +41,7 @@
<span class="tag" v-if="event.category">{{ event.category }}</span> <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="tag" v-if="event.tags" v-for="tag in event.tags">{{ tag.title }}</span>
<span class="visibility"> <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> </span>
</p> </p>
<div class="date-and-add-to-calendar"> <div class="date-and-add-to-calendar">
@ -45,7 +51,7 @@
</div> </div>
<a class="add-to-calendar" @click="downloadIcsEvent()"> <a class="add-to-calendar" @click="downloadIcsEvent()">
<b-icon icon="calendar-plus" /> <b-icon icon="calendar-plus" />
<translate>Add to my calendar</translate> {{ $t('Add to my calendar') }}
</a> </a>
</div> </div>
<p class="slug"> <p class="slug">
@ -53,24 +59,29 @@
</p> </p>
</div> </div>
<div class="column sidebar"> <div class="column sidebar">
<div class="field has-addons" v-if="actorIsOrganizer()"> <div class="field has-addons">
<p class="control"> <p class="control" v-if="actorIsOrganizer()">
<router-link <router-link
class="button" class="button"
:to="{ name: 'EditEvent', params: {eventId: event.uuid}}" :to="{ name: 'EditEvent', params: {eventId: event.uuid}}"
> >
<translate>Edit</translate> {{ $t('Edit') }}
</router-link> </router-link>
</p> </p>
<p class="control" v-if="actorIsOrganizer()">
<a class="button is-danger" @click="openDeleteEventModal()">
{{ $t('Delete') }}
</a>
</p>
<p class="control"> <p class="control">
<a class="button is-danger" @click="deleteEvent()"> <a class="button is-danger" @click="isReportModalActive = true">
<translate>Delete</translate> {{ $t('Report') }}
</a> </a>
</p> </p>
</div> </div>
<div class="address-wrapper"> <div class="address-wrapper">
<b-icon icon="map" /> <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"> <div class="address" v-if="event.physicalAddress">
<address> <address>
<span class="addressDescription">{{ event.physicalAddress.description }}</span> <span class="addressDescription">{{ event.physicalAddress.description }}</span>
@ -79,7 +90,7 @@
<!-- <span>{{ event.physicalAddress.region }} {{ event.physicalAddress.country }}</span>--> <!-- <span>{{ event.physicalAddress.region }} {{ event.physicalAddress.country }}</span>-->
</address> </address>
<span class="map-show-button" @click="showMap = !showMap" v-if="event.physicalAddress && event.physicalAddress.geom"> <span class="map-show-button" @click="showMap = !showMap" v-if="event.physicalAddress && event.physicalAddress.geom">
<translate>Show map</translate> {{ $t('Show map') }}
</span> </span>
</div> </div>
<b-modal v-if="event.physicalAddress && event.physicalAddress.geom" :active.sync="showMap" :width="800" scroll="keep"> <b-modal v-if="event.physicalAddress && event.physicalAddress.geom" :active.sync="showMap" :width="800" scroll="keep">
@ -93,14 +104,14 @@
</div> </div>
<div class="organizer"> <div class="organizer">
<actor-link :actor="event.organizerActor"> <actor-link :actor="event.organizerActor">
<translate <span v-if="event.organizerActor">
:translate-params="{name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}" {{ $t('By {name}', {name: event.organizerActor.name ? event.organizerActor.name : event.organizerActor.preferredUsername}) }}
v-if="event.organizerActor">By %{ name }</translate> </span>
<figure v-if="event.organizerActor.avatar" class="image is-48x48"> <figure v-if="event.organizerActor.avatar" class="image is-48x48">
<img <img
class="is-rounded" class="is-rounded"
:src="event.organizerActor.avatar.url" :src="event.organizerActor.avatar.url"
:alt="$gettextInterpolate('%{actor}\'s avatar', {actor: event.organizerActor.preferredUsername})" /> :alt="$t("{actor}'s avatar", {actor: event.organizerActor.preferredUsername})" />
</figure> </figure>
</actor-link> </actor-link>
</div> </div>
@ -120,7 +131,7 @@
<!-- <span>--> <!-- <span>-->
<!-- <translate--> <!-- <translate-->
<!-- :translate-n="event.participants.length"--> <!-- :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.--> <!-- One person is going.-->
<!-- </translate>--> <!-- </translate>-->
@ -130,10 +141,10 @@
<div class="description"> <div class="description">
<div class="description-container container"> <div class="description-container container">
<h3 class="title"> <h3 class="title">
<translate>About this event</translate> {{ $t('About this event') }}
</h3> </h3>
<p v-if="!event.description"> <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> </p>
<div class="columns" v-else> <div class="columns" v-else>
<div class="column is-half"> <div class="column is-half">
@ -197,7 +208,7 @@
<div class="container"> <div class="container">
<div class="columns"> <div class="columns">
<div class="column is-half has-text-centered"> <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> <div>
<b-icon icon="mastodon" size="is-large" type="is-primary" /> <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> <a :href="facebookShareUrl" target="_blank" rel="nofollow noopener"><b-icon icon="facebook" size="is-large" type="is-primary" /></a>
@ -210,20 +221,26 @@
<hr /> <hr />
<div class="column is-half has-text-right add-to-calendar"> <div class="column is-half has-text-right add-to-calendar">
<h3 @click="downloadIcsEvent()"> <h3 @click="downloadIcsEvent()">
<translate>Add to my calendar</translate> {{ $t('Add to my calendar') }}
</h3> </h3>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<section class="more-events container" v-if="event.relatedEvents.length > 0"> <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="columns">
<div class="column is-one-third-desktop" v-for="relatedEvent in event.relatedEvents" :key="relatedEvent.uuid"> <div class="column is-one-third-desktop" v-for="relatedEvent in event.relatedEvents" :key="relatedEvent.uuid">
<EventCard :event="relatedEvent" /> <EventCard :event="relatedEvent" />
</div> </div>
</div> </div>
</section> </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>
</div> </div>
</template> </template>
@ -231,7 +248,7 @@
<script lang="ts"> <script lang="ts">
import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event'; import { DELETE_EVENT, FETCH_EVENT, JOIN_EVENT, LEAVE_EVENT } from '@/graphql/event';
import { Component, Prop, Vue } from 'vue-property-decorator'; 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 { EventVisibility, IEvent, IParticipant } from '@/types/event.model';
import { IPerson } from '@/types/actor'; import { IPerson } from '@/types/actor';
import { RouteName } from '@/router'; 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 EventCard from '@/components/Event/EventCard.vue';
import EventFullDate from '@/components/Event/EventFullDate.vue'; import EventFullDate from '@/components/Event/EventFullDate.vue';
import ActorLink from '@/components/Account/ActorLink.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({ @Component({
components: { components: {
@ -249,6 +270,8 @@ import ActorLink from '@/components/Account/ActorLink.vue';
EventCard, EventCard,
BIcon, BIcon,
DateCalendarIcon, DateCalendarIcon,
ReportModal,
ParticipationModal,
// tslint:disable:space-in-parens // tslint:disable:space-in-parens
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'), 'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
// tslint:enable // tslint:enable
@ -262,8 +285,8 @@ import ActorLink from '@/components/Account/ActorLink.vue';
}; };
}, },
}, },
loggedPerson: { currentActor: {
query: LOGGED_PERSON, query: CURRENT_ACTOR_CLIENT,
}, },
}, },
}) })
@ -271,28 +294,57 @@ export default class Event extends Vue {
@Prop({ type: String, required: true }) uuid!: string; @Prop({ type: String, required: true }) uuid!: string;
event!: IEvent; event!: IEvent;
loggedPerson!: IPerson; currentActor!: IPerson;
validationSent: boolean = false; validationSent: boolean = false;
showMap: boolean = false; showMap: boolean = false;
isReportModalActive: boolean = false;
isJoinModalActive: boolean = false;
EventVisibility = EventVisibility; EventVisibility = EventVisibility;
async deleteEvent() { async openDeleteEventModal () {
const router = this.$router; const participantsLength = this.event.participants.length;
const eventTitle = this.event.title; 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 { try {
await this.$apollo.mutate<IParticipant>({ await this.$apollo.mutate<IReport>({
mutation: DELETE_EVENT, mutation: CREATE_REPORT,
variables: { variables: {
id: this.event.id, eventId: this.event.id,
actorId: this.loggedPerson.id, reporterActorId: this.currentActor.id,
reportedActorId: this.event.organizerActor.id,
content,
}, },
}); });
await router.push({ name: RouteName.HOME });
this.$buefy.notification.open({ this.$buefy.notification.open({
message: this.$gettextInterpolate('Event %{eventTitle} deleted', { eventTitle }), message: this.$t('Event {eventTitle} reported', { eventTitle }) as string,
type: 'is-success', type: 'is-success',
position: 'is-bottom-right', position: 'is-bottom-right',
duration: 5000, duration: 5000,
@ -302,13 +354,14 @@ export default class Event extends Vue {
} }
} }
async joinEvent() { async joinEvent(identity: IPerson) {
this.isJoinModalActive = false;
try { try {
await this.$apollo.mutate<{ joinEvent: IParticipant }>({ await this.$apollo.mutate<{ joinEvent: IParticipant }>({
mutation: JOIN_EVENT, mutation: JOIN_EVENT,
variables: { variables: {
eventId: this.event.id, eventId: this.event.id,
actorId: this.loggedPerson.id, actorId: identity.id,
}, },
update: (store, { data }) => { update: (store, { data }) => {
if (data == null) return; 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() { async leaveEvent() {
try { try {
await this.$apollo.mutate<{ leaveEvent: IParticipant }>({ await this.$apollo.mutate<{ leaveEvent: IParticipant }>({
mutation: LEAVE_EVENT, mutation: LEAVE_EVENT,
variables: { variables: {
eventId: this.event.id, eventId: this.event.id,
actorId: this.loggedPerson.id, actorId: this.currentActor.id,
}, },
update: (store, { data }) => { update: (store, { data }) => {
if (data == null) return; if (data == null) return;
@ -350,6 +414,7 @@ export default class Event extends Vue {
event.participants = event.participants event.participants = event.participants
.filter(p => p.actor.id !== data.leaveEvent.actor.id); .filter(p => p.actor.id !== data.leaveEvent.actor.id);
event.participantStats.approved = event.participantStats.approved - 1;
store.writeQuery({ query: FETCH_EVENT, data: { event } }); store.writeQuery({ query: FETCH_EVENT, data: { event } });
}, },
@ -373,14 +438,14 @@ export default class Event extends Vue {
actorIsParticipant() { actorIsParticipant() {
if (this.actorIsOrganizer()) return true; if (this.actorIsOrganizer()) return true;
return this.loggedPerson && return this.currentActor &&
this.event.participants this.event.participants
.some(participant => participant.actor.id === this.loggedPerson.id); .some(participant => participant.actor.id === this.currentActor.id);
} }
actorIsOrganizer() { actorIsOrganizer() {
return this.loggedPerson && return this.currentActor && this.event.organizerActor &&
this.loggedPerson.id === this.event.organizerActor.id; this.currentActor.id === this.event.organizerActor.id;
} }
get twitterShareUrl(): string { get twitterShareUrl(): string {
@ -398,6 +463,32 @@ export default class Event extends Vue {
get emailShareUrl(): string { get emailShareUrl(): string {
return `mailto:?to=&body=${this.event.url}${encodeURIComponent('\n\n')}${this.event.description}&subject=${this.event.title}`; 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

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

View file

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

View file

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

View file

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

View file

@ -6,64 +6,51 @@
<h1 class="title">{{ config.name }}</h1> <h1 class="title">{{ config.name }}</h1>
<h2 class="subtitle">{{ config.description }}</h2> <h2 class="subtitle">{{ config.description }}</h2>
<router-link class="button" :to="{ name: 'Register' }" v-if="config.registrationsOpen"> <router-link class="button" :to="{ name: 'Register' }" v-if="config.registrationsOpen">
<translate>Register</translate> {{ $t('Register') }}
</router-link> </router-link>
<p v-else> <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> </p>
</div> </div>
</div> </div>
</section> </section>
<section v-else> <section v-else>
<h1> <h1>
<translate {{ $t('Welcome back {username}', {username: loggedPerson.preferredUsername}) }}
:translate-params="{username: loggedPerson.preferredUsername}"
>Welcome back %{username}</translate>
</h1> </h1>
</section> </section>
<b-dropdown aria-role="list"> <b-dropdown aria-role="list">
<button class="button is-primary" slot="trigger"> <button class="button is-primary" slot="trigger">
<span>Create</span> <span>{{ $t('Create') }}</span>
<b-icon icon="menu-down"></b-icon> <b-icon icon="menu-down"></b-icon>
</button> </button>
<b-dropdown-item aria-role="listitem"> <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>
<b-dropdown-item aria-role="listitem"> <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>
<b-dropdown-item aria-role="listitem">Something else</b-dropdown-item>
</b-dropdown> </b-dropdown>
<section v-if="loggedPerson" class="container"> <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> <b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="goingToEvents.size > 0" v-for="row in Array.from(goingToEvents.entries())"> <div v-if="goingToEvents.size > 0" v-for="row in Array.from(goingToEvents.entries())">
<!-- Iterators will be supported in v-for with VueJS 3 --> <!-- Iterators will be supported in v-for with VueJS 3 -->
<date-component :date="row[0]"></date-component> <date-component :date="row[0]"></date-component>
<h3 class="subtitle" <h3 class="subtitle"
v-if="isToday(row[0])" v-if="isToday(row[0])">
v-translate="{count: row[1].length}" {{ $tc('You have one event today.', row[1].length, {count: row[1].length}) }}
:translate-n="row[1].length"
translate-plural="You have %{ count } events today"
>
You have one event today.
</h3> </h3>
<h3 class="subtitle" <h3 class="subtitle"
v-else-if="isTomorrow(row[0])" v-else-if="isTomorrow(row[0])">
v-translate="{count: row[1].length}" {{ $tc('You have one event tomorrow.', row[1].length, {count: row[1].length}) }}
:translate-n="row[1].length"
translate-plural="You have %{ count } events tomorrow"
>
You have one event tomorrow.
</h3> </h3>
<h3 class="subtitle" <h3 class="subtitle"
v-else v-else>
v-translate="{count: row[1].length, days: calculateDiffDays(row[0])}" {{ $tc('You have one event in {days} days.', row[1].length, {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.
</h3> </h3>
<div class="columns"> <div class="columns">
<EventCard <EventCard
@ -76,11 +63,11 @@
</div> </div>
</div> </div>
<b-message v-else type="is-danger"> <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> </b-message>
</section> </section>
<section class="container"> <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> <b-loading :active.sync="$apollo.loading"></b-loading>
<div v-if="events.length > 0" class="columns is-multiline"> <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"> <div class="column is-one-third-desktop" v-for="event in events.slice(0, 6)" :key="event.uuid">
@ -90,7 +77,7 @@
</div> </div>
</div> </div>
<b-message v-else type="is-danger"> <b-message v-else type="is-danger">
<translate>No events found</translate> {{ $t('No events found') }}
</b-message> </b-message>
</section> </section>
</div> </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"> <div class="column is-centered">
<img src="../assets/oh_no.jpg" alt="Not found 'oh no' picture"> <img src="../assets/oh_no.jpg" alt="Not found 'oh no' picture">
<h1 class="title"> <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> </h1>
<p> <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>
<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> </p>
<!-- The following should just be replaced with the SearchField component but it fails for some reason --> <!-- The following should just be replaced with the SearchField component but it fails for some reason -->
<form @submit="enter"> <form @submit="enter">
<b-field class="search"> <b-field class="search">
<b-input expanded icon="magnify" type="search" :placeholder="searchPlaceHolder" v-model="searchText" /> <b-input expanded icon="magnify" type="search" :placeholder="searchPlaceHolder" v-model="searchText" />
<p class="control"> <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> </p>
</b-field> </b-field>
</form> </form>
@ -39,7 +39,7 @@ export default class PageNotFound extends Vue {
searchText: string = ''; searchText: string = '';
get searchPlaceHolder(): string { get searchPlaceHolder(): string {
return this.$gettext('Search events, groups, etc.'); return this.$t('Search events, groups, etc.') as string;
} }
enter() { enter() {

View file

@ -1,14 +1,16 @@
<template> <template>
<section class="container"> <section class="container">
<h1> <h1>
<translate :translate-params="{ search: this.searchTerm }">Search results: « %{ search } »</translate> {{ $t('Search results: "{search}"', { search: this.searchTerm }) }}
</h1> </h1>
<b-loading :active.sync="$apollo.loading" /> <b-loading :active.sync="$apollo.loading" />
<b-tabs v-model="activeTab" type="is-boxed" class="searchTabs" @change="changeTab"> <b-tabs v-model="activeTab" type="is-boxed" class="searchTabs" @change="changeTab">
<b-tab-item> <b-tab-item>
<template slot="header"> <template slot="header">
<b-icon icon="calendar"></b-icon> <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> </template>
<div v-if="searchEvents.total > 0" class="columns is-multiline"> <div v-if="searchEvents.total > 0" class="columns is-multiline">
<div class="column is-one-quarter-desktop is-half-mobile" <div class="column is-one-quarter-desktop is-half-mobile"
@ -20,13 +22,15 @@
</div> </div>
</div> </div>
<b-message v-else-if="$apollo.loading === false" type="is-danger"> <b-message v-else-if="$apollo.loading === false" type="is-danger">
<translate>No events found</translate> {{ $t('No events found') }}
</b-message> </b-message>
</b-tab-item> </b-tab-item>
<b-tab-item> <b-tab-item>
<template slot="header"> <template slot="header">
<b-icon icon="account-multiple"></b-icon> <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> </template>
<div v-if="searchGroups.total > 0" class="columns is-multiline"> <div v-if="searchGroups.total > 0" class="columns is-multiline">
<div class="column is-one-quarter-desktop is-half-mobile" <div class="column is-one-quarter-desktop is-half-mobile"
@ -36,7 +40,7 @@
</div> </div>
</div> </div>
<b-message v-else-if="$apollo.loading === false" type="is-danger"> <b-message v-else-if="$apollo.loading === false" type="is-danger">
<translate>No groups found</translate> {{ $t('No groups found') }}
</b-message> </b-message>
</b-tab-item> </b-tab-item>
</b-tabs> </b-tabs>

View file

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

View file

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

View file

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

View file

@ -2,24 +2,22 @@
<section class="container"> <section class="container">
<div class="column"> <div class="column">
<h1 class="title"> <h1 class="title">
<translate>Resend confirmation email</translate> {{ $t('Resend confirmation email') }}
</h1> </h1>
<form v-if="!validationSent" @submit="resendConfirmationAction"> <form v-if="!validationSent" @submit="resendConfirmationAction">
<b-field label="Email"> <b-field label="Email">
<b-input aria-required="true" required type="email" v-model="credentials.email"/> <b-input aria-required="true" required type="email" v-model="credentials.email"/>
</b-field> </b-field>
<button class="button is-primary"> <button class="button is-primary">
<translate>Send confirmation email again</translate> {{ $t('Send confirmation email again') }}
</button> </button>
</form> </form>
<div v-else> <div v-else>
<b-message type="is-success" :closable="false" title="Success"> <b-message type="is-success" :closable="false" title="Success">
<translate {{ $t('If an account with this email exists, we just sent another confirmation email to {email}', {email: credentials.email}) }}
:translate-params="{email: credentials.email}"
>If an account with this email exists, we just sent another confirmation email to %{email}</translate>
</b-message> </b-message>
<b-message type="is-info"> <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> </b-message>
</div> </div>
</div> </div>

View file

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

View file

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

View file

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

View file

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

View file

@ -795,18 +795,6 @@
error-stack-parser "^2.0.0" error-stack-parser "^2.0.0"
string-width "^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": "@types/chai@^4.1.7":
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.0.tgz#2478260021408dec32c123a7cad3414beb811a07" resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.2.0.tgz#2478260021408dec32c123a7cad3414beb811a07"
@ -1156,21 +1144,6 @@
semver "^6.0.0" semver "^6.0.0"
string.prototype.padstart "^3.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": "@vue/component-compiler-utils@^3.0.0":
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-3.0.0.tgz#d16fa26b836c06df5baaeb45f3d80afc47e35634" 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" resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d"
integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== 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: abab@^2.0.0:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.1.tgz#3fa17797032b71410ec372e11668f4b4ffc86a82" 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" mime-types "~2.1.24"
negotiator "0.6.2" 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: acorn-globals@^4.3.0:
version "4.3.3" version "4.3.3"
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-4.3.3.tgz#a86f75b69680b8780d30edd21eee4e0ea170c05e" 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 "^6.0.1"
acorn-walk "^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: acorn-jsx@^5.0.2:
version "5.0.2" version "5.0.2"
resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f"
integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== 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: acorn-walk@^6.0.1, acorn-walk@^6.1.1:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c" resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-6.2.0.tgz#123cb8f3b84c2171f1f7fb252615b1c78a6b1a8c"
integrity sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA== 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: acorn@^6.0.1, acorn@^6.0.4, acorn@^6.0.7, acorn@^6.1.1, acorn@^6.2.1:
version "6.3.0" version "6.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-6.3.0.tgz#0087509119ffa4fc0a0041d1e93a417e68cb856e" 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" json-schema-traverse "^0.4.1"
uri-js "^4.2.2" 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: alphanum-sort@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" 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" resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428"
integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= 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: asn1.js@^4.0.0:
version "4.10.1" version "4.10.1"
resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.10.1.tgz#b9c2bf5805f1e64aadeed6df3a2bfafb5a73f5a0" 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" core-js "^2.4.0"
regenerator-runtime "^0.11.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: babylon@^6.18.0:
version "6.18.0" version "6.18.0"
resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" 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" camelcase "^2.0.0"
map-obj "^1.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: camelcase@^2.0.0:
version "2.1.1" version "2.1.1"
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" 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" resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= 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: chai-nightwatch@~0.1.x:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/chai-nightwatch/-/chai-nightwatch-0.1.1.tgz#1ca56de768d3c0868fe7fc2f4d32c2fe894e6be9" 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 "^1.1.1"
upper-case-first "^1.1.0" 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: chardet@^0.7.0:
version "0.7.0" version "0.7.0"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" 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" resolved "https://registry.yarnpkg.com/check-types/-/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
integrity sha512-YpeKZngUmG65rLudJ4taU7VLkOCTMhNl/u4ctNC56LQS/zJTyNH0Lrtwm1tfTsbLlwvlfsA2d1c8vCf/Kh2KwQ== 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: chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.6:
version "2.1.8" version "2.1.8"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" 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" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.6.0.tgz#2ca20dbb9ceb32d4524a683303313f0304b1e497"
integrity sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A== 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: cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/cipher-base/-/cipher-base-1.0.4.tgz#8760e4ecc272f4c363532f926d874aae2c1397de" 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" isobject "^3.0.0"
static-extend "^0.1.1" static-extend "^0.1.1"
clean-css@4.2.x, clean-css@^4.1.11: clean-css@4.2.x:
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.1.tgz#2d411ef76b8569b6d0c84068dabe85b0aa5e5c17"
integrity sha512-4ZxI6dy4lrY6FHzfiy1aEOXgu4LIsW2MhwG0VBKdcoGoH/XLFgaHSdLTGr4O8Be6A8r3MOphEiI8Gc1n0ecf3g== 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" resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.2.0.tgz#e8b988d9206c692302d8ee834e7a85c0144d8f77"
integrity sha512-tgU3fKwzYjiLEQgPMD9Jt+JjHVL9kW93FiIMX/l7rivvOD4/LL0Mf7gda3+4U2KJBloybwgj5KEoQgGRioMiKQ== 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: cli-width@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" 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" arch "^2.1.1"
execa "^1.0.0" 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: cliui@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" 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-convert "^1.9.1"
color-string "^1.5.2" 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: columnify@^1.5.4:
version "1.5.4" version "1.5.4"
resolved "https://registry.yarnpkg.com/columnify/-/columnify-1.5.4.tgz#4737ddf1c7b69a8a7c340570782e947eec8e78bb" 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" snake-case "^2.1.0"
upper-case "^1.1.1" 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: constants-browserify@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" 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" resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w== integrity sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==
css-select@^1.1.0, css-select@~1.2.0: css-select@^1.1.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858"
integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=
@ -3555,7 +3365,7 @@ debug@^3.0.0, debug@^3.1.0, debug@^3.2.5, debug@^3.2.6:
dependencies: dependencies:
ms "^2.1.1" 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" version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
@ -3803,11 +3613,6 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" 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: dom-converter@^0.2:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768" 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" domelementtype "^2.0.1"
entities "^2.0.0" 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: domain-browser@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda"
integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== 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" version "1.3.1"
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f"
integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==
@ -3888,6 +3685,14 @@ dot-case@^2.1.0:
dependencies: dependencies:
no-case "^2.2.0" 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: dot-prop@^4.1.0, dot-prop@^4.1.1:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" 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" resolved "https://registry.yarnpkg.com/easy-stack/-/easy-stack-1.0.0.tgz#12c91b3085a37f0baa336e9486eac4bf94e3e788"
integrity sha1-EskbMIWjfwuqM26UhurEv5Tj54g= 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: ecc-jsbn@~0.1.1:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" 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" memory-fs "^0.4.0"
tapable "^1.0.0" tapable "^1.0.0"
entities@^1.1.1, entities@~1.1.1: entities@^1.1.1:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56"
integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==
@ -4211,6 +4002,11 @@ eslint@^6.0.1:
text-table "^0.2.0" text-table "^0.2.0"
v8-compile-cache "^2.0.3" 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: espree@^6.1.1:
version "6.1.1" version "6.1.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de" resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de"
@ -4658,14 +4454,6 @@ find-up@^3.0.0:
dependencies: dependencies:
locate-path "^3.0.0" 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: find@^0.2.7:
version "0.2.9" version "0.2.9"
resolved "https://registry.yarnpkg.com/find/-/find-0.2.9.tgz#4b73f1ff9e56ad91b76e716407fe5ffe6554bb8c" 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" toposort "^1.0.0"
util.promisify "1.0.0" util.promisify "1.0.0"
htmlparser2@^3.3.0, htmlparser2@^3.9.1: htmlparser2@^3.3.0:
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f"
integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==
@ -5976,13 +5764,6 @@ is-ci@^1.0.10:
dependencies: dependencies:
ci-info "^1.5.0" 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: is-color-stop@^1.0.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/is-color-stop/-/is-color-stop-1.1.0.tgz#cfff471aee4dd5c9e158598fbe12967b5cdad345" 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" resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1"
integrity sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE= 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: is-extendable@^0.1.0, is-extendable@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" 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: dependencies:
isobject "^3.0.1" isobject "^3.0.1"
is-promise@^2.0.0, is-promise@^2.1.0: is-promise@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= 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" resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24"
integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ= integrity sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ=
is-regex@^1.0.3, is-regex@^1.0.4: is-regex@^1.0.4:
version "1.0.4" version "1.0.4"
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= 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" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
integrity sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI= 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: is-windows@^1.0.1, is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -6369,11 +6147,6 @@ js-queue@2.0.0:
dependencies: dependencies:
easy-stack "^1.0.0" 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: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 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" json-schema "0.2.3"
verror "1.10.0" 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: jwa@^1.4.1:
version "1.4.1" version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" 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" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051"
integrity sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA== 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: latest-version@^3.0.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15" 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" chalk "^2.3.0"
shell-quote "^1.6.1" 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: lcid@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" 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" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
integrity sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw== 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" version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== 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" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.3.tgz#77f2eb64be55a404c9fd04ad16d57c1d6d6b1280"
integrity sha512-LoEDv5pgpvWgPF4kNYuIp0qqSJVWak/dML0RY74xlzMZiT9w77teNAwKYKWBTYjlokMirg+o3jBwp+vlLrcfAA== 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: loose-envify@^1.0.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" 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" resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"
integrity sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ== 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: parse5@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-4.0.0.tgz#6d78656e3da8d78b4ec0b906f7c08ef1dfe3f608" 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" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= 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: path-browserify@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-0.0.1.tgz#e6c4ddd7ed3aa27c68a20cc4e50e1a4ee83bbc4a" 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" resolved "https://registry.yarnpkg.com/pn/-/pn-1.1.0.tgz#e2f4cef0e219f463c179ab37463e4e1ecdccbafb"
integrity sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA== 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: popper.js@^1.14.7:
version "1.15.0" version "1.15.0"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.15.0.tgz#5560b99bbad7647e9faa475c6b8056621f5a4ff2" 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 "^7.0.0"
postcss-value-parser "^3.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" version "3.1.1"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865" resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-3.1.1.tgz#4f875f4afb0c96573d5cf4d74011aee250a7e865"
integrity sha1-T4dfSvsMllc9XPTXQBGu4lCn6GU= 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" resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.2.tgz#482282c09a42706d1fc9a069b73f44ec08391dc9"
integrity sha512-LmeoohTpp/K4UiyQCwuGWlONxXamGzCMtFxLq4W1nZVGIQLYvMCJx3yAF9qyyuFpflABI9yVdtJAqbihOsCsJQ== 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" version "6.0.23"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
integrity sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag== 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" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw== 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: pretty-bytes@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" 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" resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= 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: prosemirror-collab@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/prosemirror-collab/-/prosemirror-collab-1.1.2.tgz#622fdc52692a83045ba6914c01a0416ff35f646a" 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" randombytes "^2.0.1"
safe-buffer "^5.1.2" 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: pump@^2.0.0, pump@^2.0.1:
version "2.0.1" version "2.0.1"
resolved "https://registry.yarnpkg.com/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" 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" resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.3.tgz#782e0d825c0c5a3bb39731f84efee6b742e6b1ce"
integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g== integrity sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==
repeat-string@^1.5.2, repeat-string@^1.6.1: repeat-string@^1.6.1:
version "1.6.1" version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc= 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" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= 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" version "1.12.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== 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" resolved "https://registry.yarnpkg.com/rgba-regex/-/rgba-regex-1.0.0.tgz#43374e2e2ca0968b0ef1523460b7d730ff22eeb3"
integrity sha1-QzdOLiyglosO8VI0YLfXMP8i7rM= 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: 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" version "2.7.1"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec"
@ -10288,7 +9881,7 @@ source-map@^0.4.2:
dependencies: dependencies:
amdefine ">=0.0.4" 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" version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= 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" resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M= 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: to-fast-properties@^2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" 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" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"
integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== 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: topo@2.x.x:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/topo/-/topo-2.0.2.tgz#cd5615752539057c0dc0491a621c3bc6fbe1d182" 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" commander "~2.19.0"
source-map "~0.6.1" 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: unicode-canonical-property-names-ecmascript@^1.0.4:
version "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" 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" resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.0.tgz#bd76d6a23323e2ca8ffa12028dc04559c75f9019"
integrity sha512-iq+S7vZJE60yejDYM0ek6zg308+UZsdtPExWP9VZoCFCz1zkJoXFnAX7aZfd/ZwrkidzdUZL0C/ryW+JwAiIGw== 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: vue-apollo@^3.0.0-rc.1:
version "3.0.0-rc.2" version "3.0.0-rc.2"
resolved "https://registry.yarnpkg.com/vue-apollo/-/vue-apollo-3.0.0-rc.2.tgz#3b59a93084cb37c19be46b9f18cd63214a782718" 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: dependencies:
webpack-bundle-analyzer "^3.3.2" 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: vue-hot-reload-api@^2.3.0:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf" 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== 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: vue-loader@^15.7.0:
version "15.7.1" version "15.7.1"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.1.tgz#6ccacd4122aa80f69baaac08ff295a62e3aefcfd" 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" loader-utils "^1.2.3"
svgo "^1.3.0" 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" version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc" 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== 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" de-indent "^1.0.2"
he "^1.1.0" 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" version "1.9.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825" resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw== integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
@ -11865,24 +11440,6 @@ widest-line@^2.0.0:
dependencies: dependencies:
string-width "^2.1.1" 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: wordwrap@~0.0.2:
version "0.0.3" version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
@ -12220,7 +11777,7 @@ yargs@^11.0.0:
y18n "^3.2.1" y18n "^3.2.1"
yargs-parser "^9.0.2" yargs-parser "^9.0.2"
yargs@^13.0.0: yargs@^13.0.0, yargs@^13.2.2:
version "13.3.0" version "13.3.0"
resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83"
integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==
@ -12274,16 +11831,6 @@ yargs@^8.0.2:
y18n "^3.2.1" y18n "^3.2.1"
yargs-parser "^7.0.0" 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: yauzl@2.4.1:
version "2.4.1" version "2.4.1"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"

View file

@ -42,6 +42,7 @@ defmodule Mobilizon do
Mobilizon.Service.Federator, Mobilizon.Service.Federator,
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1), cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
cachex_spec(:ics, 2500, 60, 60, &ICalendar.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) 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() @spec set_url(Ecto.Changeset.t()) :: Ecto.Changeset.t()
defp set_url(%Ecto.Changeset{changes: changes} = changeset) do defp set_url(%Ecto.Changeset{changes: changes} = changeset) do
url = uuid = Ecto.UUID.generate()
Map.get( url = Map.get(changes, :url, "#{MobilizonWeb.Endpoint.url()}/address/#{uuid}")
changes,
:url,
"#{MobilizonWeb.Endpoint.url()}/address/#{Ecto.UUID.generate()}"
)
put_change(changeset, :url, url) put_change(changeset, :url, url)
end end

View file

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

View file

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

View file

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

View file

@ -657,6 +657,28 @@ defmodule Mobilizon.Events do
|> Repo.all() |> Repo.all()
end 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 """ @doc """
Gets a single session. Gets a single session.
Raises `Ecto.NoResultsError` if the session does not exist. 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) from(p in Participant, where: p.actor_id == ^actor_id and p.role == ^:not_approved)
end 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() @spec event_participations_for_actor_query(integer) :: Ecto.Query.t()
def event_participations_for_actor_query(actor_id) do def event_participations_for_actor_query(actor_id) do
from( from(
@ -1244,11 +1271,18 @@ defmodule Mobilizon.Events do
from(q in query, where: q.visibility == ^:public) from(q in query, where: q.visibility == ^:public)
end end
@spec filter_role(Ecto.Query.t(), boolean) :: Ecto.Query.t() @spec filter_approved_role(Ecto.Query.t()) :: Ecto.Query.t()
defp filter_role(query, false) do defp filter_approved_role(query) do
from(p in query, where: p.role != ^:not_approved) from(p in query, where: p.role != ^:not_approved)
end 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 defp filter_role(query, true), do: query
@spec preload_for_event(Ecto.Query.t()) :: Ecto.Query.t() @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] @required_attrs [:content, :moderator_id, :report_id]
@attrs @required_attrs @attrs @required_attrs
@timestamps_opts [type: :utc_datetime]
@type t :: %__MODULE__{ @type t :: %__MODULE__{
content: String.t(), content: String.t(),
report: Report.t(), report: Report.t(),

View file

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

View file

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

View file

@ -135,4 +135,13 @@ defmodule MobilizonWeb.API.Events do
} }
end end
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 end

View file

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

View file

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

View file

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

View file

@ -2,10 +2,13 @@ defmodule MobilizonWeb.Resolvers.Admin do
@moduledoc """ @moduledoc """
Handles the report-related GraphQL calls Handles the report-related GraphQL calls
""" """
alias Mobilizon.Events
alias Mobilizon.Users.User alias Mobilizon.Users.User
import Mobilizon.Users.Guards import Mobilizon.Users.Guards
alias Mobilizon.Admin.ActionLog alias Mobilizon.Admin.ActionLog
alias Mobilizon.Reports.{Report, Note} alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Events.Event
alias Mobilizon.Service.Statistics
def list_action_logs(_parent, %{page: page, limit: limit}, %{ def list_action_logs(_parent, %{page: page, limit: limit}, %{
context: %{current_user: %User{role: role}} context: %{current_user: %User{role: role}}
@ -17,14 +20,19 @@ defmodule MobilizonWeb.Resolvers.Admin do
target_type: target_type, target_type: target_type,
action: action, action: action,
actor: actor, actor: actor,
id: id id: id,
inserted_at: inserted_at
} = action_log -> } = action_log ->
transform_action_log(target_type, action, action_log) with data when is_map(data) <-
|> Map.merge(%{ transform_action_log(String.to_existing_atom(target_type), action, action_log) do
Map.merge(data, %{
actor: actor, actor: actor,
id: id id: id,
inserted_at: inserted_at
}) })
end
end) end)
|> Enum.filter(& &1)
{:ok, action_logs} {:ok, action_logs}
end end
@ -35,38 +43,87 @@ defmodule MobilizonWeb.Resolvers.Admin do
end end
defp transform_action_log( defp transform_action_log(
"Elixir.Mobilizon.Reports.Report", Report,
"update", :update,
%ActionLog{} = action_log %ActionLog{} = action_log
) do ) 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 object: report
} }
end end
end end
defp transform_action_log("Elixir.Mobilizon.Reports.Note", "create", %ActionLog{ defp transform_action_log(Note, :create, %ActionLog{
changes: changes changes: changes
}) do }) do
%{ %{
action: "note_creation", action: :note_creation,
object: convert_changes_to_struct(Note, changes) object: convert_changes_to_struct(Note, changes)
} }
end end
defp transform_action_log("Elixir.Mobilizon.Reports.Note", "delete", %ActionLog{ defp transform_action_log(Note, :delete, %ActionLog{
changes: changes changes: changes
}) do }) do
%{ %{
action: "note_deletion", action: :note_deletion,
object: convert_changes_to_struct(Note, changes) object: convert_changes_to_struct(Note, changes)
} }
end 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 # 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 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
end end

View file

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

View file

@ -76,7 +76,9 @@ defmodule MobilizonWeb.Resolvers.Group do
%{group_id: group_id, actor_id: actor_id}, %{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}} %{context: %{current_user: user}}
) do ) 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), {:is_owned, %Actor{}} <- User.owns_actor(user, actor_id),
{:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id), {:ok, %Member{} = member} <- Actors.get_member(actor_id, group.id),
{:is_admin, true} <- Member.is_administrator(member), {:is_admin, true} <- Member.is_administrator(member),
@ -109,7 +111,9 @@ defmodule MobilizonWeb.Resolvers.Group do
%{group_id: group_id, actor_id: actor_id}, %{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}} %{context: %{current_user: user}}
) do ) 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), {:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:error, :member_not_found} <- Actors.get_member(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)}, {: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}, %{group_id: group_id, actor_id: actor_id},
%{context: %{current_user: user}} %{context: %{current_user: user}}
) do ) 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), {:ok, %Member{} = member} <- Actors.get_member(actor.id, group_id),
{:only_administrator, false} <- {:only_administrator, false} <-
{:only_administrator, check_that_member_is_not_last_administrator(group_id, actor_id)}, {: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( def list_reports(
_parent, _parent,
%{page: page, limit: limit}, %{page: page, limit: limit, status: status},
%{context: %{current_user: %User{role: role}}} %{context: %{current_user: %User{role: role}}}
) )
when is_moderator(role) do when is_moderator(role) do
{:ok, Mobilizon.Reports.list_reports(page, limit)} {:ok, Mobilizon.Reports.list_reports(page, limit, :updated_at, :desc, status)}
end end
def list_reports(_parent, _args, _resolution) do 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}}}) def get_report(_parent, %{id: id}, %{context: %{current_user: %User{role: role}}})
when is_moderator(role) do 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 end
def get_report(_parent, _args, _resolution) do def get_report(_parent, _args, _resolution) do

View file

@ -78,6 +78,9 @@ defmodule MobilizonWeb.Router do
get("/events/create", PageController, :index) get("/events/create", PageController, :index)
get("/events/list", PageController, :index) get("/events/list", PageController, :index)
get("/events/:uuid/edit", 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 end
scope "/", MobilizonWeb do scope "/", MobilizonWeb do

View file

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

View file

@ -13,7 +13,7 @@ defmodule MobilizonWeb.Schema.ActorInterface do
@desc "An ActivityPub actor" @desc "An ActivityPub actor"
interface :actor do 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(:url, :string, description: "The ActivityPub actor's URL")
field(:type, :actor_type, description: "The type of Actor (Person, Group,…)") field(:type, :actor_type, description: "The type of Actor (Person, Group,…)")
field(:name, :string, description: "The actor's displayed name") field(:name, :string, description: "The actor's displayed name")

View file

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

View file

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

View file

@ -15,7 +15,7 @@ defmodule MobilizonWeb.Schema.Actors.PersonType do
""" """
object :person do object :person do
interfaces([:actor]) 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(: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") 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(:country, :string)
field(:description, :string) field(:description, :string)
field(:url, :string) field(:url, :string)
field(:id, :integer) field(:id, :id)
field(:origin_id, :string) field(:origin_id, :string)
end end
@ -40,7 +40,7 @@ defmodule MobilizonWeb.Schema.AddressType do
field(:country, :string) field(:country, :string)
field(:description, :string) field(:description, :string)
field(:url, :string) field(:url, :string)
field(:id, :integer) field(:id, :id)
field(:origin_id, :string) field(:origin_id, :string)
end end

View file

@ -5,13 +5,25 @@ defmodule MobilizonWeb.Schema.AdminType do
use Absinthe.Schema.Notation use Absinthe.Schema.Notation
alias MobilizonWeb.Resolvers.Admin alias MobilizonWeb.Resolvers.Admin
alias Mobilizon.Reports.{Report, Note} alias Mobilizon.Reports.{Report, Note}
alias Mobilizon.Events.Event
@desc "An action log" @desc "An action log"
object :action_log do object :action_log do
field(:id, :id, description: "Internal ID for this comment") field(:id, :id, description: "Internal ID for this comment")
field(:actor, :actor, description: "The actor that acted") field(:actor, :actor, description: "The actor that acted")
field(:object, :action_log_object, description: "The object that was acted upon") 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 end
@desc "The objects that can be in an action log" @desc "The objects that can be in an action log"
@ -25,11 +37,22 @@ defmodule MobilizonWeb.Schema.AdminType do
%Note{}, _ -> %Note{}, _ ->
:report_note :report_note
%Event{}, _ ->
:event
_, _ -> _, _ ->
nil nil
end) end)
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 object :admin_queries do
@desc "Get the list of action logs" @desc "Get the list of action logs"
field :action_logs, type: list_of(:action_log) do field :action_logs, type: list_of(:action_log) do
@ -37,5 +60,9 @@ defmodule MobilizonWeb.Schema.AdminType do
arg(:limit, :integer, default_value: 10) arg(:limit, :integer, default_value: 10)
resolve(&Admin.list_action_logs/3) resolve(&Admin.list_action_logs/3)
end end
field :dashboard, type: :dashboard do
resolve(&Admin.get_dashboard/3)
end
end end
end end

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