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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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