Merge branch 'master' into chapril-custom-front

This commit is contained in:
tykayn 2020-12-01 17:11:57 +01:00 committed by Baptiste Lemoine
commit f90d324917
224 changed files with 5221 additions and 2516 deletions

View File

@ -146,6 +146,10 @@ config :mobilizon, :ldap,
base: System.get_env("LDAP_BASE") || "dc=example,dc=com",
uid: System.get_env("LDAP_UID") || "cn",
require_bind_for_search: !(System.get_env("LDAP_REQUIRE_BIND_FOR_SEARCH") == "false"),
# The full CN to filter by `memberOf`, or `false` if disabled
group: false,
# Either the admin UID matching the field in `uid`,
# Either a tuple with the fully qualified DN: {:full, uid=admin,dc=example.com,dc=local}
bind_uid: System.get_env("LDAP_BIND_UID"),
bind_password: System.get_env("LDAP_BIND_PASSWORD")
@ -158,17 +162,14 @@ config :geolix,
}
]
config :auto_linker,
opts: [
scheme: true,
extra: true,
# TODO: Set to :no_scheme when it works properly
validate_tld: true,
class: false,
strip_prefix: false,
new_window: true,
rel: "noopener noreferrer ugc"
]
config :mobilizon, Mobilizon.Service.Formatter,
class: false,
rel: "noopener noreferrer ugc",
new_window: true,
truncate: false,
strip_prefix: false,
extra: true,
validate_tld: :no_scheme
config :tesla, adapter: Tesla.Adapter.Hackney

View File

@ -4,4 +4,4 @@ indent_size = 2
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
max_line_length = 80

View File

@ -38,8 +38,11 @@ module.exports = {
"error",
{
ignoreStrings: true,
ignoreHTMLTextContents: true,
ignoreTemplateLiterals: true,
ignoreComments: true,
template: 170,
code: 100,
code: 80,
},
],
"prettier/prettier": "error",
@ -48,13 +51,18 @@ module.exports = {
"import/prefer-default-export": "off",
"import/extensions": "off",
"import/no-unresolved": "off",
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
},
ignorePatterns: ["src/typings/*.d.ts", "vue.config.js"],
overrides: [
{
files: ["**/__tests__/*.{j,t}s?(x)", "**/tests/unit/**/*.spec.{j,t}s?(x)"],
files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)",
],
env: {
mocha: true,
},

View File

@ -24,7 +24,9 @@ fetch(`http://localhost:4000/api`, {
.then((result) => result.json())
.then((result) => {
// here we're filtering out any type information unrelated to unions or interfaces
const filteredData = result.data.__schema.types.filter((type) => type.possibleTypes !== null);
const filteredData = result.data.__schema.types.filter(
(type) => type.possibleTypes !== null
);
result.data.__schema.types = filteredData;
fs.writeFile("./fragmentTypes.json", JSON.stringify(result.data), (err) => {
if (err) {

View File

@ -3,7 +3,6 @@
"version": "1.0.2",
"private": true,
"scripts": {
"start": "vue-cli-service serve",
"serve": "vue-cli-service serve",
"build": "vue-cli-service build --modern",
"test:unit": "vue-cli-service test:unit",
@ -64,13 +63,13 @@
"@types/vuedraggable": "^2.23.0",
"@typescript-eslint/eslint-plugin": "^4.0.1",
"@typescript-eslint/parser": "^4.0.1",
"@vue/cli-plugin-babel": "~4.5.8",
"@vue/cli-plugin-e2e-cypress": "~4.5.8",
"@vue/cli-plugin-eslint": "~4.5.8",
"@vue/cli-plugin-pwa": "~4.5.8",
"@vue/cli-plugin-router": "~4.5.8",
"@vue/cli-plugin-typescript": "~4.5.8",
"@vue/cli-service": "~4.5.8",
"@vue/cli-plugin-babel": "~4.5.9",
"@vue/cli-plugin-e2e-cypress": "~4.5.9",
"@vue/cli-plugin-eslint": "~4.5.9",
"@vue/cli-plugin-pwa": "~4.5.9",
"@vue/cli-plugin-router": "~4.5.9",
"@vue/cli-plugin-typescript": "~4.5.9",
"@vue/cli-service": "~4.5.9",
"@vue/eslint-config-airbnb": "^5.0.2",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^7.0.0",
@ -80,11 +79,11 @@
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^7.0.0",
"prettier": "2.1.2",
"prettier-eslint": "^11.0.0",
"prettier": "2.2.1",
"prettier-eslint": "^12.0.0",
"sass": "^1.29.0",
"sass-loader": "^10.0.1",
"typescript": "~4.0.2",
"typescript": "~4.1.2",
"vue-cli-plugin-svg": "~0.1.3",
"vue-i18n-extract": "^1.0.2",
"vue-template-compiler": "^2.6.11",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 668 B

BIN
js/public/img/mobilizon_default_card.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
js/public/img/mobilizon_logo.png Executable file → Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

18
js/src/@types/dom.d.ts vendored Normal file
View File

@ -0,0 +1,18 @@
declare global {
interface GeolocationCoordinates {
readonly accuracy: number;
readonly altitude: number | null;
readonly altitudeAccuracy: number | null;
readonly heading: number | null;
readonly latitude: number;
readonly longitude: number;
readonly speed: number | null;
}
interface GeolocationPosition {
readonly coords: GeolocationCoordinates;
readonly timestamp: number;
}
}
export {};

View File

@ -32,8 +32,16 @@
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import NavBar from "./components/NavBar.vue";
import { AUTH_ACCESS_TOKEN, AUTH_USER_EMAIL, AUTH_USER_ID, AUTH_USER_ROLE } from "./constants";
import { CURRENT_USER_CLIENT, UPDATE_CURRENT_USER_CLIENT } from "./graphql/user";
import {
AUTH_ACCESS_TOKEN,
AUTH_USER_EMAIL,
AUTH_USER_ID,
AUTH_USER_ROLE,
} from "./constants";
import {
CURRENT_USER_CLIENT,
UPDATE_CURRENT_USER_CLIENT,
} from "./graphql/user";
import Footer from "./components/Footer.vue";
import Logo from "./components/Logo.vue";
import { initializeCurrentActor } from "./utils/auth";

View File

@ -1,8 +1,11 @@
import { ICurrentUserRole } from "@/types/enums";
import { ApolloCache } from "apollo-cache";
import { NormalizedCacheObject } from "apollo-cache-inmemory";
import { ICurrentUserRole } from "@/types/current-user.model";
import { Resolvers } from "apollo-client/core/types";
export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCacheObject>) {
export default function buildCurrentUserResolver(
cache: ApolloCache<NormalizedCacheObject>
): Resolvers {
cache.writeData({
data: {
currentUser: {
@ -53,7 +56,12 @@ export default function buildCurrentUserResolver(cache: ApolloCache<NormalizedCa
preferredUsername,
avatar,
name,
}: { id: string; preferredUsername: string; avatar: string; name: string },
}: {
id: string;
preferredUsername: string;
avatar: string;
name: string;
},
{ cache: localCache }: { cache: ApolloCache<NormalizedCacheObject> }
) => {
const data = {

View File

@ -1,4 +1,7 @@
import { IntrospectionFragmentMatcher, NormalizedCacheObject } from "apollo-cache-inmemory";
import {
IntrospectionFragmentMatcher,
NormalizedCacheObject,
} from "apollo-cache-inmemory";
import { AUTH_ACCESS_TOKEN, AUTH_REFRESH_TOKEN } from "@/constants";
import { REFRESH_TOKEN } from "@/graphql/auth";
import { saveTokenData } from "@/utils/auth";
@ -11,7 +14,11 @@ export const fragmentMatcher = new IntrospectionFragmentMatcher({
{
kind: "UNION",
name: "SearchResult",
possibleTypes: [{ name: "Event" }, { name: "Person" }, { name: "Group" }],
possibleTypes: [
{ name: "Event" },
{ name: "Person" },
{ name: "Group" },
],
},
{
kind: "INTERFACE",

View File

@ -4,10 +4,6 @@
@import "~bulma-divider";
@import "~buefy/src/scss/buefy";
// a {
// color: $violet-2;
// }
a.out,
.content a,
.ProseMirror a {
@ -16,9 +12,6 @@ a.out,
text-decoration-thickness: 2px;
}
// input.input {
// border-color: $input-border-color !important;
// }
.section {
padding: 1rem 1% 4rem;

View File

@ -13,7 +13,12 @@
<template slot-scope="props">
<div class="media">
<div class="media-left">
<img width="32" :src="props.option.avatar.url" v-if="props.option.avatar" alt="" />
<img
width="32"
:src="props.option.avatar.url"
v-if="props.option.avatar"
alt=""
/>
<b-icon v-else icon="account-circle" />
</div>
<div class="media-content">
@ -21,7 +26,9 @@
{{ props.option.name }}
<br />
<small>{{ `@${props.option.preferredUsername}` }}</small>
<small v-if="props.option.domain">{{ `@${props.option.domain}` }}</small>
<small v-if="props.option.domain">{{
`@${props.option.domain}`
}}</small>
</span>
<span v-else>
{{ `@${props.option.preferredUsername}` }}
@ -53,7 +60,9 @@ export default class ActorAutoComplete extends Vue {
selected: IPerson | null = this.defaultSelected;
name: string = this.defaultSelected ? this.defaultSelected.preferredUsername : "";
name: string = this.defaultSelected
? this.defaultSelected.preferredUsername
: "";
page = 1;

View File

@ -12,8 +12,15 @@
<p>
{{ actor.name || `@${usernameWithDomain(actor)}` }}
</p>
<p class="has-text-grey" v-if="actor.name">@{{ usernameWithDomain(actor) }}</p>
<div v-if="full" class="summary" :class="{ limit: limit }" v-html="actor.summary" />
<p class="has-text-grey" v-if="actor.name">
@{{ usernameWithDomain(actor) }}
</p>
<div
v-if="full"
class="summary"
:class="{ limit: limit }"
v-html="actor.summary"
/>
</div>
</div>
</div>

View File

@ -7,7 +7,10 @@
<ul class="identities">
<li v-for="identity in identities" :key="identity.id">
<router-link
:to="{ name: 'UpdateIdentity', params: { identityName: identity.preferredUsername } }"
:to="{
name: 'UpdateIdentity',
params: { identityName: identity.preferredUsername },
}"
class="media identity"
v-bind:class="{ 'is-current-identity': isCurrentIdentity(identity) }"
>
@ -24,7 +27,10 @@
</li>
</ul>
<router-link :to="{ name: 'CreateIdentity' }" class="button create-identity is-primary">
<router-link
:to="{ name: 'CreateIdentity' }"
class="button create-identity is-primary"
>
{{ $t("Create a new identity") }}
</router-link>
</section>
@ -53,7 +59,7 @@ export default class Identities extends Vue {
errors: string[] = [];
isCurrentIdentity(identity: IPerson) {
isCurrentIdentity(identity: IPerson): boolean {
return identity.preferredUsername === this.currentIdentityName;
}
}

View File

@ -12,8 +12,9 @@
</v-popover>
</template>
<script lang="ts">
import { ActorType } from "@/types/enums";
import { Component, Vue, Prop } from "vue-property-decorator";
import { IActor, ActorType } from "../../types/actor";
import { IActor } from "../../types/actor";
import ActorCard from "./ActorCard.vue";
@Component({

View File

@ -16,11 +16,21 @@
checkable
checkbox-position="left"
>
<b-table-column field="actor.id" label="ID" width="40" numeric v-slot="props">{{
props.row.actor.id
}}</b-table-column>
<b-table-column
field="actor.id"
label="ID"
width="40"
numeric
v-slot="props"
>{{ props.row.actor.id }}</b-table-column
>
<b-table-column field="actor.type" :label="$t('Type')" width="80" v-slot="props">
<b-table-column
field="actor.type"
:label="$t('Type')"
width="80"
v-slot="props"
>
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.actor)" />
<b-icon icon="account-circle" v-else />
</b-table-column>
@ -33,26 +43,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<span
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column>
<b-table-column field="actor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.actor)">{{
props.row.actor.domain
}}</a>
<a
@click="toggle(props.row)"
v-if="RelayMixin.isInstance(props.row.actor)"
>{{ props.row.actor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{
`${props.row.actor.preferredUsername}@${props.row.actor.domain}`
}}</a>
</template>
</b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
}}</span></b-table-column
<b-table-column
field="targetActor.updatedAt"
:label="$t('Date')"
sortable
v-slot="props"
>
<span
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
>{{
formatDistanceToNow(new Date(props.row.updatedAt), {
locale: $dateFnsLocale,
})
}}</span
></b-table-column
>
<template slot="detail" slot-scope="props">
@ -143,7 +166,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
@ -158,7 +185,11 @@ export default class Followers extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowers.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}

View File

@ -1,13 +1,22 @@
<template>
<div>
<form @submit="followRelay">
<b-field :label="$t('Add an instance')" custom-class="add-relay" horizontal>
<b-field
:label="$t('Add an instance')"
custom-class="add-relay"
horizontal
>
<b-field grouped expanded size="is-large">
<p class="control">
<b-input v-model="newRelayAddress" :placeholder="$t('Ex: mobilizon.fr')" />
<b-input
v-model="newRelayAddress"
:placeholder="$t('Ex: mobilizon.fr')"
/>
</p>
<p class="control">
<b-button type="is-primary" native-type="submit">{{ $t("Add an instance") }}</b-button>
<b-button type="is-primary" native-type="submit">{{
$t("Add an instance")
}}</b-button>
</p>
</b-field>
</b-field>
@ -29,12 +38,25 @@
checkable
checkbox-position="left"
>
<b-table-column field="targetActor.id" label="ID" width="40" numeric v-slot="props">{{
props.row.targetActor.id
}}</b-table-column>
<b-table-column
field="targetActor.id"
label="ID"
width="40"
numeric
v-slot="props"
>{{ props.row.targetActor.id }}</b-table-column
>
<b-table-column field="targetActor.type" :label="$t('Type')" width="80" v-slot="props">
<b-icon icon="lan" v-if="RelayMixin.isInstance(props.row.targetActor)" />
<b-table-column
field="targetActor.type"
:label="$t('Type')"
width="80"
v-slot="props"
>
<b-icon
icon="lan"
v-if="RelayMixin.isInstance(props.row.targetActor)"
/>
<b-icon icon="account-circle" v-else />
</b-table-column>
@ -46,26 +68,39 @@
centered
v-slot="props"
>
<span :class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`">{{
props.row.approved ? $t("Accepted") : $t("Pending")
}}</span>
<span
:class="`tag ${props.row.approved ? 'is-success' : 'is-danger'}`"
>{{ props.row.approved ? $t("Accepted") : $t("Pending") }}</span
>
</b-table-column>
<b-table-column field="targetActor.domain" :label="$t('Domain')" sortable>
<template v-slot:default="props">
<a @click="toggle(props.row)" v-if="RelayMixin.isInstance(props.row.targetActor)">{{
props.row.targetActor.domain
}}</a>
<a
@click="toggle(props.row)"
v-if="RelayMixin.isInstance(props.row.targetActor)"
>{{ props.row.targetActor.domain }}</a
>
<a @click="toggle(props.row)" v-else>{{
`${props.row.targetActor.preferredUsername}@${props.row.targetActor.domain}`
}}</a>
</template>
</b-table-column>
<b-table-column field="targetActor.updatedAt" :label="$t('Date')" sortable v-slot="props">
<span :title="$options.filters.formatDateTimeString(props.row.updatedAt)">{{
formatDistanceToNow(new Date(props.row.updatedAt), { locale: $dateFnsLocale })
}}</span></b-table-column
<b-table-column
field="targetActor.updatedAt"
:label="$t('Date')"
sortable
v-slot="props"
>
<span
:title="$options.filters.formatDateTimeString(props.row.updatedAt)"
>{{
formatDistanceToNow(new Date(props.row.updatedAt), {
locale: $dateFnsLocale,
})
}}</span
></b-table-column
>
<template slot="detail" slot-scope="props">
@ -103,7 +138,6 @@ import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns";
import { ADD_RELAY, REMOVE_RELAY } from "../../graphql/admin";
import { IFollower } from "../../types/actor/follower.model";
import { Paginate } from "../../types/paginate";
import RelayMixin from "../../mixins/relay";
@Component({
@ -133,13 +167,19 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowings.refetch();
this.newRelayAddress = "";
} catch (err) {
Snackbar.open({ message: err.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: err.message,
type: "is-danger",
position: "is-bottom",
});
}
}
async removeRelays(): Promise<void> {
await this.checkedRows.forEach((row: IFollower) => {
this.removeRelay(`${row.targetActor.preferredUsername}@${row.targetActor.domain}`);
this.removeRelay(
`${row.targetActor.preferredUsername}@${row.targetActor.domain}`
);
});
}
@ -154,7 +194,11 @@ export default class Followings extends Mixins(RelayMixin) {
await this.$apollo.queries.relayFollowings.refetch();
this.checkedRows = [];
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -1,19 +1,34 @@
<template>
<li :class="{ reply: comment.inReplyToComment }">
<article class="media" :class="{ selected: commentSelected }" :id="commentId">
<article
class="media"
:class="{ selected: commentSelected }"
:id="commentId"
>
<popover-actor-card
class="media-left"
:actor="comment.actor"
:inline="true"
v-if="comment.actor"
>
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar">
<figure
class="image is-48x48"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
</popover-actor-card>
<div v-else class="media-left">
<figure class="image is-48x48" v-if="!comment.deletedAt && comment.actor.avatar">
<figure
class="image is-48x48"
v-if="!comment.deletedAt && comment.actor.avatar"
>
<img class="is-rounded" :src="comment.actor.avatar.url" alt="" />
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
@ -21,7 +36,9 @@
<div class="media-content">
<div class="content">
<span class="first-line" v-if="!comment.deletedAt">
<strong :class="{ organizer: commentFromOrganizer }">{{ comment.actor.name }}</strong>
<strong :class="{ organizer: commentFromOrganizer }">{{
comment.actor.name
}}</strong>
<small>@{{ usernameWithDomain(comment.actor) }}</small>
<a class="comment-link has-text-grey" :href="commentURL">
<small>{{
@ -54,10 +71,15 @@
<div class="load-replies" v-if="comment.totalReplies">
<p v-if="!showReplies" @click="fetchReplies">
<b-icon icon="chevron-down" /><span>{{
$tc("View a reply", comment.totalReplies, { totalReplies: comment.totalReplies })
$tc("View a reply", comment.totalReplies, {
totalReplies: comment.totalReplies,
})
}}</span>
</p>
<p v-else-if="comment.totalReplies && showReplies" @click="showReplies = false">
<p
v-else-if="comment.totalReplies && showReplies"
@click="showReplies = false"
>
<b-icon icon="chevron-up" />
<span>{{ $t("Hide replies") }}</span>
</p>
@ -86,14 +108,24 @@
</nav>
</div>
</article>
<form class="reply" @submit.prevent="replyToComment" v-if="currentActor.id" v-show="replyTo">
<form
class="reply"
@submit.prevent="replyToComment"
v-if="currentActor.id"
v-show="replyTo"
>
<article class="media reply">
<figure class="media-left" v-if="currentActor.avatar">
<p class="image is-48x48">
<img :src="currentActor.avatar.url" alt="" />
</p>
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<div class="content">
<span class="first-line">
@ -102,7 +134,12 @@
</span>
<br />
<span class="editor-line">
<editor class="editor" ref="commentEditor" v-model="newComment.text" mode="comment" />
<editor
class="editor"
ref="commentEditor"
v-model="newComment.text"
mode="comment"
/>
<b-button
:disabled="newComment.text.trim().length === 0"
native-type="submit"
@ -118,7 +155,12 @@
<div class="left">
<div class="vertical-border" @click="showReplies = false" />
</div>
<transition-group name="comment-replies" v-if="showReplies" class="comment-replies" tag="ul">
<transition-group
name="comment-replies"
v-if="showReplies"
class="comment-replies"
tag="ul"
>
<comment
class="reply"
v-for="reply in comment.replies"
@ -137,7 +179,7 @@ import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import EditorComponent from "@/components/Editor.vue";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { formatDistanceToNow } from "date-fns";
import { CommentModeration } from "../../types/event-options.model";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
import { IPerson, usernameWithDomain } from "../../types/actor";
@ -155,7 +197,8 @@ import PopoverActorCard from "../Account/PopoverActorCard.vue";
},
},
components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
comment: () => import(/* webpackChunkName: "comment" */ "./Comment.vue"),
PopoverActorCard,
},
@ -167,7 +210,9 @@ export default class Comment extends Vue {
// Hack because Vue only exports it's own interface.
// See https://github.com/kaorun343/vue-property-decorator/issues/257
@Ref() readonly commentEditor!: EditorComponent & { replyToComment: (comment: IComment) => void };
@Ref() readonly commentEditor!: EditorComponent & {
replyToComment: (comment: IComment) => void;
};
currentActor!: IPerson;
@ -231,7 +276,9 @@ export default class Comment extends Vue {
if (!eventData) return;
const { event } = eventData;
const { comments } = event;
const parentCommentIndex = comments.findIndex((oldComment) => oldComment.id === parentId);
const parentCommentIndex = comments.findIndex(
(oldComment) => oldComment.id === parentId
);
const parentComment = comments[parentCommentIndex];
if (!parentComment) return;
parentComment.replies = thread;
@ -303,7 +350,11 @@ export default class Comment extends Vue {
duration: 5000,
});
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -6,9 +6,11 @@
@submit.prevent="createCommentForEvent(newComment)"
@keyup.ctrl.enter="createCommentForEvent(newComment)"
>
<b-notification v-if="isEventOrganiser && !areCommentsClosed" :closable="false">{{
$t("Comments are closed for everybody else.")
}}</b-notification>
<b-notification
v-if="isEventOrganiser && !areCommentsClosed"
:closable="false"
>{{ $t("Comments are closed for everybody else.") }}</b-notification
>
<article class="media">
<figure class="media-left">
<identity-picker-wrapper :inline="false" v-model="newComment.actor" />
@ -16,11 +18,17 @@
<div class="media-content">
<div class="field">
<p class="control">
<editor ref="commenteditor" mode="comment" v-model="newComment.text" />
<editor
ref="commenteditor"
mode="comment"
v-model="newComment.text"
/>
</p>
</div>
<div class="send-comment">
<b-button native-type="submit" type="is-primary">{{ $t("Post a comment") }}</b-button>
<b-button native-type="submit" type="is-primary">{{
$t("Post a comment")
}}</b-button>
</div>
</div>
</article>
@ -29,7 +37,12 @@
$t("The organiser has chosen to close comments.")
}}</b-notification>
<transition name="comment-empty-list" mode="out-in">
<transition-group name="comment-list" v-if="comments.length" class="comment-list" tag="ul">
<transition-group
name="comment-list"
v-if="comments.length"
class="comment-list"
tag="ul"
>
<comment
class="root-comment"
:comment="comment"
@ -51,7 +64,7 @@
import { Prop, Vue, Component, Watch } from "vue-property-decorator";
import Comment from "@/components/Comment/Comment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "../../types/event-options.model";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import {
CREATE_COMMENT_FROM_EVENT,
@ -76,7 +89,9 @@ import { IEvent } from "../../types/event.model";
};
},
update(data) {
return data.event.comments.map((comment: IComment) => new CommentModel(comment));
return data.event.comments.map(
(comment: IComment) => new CommentModel(comment)
);
},
skip() {
return !this.event.uuid;
@ -86,7 +101,8 @@ import { IEvent } from "../../types/event.model";
components: {
Comment,
IdentityPickerWrapper,
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
})
export default class CommentTree extends Vue {
@ -113,7 +129,9 @@ export default class CommentTree extends Vue {
variables: {
eventId: this.event.id,
text: comment.text,
inReplyToCommentId: comment.inReplyToComment ? comment.inReplyToComment.id : null,
inReplyToCommentId: comment.inReplyToComment
? comment.inReplyToComment.id
: null,
},
update: (store, { data }) => {
if (data == null) return;
@ -228,7 +246,9 @@ export default class CommentTree extends Vue {
});
if (!localData) return;
const { thread: oldReplyList } = localData;
const replies = oldReplyList.filter((reply) => reply.id !== deletedCommentId);
const replies = oldReplyList.filter(
(reply) => reply.id !== deletedCommentId
);
store.writeQuery({
query: FETCH_THREAD_REPLIES,
variables: {
@ -249,7 +269,9 @@ export default class CommentTree extends Vue {
event.comments = oldComments;
} else {
// we have deleted a thread itself
event.comments = oldComments.filter((reply) => reply.id !== deletedCommentId);
event.comments = oldComments.filter(
(reply) => reply.id !== deletedCommentId
);
}
store.writeQuery({
query: COMMENTS_THREADS,
@ -274,14 +296,18 @@ export default class CommentTree extends Vue {
.filter((comment) => comment.inReplyToComment == null)
.sort((a, b) => {
if (a.updatedAt && b.updatedAt) {
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
return (
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
}
return 0;
});
}
get filteredOrderedComments(): IComment[] {
return this.orderedComments.filter((comment) => !comment.deletedAt || comment.totalReplies > 0);
return this.orderedComments.filter(
(comment) => !comment.deletedAt || comment.totalReplies > 0
);
}
get isEventOrganiser(): boolean {

View File

@ -15,7 +15,10 @@
<span v-else class="name comment-link has-text-grey">
{{ $t("[deleted]") }}
</span>
<span class="icons" v-if="!comment.deletedAt && comment.actor.id === currentActor.id">
<span
class="icons"
v-if="!comment.deletedAt && comment.actor.id === currentActor.id"
>
<b-dropdown aria-role="list">
<b-icon slot="trigger" role="button" icon="dots-horizontal" />
@ -44,8 +47,9 @@
<div class="post-infos">
<span :title="comment.insertedAt | formatDateTimeString">
{{
formatDistanceToNow(new Date(comment.updatedAt), { locale: $dateFnsLocale }) ||
$t("Right now")
formatDistanceToNow(new Date(comment.updatedAt), {
locale: $dateFnsLocale,
}) || $t("Right now")
}}</span
>
</div>
@ -77,7 +81,9 @@
type="is-primary"
>{{ $t("Update") }}</b-button
>
<b-button native-type="button" @click="toggleEditMode">{{ $t("Cancel") }}</b-button>
<b-button native-type="button" @click="toggleEditMode">{{
$t("Cancel")
}}</b-button>
</div>
</form>
</div>
@ -95,7 +101,8 @@ import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
currentActor: CURRENT_ACTOR_CLIENT,
},
components: {
editor: () => import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
editor: () =>
import(/* webpackChunkName: "editor" */ "@/components/Editor.vue"),
},
})
export default class DiscussionComment extends Vue {

View File

@ -1,14 +1,23 @@
<template>
<router-link
class="discussion-minimalist-card-wrapper"
:to="{ name: RouteName.DISCUSSION, params: { slug: discussion.slug, id: discussion.id } }"
:to="{
name: RouteName.DISCUSSION,
params: { slug: discussion.slug, id: discussion.id },
}"
>
<div class="media-left">
<figure
class="image is-32x32"
v-if="discussion.lastComment.actor && discussion.lastComment.actor.avatar"
v-if="
discussion.lastComment.actor && discussion.lastComment.actor.avatar
"
>
<img class="is-rounded" :src="discussion.lastComment.actor.avatar.url" alt />
<img
class="is-rounded"
:src="discussion.lastComment.actor.avatar.url"
alt
/>
</figure>
<b-icon v-else size="is-medium" icon="account-circle" />
</div>
@ -17,15 +26,18 @@
<p class="discussion-minimalist-title">{{ discussion.title }}</p>
<span :title="actualDate | formatDateTimeString">
{{
formatDistanceToNowStrict(new Date(actualDate), { locale: $dateFnsLocale }) ||
$t("Right now")
formatDistanceToNowStrict(new Date(actualDate), {
locale: $dateFnsLocale,
}) || $t("Right now")
}}</span
>
</div>
<div class="has-text-grey" v-if="!discussion.lastComment.deletedAt">
{{ htmlTextEllipsis }}
</div>
<div v-else class="has-text-grey">{{ $t("[This comment has been deleted]") }}</div>
<div v-else class="has-text-grey">
{{ $t("[This comment has been deleted]") }}
</div>
</div>
</router-link>
</template>
@ -54,7 +66,10 @@ export default class DiscussionListItem extends Vue {
}
get actualDate(): string | Date | undefined {
if (this.discussion.updatedAt === this.discussion.insertedAt && this.discussion.lastComment) {
if (
this.discussion.updatedAt === this.discussion.insertedAt &&
this.discussion.lastComment
) {
return this.discussion.lastComment.publishedAt;
}
return this.discussion.updatedAt;
@ -83,7 +98,8 @@ export default class DiscussionListItem extends Vue {
.discussion-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica,
Arial, serif;
font-size: 1.25rem;
font-weight: 700;
flex: 1;

View File

@ -117,11 +117,21 @@
<b-icon icon="format-quote-close" />
</button>
<button v-if="!isBasicMode" class="menubar__button" @click="commands.undo" type="button">
<button
v-if="!isBasicMode"
class="menubar__button"
@click="commands.undo"
type="button"
>
<b-icon icon="undo" />
</button>
<button v-if="!isBasicMode" class="menubar__button" @click="commands.redo" type="button">
<button
v-if="!isBasicMode"
class="menubar__button"
@click="commands.redo"
type="button"
>
<b-icon icon="redo" />
</button>
</div>
@ -181,7 +191,9 @@
</div>
</div>
</template>
<div v-else class="suggestion-list__item is-empty">{{ $t("No profiles found") }}</div>
<div v-else class="suggestion-list__item is-empty">
{{ $t("No profiles found") }}
</div>
</div>
</div>
</template>
@ -395,15 +407,7 @@ export default class EditorComponent extends Vue {
new Image(),
new MaxSize({ maxSize: this.maxSize }),
],
onUpdate: ({
getHTML,
transaction,
getJSON,
}: {
getHTML: Function;
getJSON: Function;
transaction: unknown;
}) => {
onUpdate: ({ getHTML }: { getHTML: Function }) => {
this.$emit("input", getHTML());
},
});
@ -438,7 +442,8 @@ export default class EditorComponent extends Vue {
upHandler(): void {
this.navigatedActorIndex =
(this.navigatedActorIndex + this.filteredActors.length - 1) % this.filteredActors.length;
(this.navigatedActorIndex + this.filteredActors.length - 1) %
this.filteredActors.length;
}
/**
@ -446,7 +451,8 @@ export default class EditorComponent extends Vue {
* if it's the last item, navigate to the first one
*/
downHandler(): void {
this.navigatedActorIndex = (this.navigatedActorIndex + 1) % this.filteredActors.length;
this.navigatedActorIndex =
(this.navigatedActorIndex + 1) % this.filteredActors.length;
}
enterHandler(): void {
@ -541,7 +547,10 @@ export default class EditorComponent extends Vue {
},
});
if (data.uploadMedia && data.uploadMedia.url) {
command({ src: data.uploadMedia.url, "data-media-id": data.uploadMedia.id });
command({
src: data.uploadMedia.url,
"data-media-id": data.uploadMedia.id,
});
}
} catch (error) {
console.error(error);

View File

@ -48,9 +48,14 @@ export default class Image extends Node {
}
commands({ type }: { type: NodeType }): any {
return (attrs: { [key: string]: string }) => (state: EditorState, dispatch: DispatchFn) => {
return (attrs: { [key: string]: string }) => (
state: EditorState,
dispatch: DispatchFn
) => {
const { selection }: { selection: TextSelection } = state;
const position = selection.$cursor ? selection.$cursor.pos : selection.$to.pos;
const position = selection.$cursor
? selection.$cursor.pos
: selection.$to.pos;
const node = type.create(attrs);
const transaction = state.tr.insert(position, node);
dispatch(transaction);
@ -75,7 +80,8 @@ export default class Image extends Node {
}
const images = Array.from(realEvent.dataTransfer.files).filter(
(file: any) => /image/i.test(file.type) && !/svg/i.test(file.type)
(file: any) =>
/image/i.test(file.type) && !/svg/i.test(file.type)
);
if (images.length === 0) {
@ -105,7 +111,10 @@ export default class Image extends Node {
src: data.uploadMedia.url,
"data-media-id": data.uploadMedia.id,
});
const transaction = view.state.tr.insert(coordinates.pos, node);
const transaction = view.state.tr.insert(
coordinates.pos,
node
);
view.dispatch(transaction);
});
return true;

View File

@ -1,11 +1,14 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
import { Extension, Plugin } from "tiptap";
export default class MaxSize extends Extension {
// eslint-disable-next-line class-methods-use-this
get name() {
return "maxSize";
}
// eslint-disable-next-line class-methods-use-this
get defaultOptions() {
return {
maxSize: null,
@ -21,7 +24,7 @@ export default class MaxSize extends Extension {
const newLength = newState.doc.content.size;
if (newLength > max && newLength > oldLength) {
let newTr = newState.tr;
const newTr = newState.tr;
newTr.insertText("", max + 1, newLength);
return newTr;

View File

@ -21,9 +21,13 @@
</b-autocomplete>
</b-field>
<b-field v-if="isSecureContext()">
<b-button type="is-text" v-if="!gettingLocation" icon-right="target" @click="locateMe">{{
$t("Use my location")
}}</b-button>
<b-button
type="is-text"
v-if="!gettingLocation"
icon-right="target"
@click="locateMe"
>{{ $t("Use my location") }}</b-button
>
<span v-else>{{ $t("Getting location") }}</span>
</b-field>
<!--
@ -50,7 +54,7 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet";
import { debounce } from "lodash";
import { debounce, DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config";
@ -58,7 +62,8 @@ import { IConfig } from "../../types/config.model";
@Component({
components: {
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
"map-leaflet": () =>
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
},
apollo: {
config: CONFIG,
@ -81,7 +86,7 @@ export default class AddressAutoComplete extends Vue {
private gettingLocation = false;
private location!: Position;
private location!: GeolocationPosition;
private gettingLocationError: any;
@ -89,7 +94,7 @@ export default class AddressAutoComplete extends Vue {
config!: IConfig;
fetchAsyncData!: Function;
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
// We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263
@ -121,7 +126,9 @@ export default class AddressAutoComplete extends Vue {
},
});
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address));
this.addressData = result.data.searchAddress.map(
(address: IAddress) => new Address(address)
);
this.isFetching = false;
}
@ -174,7 +181,9 @@ export default class AddressAutoComplete extends Vue {
},
});
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address));
this.addressData = result.data.reverseGeocode.map(
(address: IAddress) => new Address(address)
);
if (this.addressData.length > 0) {
const defaultAddress = new Address(this.addressData[0]);
this.selected = defaultAddress;
@ -197,7 +206,10 @@ export default class AddressAutoComplete extends Vue {
this.location = await AddressAutoComplete.getLocation();
this.mapDefaultZoom = 12;
this.reverseGeoCode(
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
new LatLng(
this.location.coords.latitude,
this.location.coords.longitude
),
12
);
} catch (e) {
@ -207,7 +219,7 @@ export default class AddressAutoComplete extends Vue {
this.gettingLocation = false;
}
static async getLocation(): Promise<Position> {
static async getLocation(): Promise<GeolocationPosition> {
return new Promise((resolve, reject) => {
if (!("geolocation" in navigator)) {
reject(new Error("Geolocation is not available."));

View File

@ -102,14 +102,20 @@ export default class DateTimePicker extends Vue {
}
get minTime(): Date | null {
if (this.minDatetime && this.datesAreOnSameDay(this.dateWithTime, this.minDatetime)) {
if (
this.minDatetime &&
this.datesAreOnSameDay(this.dateWithTime, this.minDatetime)
) {
return this.minDatetime;
}
return null;
}
get maxTime(): Date | null {
if (this.maxDatetime && this.datesAreOnSameDay(this.dateWithTime, this.maxDatetime)) {
if (
this.maxDatetime &&
this.datesAreOnSameDay(this.dateWithTime, this.maxDatetime)
) {
return this.maxDatetime;
}
return null;

View File

@ -1,5 +1,8 @@
<template>
<router-link class="card" :to="{ name: 'Event', params: { uuid: event.uuid } }">
<router-link
class="card"
:to="{ name: 'Event', params: { uuid: event.uuid } }"
>
<div class="card-image">
<figure
class="image is-16by9"
@ -7,7 +10,16 @@
event.picture ? event.picture.url : '/img/mobilizon_default_card.png'
}')`"
>
<div class="tag-container" v-if="event.tags">
<div
class="tag-container"
v-if="event.tags || event.status !== EventStatus.CONFIRMED"
>
<b-tag type="is-info" v-if="event.status === EventStatus.TENTATIVE">
{{ $t("Tentative") }}
</b-tag>
<b-tag type="is-danger" v-if="event.status === EventStatus.CANCELLED">
{{ $t("Cancelled") }}
</b-tag>
<router-link
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
v-for="tag in event.tags.slice(0, 3)"
@ -21,14 +33,18 @@
<div class="card-content">
<div class="media">
<div class="media-left">
<date-calendar-icon v-if="!mergedOptions.hideDate" :date="event.beginsOn" />
<date-calendar-icon
v-if="!mergedOptions.hideDate"
:date="event.beginsOn"
/>
</div>
<div class="media-content">
<p class="event-title">{{ event.title }}</p>
<div class="event-subtitle" v-if="event.physicalAddress">
<!-- <p>{{ $t('By @{username}', { username: actor.preferredUsername }) }}</p>-->
<span>
{{ event.physicalAddress.description }}, {{ event.physicalAddress.locality }}
{{ event.physicalAddress.description }},
{{ event.physicalAddress.locality }}
</span>
</div>
</div>
@ -77,7 +93,7 @@ import { IEvent, IEventCardOptions } from "@/types/event.model";
import { Component, Prop, Vue } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { Actor, Person } from "@/types/actor";
import { ParticipantRole } from "../../types/participant.model";
import { EventStatus, ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
@Component({
@ -92,6 +108,8 @@ export default class EventCard extends Vue {
ParticipantRole = ParticipantRole;
EventStatus = EventStatus;
RouteName = RouteName;
defaultOptions: IEventCardOptions = {

View File

@ -18,7 +18,9 @@
</docs>
<template>
<span v-if="!endsOn">{{ beginsOn | formatDateTimeString(showStartTime) }}</span>
<span v-if="!endsOn">{{
beginsOn | formatDateTimeString(showStartTime)
}}</span>
<span v-else-if="isSameDay() && showStartTime && showEndTime">
{{
$t("On {date} from {startTime} to {endTime}", {
@ -44,7 +46,9 @@
})
}}
</span>
<span v-else-if="isSameDay()">{{ $t("On {date}", { date: formatDate(beginsOn) }) }}</span>
<span v-else-if="isSameDay()">{{
$t("On {date}", { date: formatDate(beginsOn) })
}}</span>
<span v-else-if="endsOn && showStartTime && showEndTime">
{{
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
@ -97,7 +101,9 @@ export default class EventFullDate extends Vue {
}
isSameDay(): boolean {
const sameDay = new Date(this.beginsOn).toDateString() === new Date(this.endsOn).toDateString();
const sameDay =
new Date(this.beginsOn).toDateString() ===
new Date(this.endsOn).toDateString();
return this.endsOn !== undefined && sameDay;
}
}

View File

@ -3,54 +3,41 @@
<div class="columns">
<div class="content column">
<div class="title-wrapper">
<div class="columns">
<div class="column is-narrow">
<figure
class="image is-16by9"
:style="`background-image: url('${
participation.event.picture
? participation.event.picture.url
: '/img/mobilizon_default_card.png'
}')`"
>
<div class="tag-container" v-if="event.tags">
<router-link
:to="{ name: RouteName.TAG, params: { tag: tag.title } }"
v-for="tag in participation.event.tags.slice(0, 3)"
:key="tag.slug"
>
<b-tag type="is-light">{{ tag.title }}</b-tag>
</router-link>
</div>
</figure>
</div>
<div class="column">
<div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" />
</div>
<router-link
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"
>
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</div>
<div class="date-component">
<date-calendar-icon :date="participation.event.beginsOn" />
</div>
<router-link
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>
<h3 class="title">{{ participation.event.title }}</h3>
</router-link>
</div>
<div class="participation-actor has-text-grey">
<span>
<b-icon icon="earth" v-if="participation.event.visibility === EventVisibility.PUBLIC" />
<b-icon
icon="earth"
v-if="participation.event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-else-if="participation.event.visibility === EventVisibility.UNLISTED"
v-else-if="
participation.event.visibility === EventVisibility.UNLISTED
"
/>
<b-icon
icon="lock"
v-else-if="participation.event.visibility === EventVisibility.PRIVATE"
v-else-if="
participation.event.visibility === EventVisibility.PRIVATE
"
/>
</span>
<span
v-if="
participation.event.physicalAddress && participation.event.physicalAddress.locality
participation.event.physicalAddress &&
participation.event.physicalAddress.locality
"
>{{ participation.event.physicalAddress.locality }} -</span
>
@ -69,7 +56,11 @@
path="Going as {name}"
tag="span"
>
<popover-actor-card slot="name" :actor="participation.actor" :inline="true">
<popover-actor-card
slot="name"
:actor="participation.actor"
:inline="true"
>
{{ participation.actor.displayName() }}
</popover-actor-card>
</i18n>
@ -79,12 +70,15 @@
<span
class="participant-stats"
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<span v-if="participation.event.options.maximumAttendeeCapacity !== 0">
<span
v-if="participation.event.options.maximumAttendeeCapacity !== 0"
>
{{
$tc(
"{available}/{capacity} available places",
@ -94,16 +88,21 @@
available:
participation.event.options.maximumAttendeeCapacity -
participation.event.participantStats.participant,
capacity: participation.event.options.maximumAttendeeCapacity,
capacity:
participation.event.options.maximumAttendeeCapacity,
}
)
}}
</span>
<span v-else>
{{
$tc("{count} participants", participation.event.participantStats.participant, {
count: participation.event.participantStats.participant,
})
$tc(
"{count} participants",
participation.event.participantStats.participant,
{
count: participation.event.participantStats.participant,
}
)
}}
</span>
<span v-if="participation.event.participantStats.notApproved > 0">
@ -133,9 +132,10 @@
<ul>
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<b-button
@ -147,8 +147,8 @@
})
"
icon-left="pencil"
>{{ $t("Edit") }}
</b-button>
>{{ $t("Edit") }}</b-button
>
</li>
<li v-if="participation.role === ParticipantRole.CREATOR">
<b-button
@ -166,19 +166,23 @@
</li>
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
@click="openDeleteEventModalWrapper"
>
<b-button type="is-text" icon-left="delete">{{ $t("Delete") }} </b-button>
<b-button type="is-text" icon-left="delete">{{
$t("Delete")
}}</b-button>
</li>
<li
v-if="
![ParticipantRole.PARTICIPANT, ParticipantRole.NOT_APPROVED].includes(
participation.role
)
![
ParticipantRole.PARTICIPANT,
ParticipantRole.NOT_APPROVED,
].includes(participation.role)
"
>
<b-button
@ -190,17 +194,20 @@
})
"
icon-left="account-multiple-plus"
>{{ $t("Manage participations") }}
</b-button>
>{{ $t("Manage participations") }}</b-button
>
</li>
<li>
<b-button
tag="router-link"
icon-left="view-compact"
type="is-text"
:to="{ name: RouteName.EVENT, params: { uuid: participation.event.uuid } }"
>{{ $t("View event page") }}
</b-button>
:to="{
name: RouteName.EVENT,
params: { uuid: participation.event.uuid },
}"
>{{ $t("View event page") }}</b-button
>
</li>
</ul>
</div>
@ -213,8 +220,9 @@ import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { mixins } from "vue-class-component";
import { RawLocation, Route } from "vue-router";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { EventVisibility, IEventCardOptions } from "../../types/event.model";
import { EventVisibility, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import { IEventCardOptions } from "../../types/event.model";
import { IPerson } from "../../types/actor";
import ActorMixin from "../../mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "../../graphql/actor";
@ -275,8 +283,14 @@ export default class EventListCard extends mixins(ActorMixin, EventMixin) {
await this.openDeleteEventModal(this.participation.event);
}
async gotToWithCheck(participation: IParticipant, route: RawLocation): Promise<Route> {
if (participation.actor.id !== this.currentActor.id && participation.event.organizerActor) {
async gotToWithCheck(
participation: IParticipant,
route: RawLocation
): Promise<Route> {
if (
participation.actor.id !== this.currentActor.id &&
participation.event.organizerActor
) {
const organizer = participation.event.organizerActor as IPerson;
await changeIdentity(this.$apollo.provider.defaultClient, organizer);
this.$buefy.notification.open({
@ -319,7 +333,6 @@ article.box {
line-height: 1.75em;
}
}
div.content {
padding: 5px;
@ -363,7 +376,6 @@ article.box {
.actions {
ul li {
margin: 0 auto;
.is-link {
cursor: pointer;
}

View File

@ -6,7 +6,9 @@
<div class="date-component">
<date-calendar-icon :date="event.beginsOn" />
</div>
<router-link :to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }">
<router-link
:to="{ name: RouteName.EVENT, params: { uuid: event.uuid } }"
>
<h2 class="title">{{ event.title }}</h2>
</router-link>
</div>
@ -15,17 +17,34 @@
{{ event.physicalAddress.locality }}
</span>
<span v-if="event.attributedTo && options.memberofGroup">
{{ $t("Created by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
{{
$t("Created by {name}", {
name: usernameWithDomain(event.organizerActor),
})
}}
</span>
<span v-else-if="options.memberofGroup">
{{ $t("Organized by {name}", { name: usernameWithDomain(event.organizerActor) }) }}
{{
$t("Organized by {name}", {
name: usernameWithDomain(event.organizerActor),
})
}}
</span>
</div>
<div class="columns">
<span class="column is-narrow">
<b-icon icon="earth" v-if="event.visibility === EventVisibility.PUBLIC" />
<b-icon icon="link" v-if="event.visibility === EventVisibility.UNLISTED" />
<b-icon icon="lock" v-if="event.visibility === EventVisibility.PRIVATE" />
<b-icon
icon="earth"
v-if="event.visibility === EventVisibility.PUBLIC"
/>
<b-icon
icon="link"
v-if="event.visibility === EventVisibility.UNLISTED"
/>
<b-icon
icon="lock"
v-if="event.visibility === EventVisibility.PRIVATE"
/>
</span>
<span class="column is-narrow participant-stats">
<span v-if="event.options.maximumAttendeeCapacity !== 0">
@ -38,9 +57,13 @@
</span>
<span v-else>
{{
$tc("{count} participants", event.participantStats.participant, {
count: event.participantStats.participant,
})
$tc(
"{count} participants",
event.participantStats.participant,
{
count: event.participantStats.participant,
}
)
}}
</span>
</span>
@ -51,7 +74,7 @@
</template>
<script lang="ts">
import { EventVisibility, IEventCardOptions, IEvent } from "@/types/event.model";
import { IEventCardOptions, IEvent } from "@/types/event.model";
import { Component, Prop } from "vue-property-decorator";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { IPerson, usernameWithDomain } from "@/types/actor";
@ -59,7 +82,7 @@ import { mixins } from "vue-class-component";
import ActorMixin from "@/mixins/actor";
import { CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import EventMixin from "@/mixins/event";
import { ParticipantRole } from "../../types/participant.model";
import { EventVisibility, ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
const defaultOptions: IEventCardOptions = {

View File

@ -14,10 +14,12 @@
{{
$tc(
"{available}/{capacity} available places",
event.options.maximumAttendeeCapacity - event.participantStats.participant,
event.options.maximumAttendeeCapacity -
event.participantStats.participant,
{
available:
event.options.maximumAttendeeCapacity - event.participantStats.participant,
event.options.maximumAttendeeCapacity -
event.participantStats.participant,
capacity: event.options.maximumAttendeeCapacity,
}
)
@ -42,9 +44,13 @@
"
>
{{
$tc("{count} requests waiting", event.participantStats.notApproved, {
count: event.participantStats.notApproved,
})
$tc(
"{count} requests waiting",
event.participantStats.notApproved,
{
count: event.participantStats.notApproved,
}
)
}}
</b-button>
</span>
@ -56,7 +62,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import { IEvent } from "@/types/event.model";
import DateCalendarIcon from "@/components/Event/DateCalendarIcon.vue";
import { ParticipantRole } from "../../types/participant.model";
import { ParticipantRole } from "@/types/enums";
import RouteName from "../../router/name";
@Component({
@ -88,7 +94,8 @@ export default class EventMinimalistCard extends Vue {
.event-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1.25rem;
font-weight: 700;
}

View File

@ -33,9 +33,12 @@
<div v-else-if="queryText.length >= 3" class="is-enabled">
<span>{{ $t('No results for "{queryText}"') }}</span>
<span>{{
$t("You can try another search term or drag and drop the marker on the map", {
queryText,
})
$t(
"You can try another search term or drag and drop the marker on the map",
{
queryText,
}
)
}}</span>
<!-- <p class="control" @click="openNewAddressModal">-->
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
@ -102,7 +105,7 @@
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { LatLng } from "leaflet";
import { debounce } from "lodash";
import { debounce, DebouncedFunc } from "lodash";
import { Address, IAddress } from "../../types/address.model";
import { ADDRESS, REVERSE_GEOCODE } from "../../graphql/address";
import { CONFIG } from "../../graphql/config";
@ -110,7 +113,8 @@ import { IConfig } from "../../types/config.model";
@Component({
components: {
"map-leaflet": () => import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
"map-leaflet": () =>
import(/* webpackChunkName: "map" */ "@/components/Map.vue"),
},
apollo: {
config: CONFIG,
@ -133,7 +137,7 @@ export default class FullAddressAutoComplete extends Vue {
private gettingLocation = false;
private location!: Position;
private location!: GeolocationPosition;
private gettingLocationError: any;
@ -141,11 +145,11 @@ export default class FullAddressAutoComplete extends Vue {
config!: IConfig;
fetchAsyncData!: Function;
fetchAsyncData!: DebouncedFunc<(query: string) => Promise<void>>;
// We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263
data() {
data(): Record<string, unknown> {
return {
fetchAsyncData: debounce(this.asyncData, 200),
};
@ -173,7 +177,9 @@ export default class FullAddressAutoComplete extends Vue {
},
});
this.addressData = result.data.searchAddress.map((address: IAddress) => new Address(address));
this.addressData = result.data.searchAddress.map(
(address: IAddress) => new Address(address)
);
this.isFetching = false;
}
@ -224,7 +230,9 @@ export default class FullAddressAutoComplete extends Vue {
},
});
this.addressData = result.data.reverseGeocode.map((address: IAddress) => new Address(address));
this.addressData = result.data.reverseGeocode.map(
(address: IAddress) => new Address(address)
);
if (this.addressData.length > 0) {
const defaultAddress = new Address(this.addressData[0]);
this.selected = defaultAddress;
@ -248,7 +256,10 @@ export default class FullAddressAutoComplete extends Vue {
this.location = await FullAddressAutoComplete.getLocation();
this.mapDefaultZoom = 12;
this.reverseGeoCode(
new LatLng(this.location.coords.latitude, this.location.coords.longitude),
new LatLng(
this.location.coords.latitude,
this.location.coords.longitude
),
12
);
} catch (e) {
@ -266,7 +277,7 @@ export default class FullAddressAutoComplete extends Vue {
return window.isSecureContext;
}
static async getLocation(): Promise<Position> {
static async getLocation(): Promise<GeolocationPosition> {
return new Promise((resolve, reject) => {
if (!("geolocation" in navigator)) {
reject(new Error("Geolocation is not available."));

View File

@ -10,9 +10,18 @@
>
<div class="media">
<figure class="image is-48x48" v-if="availableActor.avatar">
<img class="media-left is-rounded" :src="availableActor.avatar.url" alt="" />
<img
class="media-left is-rounded"
:src="availableActor.avatar.url"
alt=""
/>
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>{{ availableActor.name }}</h3>
<small>{{ `@${availableActor.preferredUsername}` }}</small>
@ -23,9 +32,11 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IMember, IPerson, MemberRole, IActor, Actor } from "@/types/actor";
import { IPerson, IActor, Actor } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({
apollo: {
@ -59,16 +70,21 @@ export default class OrganizerPicker extends Vue {
get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) {
return this.groupMemberships.elements.filter((membership: IMember) =>
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
membership.role
)
[
MemberRole.ADMINISTRATOR,
MemberRole.MODERATOR,
MemberRole.CREATOR,
].includes(membership.role)
);
}
return this.groupMemberships.elements;
}
get actualAvailableActors(): IActor[] {
return [this.identity, ...this.actualMemberships.map((member) => member.parent)];
return [
this.identity,
...this.actualMemberships.map((member) => member.parent),
];
}
@Watch("currentActor")

View File

@ -1,7 +1,11 @@
<template>
<div class="organizer-picker">
<!-- If we have a current actor (inline) -->
<div v-if="inline && currentActor.id" class="inline box" @click="isComponentModalActive = true">
<div
v-if="inline && currentActor.id"
class="inline box"
@click="isComponentModalActive = true"
>
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="currentActor.avatar">
@ -15,7 +19,9 @@
</div>
<div class="media-content" v-if="currentActor.name">
<p class="is-4">{{ currentActor.name }}</p>
<p class="is-6 has-text-grey">{{ `@${currentActor.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${currentActor.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${currentActor.preferredUsername}` }}
@ -26,7 +32,11 @@
</div>
</div>
<!-- If we have a current actor -->
<span v-else-if="currentActor.id" class="block" @click="isComponentModalActive = true">
<span
v-else-if="currentActor.id"
class="block"
@click="isComponentModalActive = true"
>
<img
class="image is-48x48"
v-if="currentActor.avatar"
@ -40,13 +50,19 @@
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="identity.avatar">
<img class="image is-rounded" :src="identity.avatar.url" :alt="identity.avatar.alt" />
<img
class="image is-rounded"
:src="identity.avatar.url"
:alt="identity.avatar.alt"
/>
</figure>
<b-icon v-else size="is-large" icon="account-circle" />
</div>
<div class="media-content" v-if="identity.name">
<p class="is-4">{{ identity.name }}</p>
<p class="is-6 has-text-grey">{{ `@${identity.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${identity.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${identity.preferredUsername}` }}
@ -74,7 +90,11 @@
<div class="column">
<div v-if="actorMembersForCurrentActor.length > 0">
<p>{{ $t("Add a contact") }}</p>
<p class="field" v-for="actor in actorMembersForCurrentActor" :key="actor.id">
<p
class="field"
v-for="actor in actorMembersForCurrentActor"
:key="actor.id"
>
<b-checkbox v-model="actualContacts" :native-value="actor.id">
<div class="media">
<div class="media-left">
@ -89,7 +109,9 @@
</div>
<div class="media-content" v-if="actor.name">
<p class="is-4">{{ actor.name }}</p>
<p class="is-6 has-text-grey">{{ `@${actor.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${actor.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${actor.preferredUsername}` }}
@ -115,7 +137,8 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IActor, IGroup, IMember, IPerson } from "../../types/actor";
import { IMember } from "@/types/actor/member.model";
import { IActor, IGroup, IPerson } from "../../types/actor";
import OrganizerPicker from "./OrganizerPicker.vue";
import { PERSON_MEMBERSHIPS_WITH_MEMBERS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate";
@ -150,7 +173,8 @@ export default class OrganizerPickerWrapper extends Vue {
groupMemberships: Paginate<IMember> = { elements: [], total: 0 };
@Prop({ type: Array, required: false, default: () => [] }) contacts!: IActor[];
@Prop({ type: Array, required: false, default: () => [] })
contacts!: IActor[];
actualContacts: (string | undefined)[] = this.contacts.map(({ id }) => id);
@ -171,7 +195,9 @@ export default class OrganizerPickerWrapper extends Vue {
pickActor(): void {
this.$emit(
"update:contacts",
this.actorMembersForCurrentActor.filter(({ id }) => this.actualContacts.includes(id))
this.actorMembersForCurrentActor.filter(({ id }) =>
this.actualContacts.includes(id)
)
);
this.$emit("input", this.currentActor);
this.isComponentModalActive = false;
@ -182,7 +208,9 @@ export default class OrganizerPickerWrapper extends Vue {
({ parent: { id } }) => id === this.currentActor.id
);
if (currentMembership) {
return currentMembership.parent.members.elements.map(({ actor }) => actor);
return currentMembership.parent.members.elements.map(
({ actor }: { actor: IActor }) => actor
);
}
return [];
}

View File

@ -47,8 +47,16 @@ A button to set your participation
>
</b-dropdown>
<div v-else-if="participation && participation.role === ParticipantRole.NOT_APPROVED">
<b-dropdown aria-role="list" position="is-bottom-left" class="dropdown-disabled">
<div
v-else-if="
participation && participation.role === ParticipantRole.NOT_APPROVED
"
>
<b-dropdown
aria-role="list"
position="is-bottom-left"
class="dropdown-disabled"
>
<button class="button is-success is-large" type="button" slot="trigger">
<b-icon icon="timer-sand-empty" />
<template>
@ -74,9 +82,17 @@ A button to set your participation
<small>{{ $t("Waiting for organization team approval.") }}</small>
</div>
<div v-else-if="participation && participation.role === ParticipantRole.REJECTED">
<div
v-else-if="
participation && participation.role === ParticipantRole.REJECTED
"
>
<span>
{{ $t("Unfortunately, your participation request was rejected by the organizers.") }}
{{
$t(
"Unfortunately, your participation request was rejected by the organizers."
)
}}
</span>
</div>
@ -92,7 +108,11 @@ A button to set your participation
<b-icon icon="menu-down" />
</button>
<b-dropdown-item :value="true" aria-role="listitem" @click="joinEvent(currentActor)">
<b-dropdown-item
:value="true"
aria-role="listitem"
@click="joinEvent(currentActor)"
>
<div class="media">
<div class="media-left">
<figure class="image is-32x32" v-if="currentActor.avatar">
@ -103,7 +123,8 @@ A button to set your participation
<span>
{{
$t("as {identity}", {
identity: currentActor.name || `@${currentActor.preferredUsername}`,
identity:
currentActor.name || `@${currentActor.preferredUsername}`,
})
}}
</span>
@ -121,7 +142,10 @@ A button to set your participation
</b-dropdown>
<b-button
tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT, params: { uuid: event.uuid } }"
:to="{
name: RouteName.EVENT_PARTICIPATE_LOGGED_OUT,
params: { uuid: event.uuid },
}"
v-else-if="!participation && hasAnonymousParticipationMethods"
type="is-primary"
size="is-large"
@ -130,7 +154,10 @@ A button to set your participation
>
<b-button
tag="router-link"
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT, params: { uuid: event.uuid } }"
:to="{
name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
params: { uuid: event.uuid },
}"
v-else-if="!currentActor.id"
type="is-primary"
size="is-large"
@ -142,8 +169,9 @@ A button to set your participation
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { EventJoinOptions, IEvent } from "../../types/event.model";
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import { IEvent } from "../../types/event.model";
import { IPerson, Person } from "../../types/actor";
import { CURRENT_ACTOR_CLIENT, IDENTITIES } from "../../graphql/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user";
@ -161,7 +189,9 @@ import RouteName from "../../router/name";
identities: {
query: IDENTITIES,
update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
identities
? identities.map((identity: IPerson) => new Person(identity))
: [],
skip() {
return this.currentUser.isLoggedIn === false;
},

View File

@ -59,7 +59,12 @@
<a :href="linkedInShareUrl" target="_blank" rel="nofollow noopener"
><b-icon icon="linkedin" size="is-large" type="is-primary"
/></a>
<a :href="diasporaShareUrl" class="diaspora" target="_blank" rel="nofollow noopener">
<a
:href="diasporaShareUrl"
class="diaspora"
target="_blank"
rel="nofollow noopener"
>
<span data-v-5e15e80a="" class="icon has-text-primary is-large">
<DiasporaLogo alt="diaspora-logo" />
</span>
@ -76,7 +81,9 @@
<script lang="ts">
import { Component, Prop, Vue, Ref } from "vue-property-decorator";
import { IEvent, EventVisibility, EventStatus } from "../../types/event.model";
import { EventStatus, EventVisibility } from "@/types/enums";
import { IEvent } from "../../types/event.model";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
@ -88,7 +95,8 @@ import DiasporaLogo from "../../assets/diaspora-icon.svg?inline";
export default class ShareEventModal extends Vue {
@Prop({ type: Object, required: true }) event!: IEvent;
@Prop({ type: Boolean, required: false, default: true }) eventCapacityOK!: boolean;
@Prop({ type: Boolean, required: false, default: true })
eventCapacityOK!: boolean;
@Ref("eventURLInput") readonly eventURLInput!: any;
@ -99,13 +107,15 @@ export default class ShareEventModal extends Vue {
showCopiedTooltip = false;
get twitterShareUrl(): string {
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(this.event.url)}&text=${
this.event.title
}`;
return `https://twitter.com/intent/tweet?url=${encodeURIComponent(
this.event.url
)}&text=${this.event.title}`;
}
get facebookShareUrl(): string {
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(this.event.url)}`;
return `https://www.facebook.com/sharer/sharer.php?u=${encodeURIComponent(
this.event.url
)}`;
}
get linkedInShareUrl(): string {
@ -124,7 +134,7 @@ export default class ShareEventModal extends Vue {
)}&url=${encodeURIComponent(this.event.url)}`;
}
copyURL() {
copyURL(): void {
this.eventURLInput.$refs.input.select();
document.execCommand("copy");
this.showCopiedTooltip = true;

View File

@ -4,7 +4,9 @@
{{ $t("Add some tags") }}
<b-tooltip
type="is-dark"
:label="$t('You can add tags by hitting the Enter key or by adding a comma')"
:label="
$t('You can add tags by hitting the Enter key or by adding a comma')
"
>
<b-icon size="is-small" icon="help-circle-outline"></b-icon>
</b-tooltip>
@ -40,7 +42,6 @@ import { ITag } from "../../types/tag.model";
if (typeof tag !== "string") {
return tag;
}
// @ts-ignore
return { title: tag, slug: tag } as ITag;
});
this.$emit("input", tagEntities);
@ -57,14 +58,14 @@ export default class TagInput extends Vue {
filteredTags: ITag[] = [];
getFilteredTags(text: string) {
getFilteredTags(text: string): void {
this.filteredTags = differenceBy(this.data, this.value, "id").filter(
(option) => get(option, this.path).toString().toLowerCase().indexOf(text.toLowerCase()) >= 0
(option) =>
get(option, this.path)
.toString()
.toLowerCase()
.indexOf(text.toLowerCase()) >= 0
);
}
static isTag(x: any): x is ITag {
return x.slug !== undefined;
}
}
</script>

View File

@ -3,13 +3,31 @@
<img :src="`/img/pics/footer_${random}.jpg`" alt="" />
<ul>
<li>
<router-link :to="{ name: RouteName.ABOUT }">{{ $t("About") }}</router-link>
<b-select
v-if="$i18n"
v-model="locale"
:placeholder="$t('Select a language')"
>
<option v-for="(language, lang) in langs" :value="lang" :key="lang">
{{ language }}
</option>
</b-select>
</li>
<li>
<router-link :to="{ name: RouteName.TERMS }">{{ $t("Terms") }}</router-link>
<router-link :to="{ name: RouteName.ABOUT }">{{
$t("About")
}}</router-link>
</li>
<li>
<a hreflang="en" href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE">
<router-link :to="{ name: RouteName.TERMS }">{{
$t("Terms")
}}</router-link>
</li>
<li>
<a
hreflang="en"
href="https://framagit.org/framasoft/mobilizon/blob/master/LICENSE"
>
{{ $t("License") }}
</a>
</li>
@ -19,7 +37,9 @@
tag="span"
path="Powered by {mobilizon}. © 2018 - {date} The Mobilizon Contributors - Made with the financial support of {contributors}."
>
<a slot="mobilizon" href="https://joinmobilizon.org">{{ $t("Mobilizon") }}</a>
<a slot="mobilizon" href="https://joinmobilizon.org">{{
$t("Mobilizon")
}}</a>
<span slot="date">{{ new Date().getFullYear() }}</span>
<a href="https://joinmobilizon.org/hall-of-fame" slot="contributors">{{
$t("more than 1360 contributors")
@ -29,17 +49,40 @@
</footer>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import { Component, Vue, Watch } from "vue-property-decorator";
import { saveLocaleData } from "@/utils/auth";
import { loadLanguageAsync } from "@/utils/i18n";
import RouteName from "../router/name";
import langs from "../i18n/langs.json";
@Component
export default class Footer extends Vue {
RouteName = RouteName;
locale: string | null = this.$i18n.locale;
langs: Record<string, string> = langs;
// eslint-disable-next-line class-methods-use-this
get random(): number {
return Math.floor(Math.random() * 4) + 1;
}
@Watch("locale")
// eslint-disable-next-line class-methods-use-this
async updateLocale(locale: string): Promise<void> {
if (locale) {
await loadLanguageAsync(locale);
saveLocaleData(locale);
}
}
@Watch("$i18n.locale", { deep: true })
updateLocaleFromI18n(locale: string): void {
if (locale) {
this.locale = locale;
}
}
}
</script>
<style lang="scss" scoped>
@ -83,5 +126,10 @@ footer.footer {
text-decoration: underline;
text-decoration-color: $secondary;
}
::v-deep span.select select {
background: $background-color;
color: $white;
}
}
</style>

View File

@ -17,7 +17,9 @@
>
<h3>{{ group.name }}</h3>
<p class="is-6 has-text-grey">
<span v-if="group.domain">{{ `@${group.preferredUsername}@${group.domain}` }}</span>
<span v-if="group.domain">{{
`@${group.preferredUsername}@${group.domain}`
}}</span>
<span v-else>{{ `@${group.preferredUsername}` }}</span>
</p>
</router-link>

View File

@ -13,7 +13,9 @@
<router-link
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) },
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
@ -23,12 +25,16 @@
}}</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<b-taglist>
<b-tag type="is-info" v-if="member.role === MemberRole.ADMINISTRATOR">{{
$t("Administrator")
}}</b-tag>
<b-tag type="is-info" v-else-if="member.role === MemberRole.MODERATOR">{{
$t("Moderator")
}}</b-tag>
<b-tag
type="is-info"
v-if="member.role === MemberRole.ADMINISTRATOR"
>{{ $t("Administrator") }}</b-tag
>
<b-tag
type="is-info"
v-else-if="member.role === MemberRole.MODERATOR"
>{{ $t("Moderator") }}</b-tag
>
</b-taglist>
</p>
</router-link>
@ -54,7 +60,9 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IMember, MemberRole, usernameWithDomain } from "@/types/actor";
import { usernameWithDomain } from "@/types/actor";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
import RouteName from "../../router/name";
@Component

View File

@ -8,7 +8,9 @@
<a
class="list-item"
v-for="groupMembership in actualMemberships"
:class="{ 'is-active': groupMembership.parent.id === currentGroup.id }"
:class="{
'is-active': groupMembership.parent.id === currentGroup.id,
}"
@click="changeCurrentGroup(groupMembership.parent)"
:key="groupMembership.id"
>
@ -19,14 +21,25 @@
:src="groupMembership.parent.avatar.url"
alt=""
/>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
<div class="media-content">
<h3>@{{ groupMembership.parent.name }}</h3>
<small>{{ `@${groupMembership.parent.preferredUsername}` }}</small>
<small>{{
`@${groupMembership.parent.preferredUsername}`
}}</small>
</div>
</div>
</a>
<a class="list-item" @click="changeCurrentGroup(new Group())" v-if="currentGroup.id">
<a
class="list-item"
@click="changeCurrentGroup(new Group())"
v-if="currentGroup.id"
>
<h3>{{ $t("Unset group") }}</h3>
</a>
</div>
@ -36,9 +49,11 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IGroup, IMember, IPerson, Group, MemberRole } from "@/types/actor";
import { IGroup, IPerson, Group } from "@/types/actor";
import { PERSON_MEMBERSHIPS } from "@/graphql/actor";
import { Paginate } from "@/types/paginate";
import { IMember } from "@/types/actor/member.model";
import { MemberRole } from "@/types/enums";
@Component({
apollo: {
@ -77,9 +92,11 @@ export default class GroupPicker extends Vue {
get actualMemberships(): IMember[] {
if (this.restrictModeratorLevel) {
return this.groupMemberships.elements.filter((membership: IMember) =>
[MemberRole.ADMINISTRATOR, MemberRole.MODERATOR, MemberRole.CREATOR].includes(
membership.role
)
[
MemberRole.ADMINISTRATOR,
MemberRole.MODERATOR,
MemberRole.CREATOR,
].includes(membership.role)
);
}
return this.groupMemberships.elements;

View File

@ -10,7 +10,11 @@
{{ $t("The event will show the group as organizer.") }}
</p>
</div>
<div v-if="inline && currentGroup.id" class="inline box" @click="isComponentModalActive = true">
<div
v-if="inline && currentGroup.id"
class="inline box"
@click="isComponentModalActive = true"
>
<div class="media">
<div class="media-left">
<figure class="image is-48x48" v-if="currentGroup.avatar">
@ -24,7 +28,9 @@
</div>
<div class="media-content" v-if="currentGroup.name">
<p class="is-4">{{ currentGroup.name }}</p>
<p class="is-6 has-text-grey">{{ `@${currentGroup.preferredUsername}` }}</p>
<p class="is-6 has-text-grey">
{{ `@${currentGroup.preferredUsername}` }}
</p>
</div>
<div class="media-content" v-else>
{{ `@${currentGroup.preferredUsername}` }}
@ -34,7 +40,11 @@
</b-button>
</div>
</div>
<span v-else-if="currentGroup.id" class="block" @click="isComponentModalActive = true">
<span
v-else-if="currentGroup.id"
class="block"
@click="isComponentModalActive = true"
>
<img
class="image is-48x48"
v-if="currentGroup.avatar"
@ -44,7 +54,9 @@
<b-icon v-else size="is-large" icon="account-circle" />
</span>
<div v-if="groupMemberships.total === 0" class="box">
<p class="is-4">{{ $t("This identity is not a member of any group.") }}</p>
<p class="is-4">
{{ $t("This identity is not a member of any group.") }}
</p>
<p class="is-6 is-size-6 has-text-grey">
{{ $t("You need to create the group before you create an event.") }}
</p>
@ -61,7 +73,8 @@
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
import { IGroup, IMember, IPerson } from "../../types/actor";
import { IMember } from "@/types/actor/member.model";
import { IGroup, IPerson } from "../../types/actor";
import GroupPicker from "./GroupPicker.vue";
import { PERSON_MEMBERSHIPS } from "../../graphql/actor";
import { Paginate } from "../../types/paginate";

View File

@ -26,7 +26,8 @@ export default class GroupSection extends Vue {
@Prop({ required: true, type: String }) icon!: string;
@Prop({ required: false, type: Boolean, default: true }) privateSection!: boolean;
@Prop({ required: false, type: Boolean, default: true })
privateSection!: boolean;
@Prop({ required: true, type: Object }) route!: Route;
}
@ -76,7 +77,8 @@ div.group-section-title {
::v-deep span {
display: inline;
padding: 3px 8px;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-weight: 500;
font-size: 30px;
flex: 1;

View File

@ -2,7 +2,10 @@
<div class="media">
<div class="media-content">
<div class="content">
<i18n tag="p" path="You have been invited by {invitedBy} to the following group:">
<i18n
tag="p"
path="You have been invited by {invitedBy} to the following group:"
>
<b slot="invitedBy">{{ member.invitedBy.name }}</b>
</i18n>
</div>
@ -20,15 +23,21 @@
<router-link
:to="{
name: RouteName.GROUP,
params: { preferredUsername: usernameWithDomain(member.parent) },
params: {
preferredUsername: usernameWithDomain(member.parent),
},
}"
>
<h3>{{ member.parent.name }}</h3>
<p class="is-6 has-text-grey">
<span v-if="member.parent.domain">
{{ `@${member.parent.preferredUsername}@${member.parent.domain}` }}
{{
`@${member.parent.preferredUsername}@${member.parent.domain}`
}}
</span>
<span v-else>{{ `@${member.parent.preferredUsername}` }}</span>
<span v-else>{{
`@${member.parent.preferredUsername}`
}}</span>
</p>
</router-link>
</div>
@ -54,7 +63,8 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IMember, usernameWithDomain } from "@/types/actor";
import { usernameWithDomain } from "@/types/actor";
import { IMember } from "@/types/actor/member.model";
import RouteName from "../../router/name";
@Component

View File

@ -11,10 +11,10 @@
</template>
<script lang="ts">
import { ACCEPT_INVITATION, REJECT_INVITATION } from "@/graphql/member";
import { IMember } from "@/types/actor";
import { Component, Prop, Vue } from "vue-property-decorator";
import InvitationCard from "@/components/Group/InvitationCard.vue";
import { LOGGED_USER_MEMBERSHIPS } from "@/graphql/actor";
import { IMember } from "@/types/actor/member.model";
@Component({
components: {
@ -26,13 +26,15 @@ export default class Invitations extends Vue {
async acceptInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>({
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
});
const { data } = await this.$apollo.mutate<{ acceptInvitation: IMember }>(
{
mutation: ACCEPT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("accept-invitation", data.acceptInvitation);
}
@ -46,13 +48,15 @@ export default class Invitations extends Vue {
async rejectInvitation(id: string): Promise<void> {
try {
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>({
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
});
const { data } = await this.$apollo.mutate<{ rejectInvitation: IMember }>(
{
mutation: REJECT_INVITATION,
variables: {
id,
},
refetchQueries: [{ query: LOGGED_USER_MEMBERSHIPS }],
}
);
if (data) {
this.$emit("reject-invitation", data.rejectInvitation);
}

View File

@ -25,6 +25,8 @@ export default class JoinGroupWithAccount extends Vue {
}`;
}
sentence = this.$t("We will redirect you to your instance in order to interact with this group");
sentence = this.$t(
"We will redirect you to your instance in order to interact with this group"
);
}
</script>

View File

@ -1,12 +1,12 @@
<template>
<img src="/img/pics/logo_chapril_mobilizon.png" alt="logo chapril" />
<MobilizonLogo />
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import MobilizonLogo from "../assets/logo_chapril_mobilizon.png";
import MobilizonLogo from "../assets/mobilizon_logo.svg?inline";
@Component({
components: {

View File

@ -8,7 +8,11 @@
@click="clickMap"
@update:zoom="updateZoom"
>
<l-tile-layer :url="config.maps.tiles.endpoint" :attribution="attribution"> </l-tile-layer>
<l-tile-layer
:url="config.maps.tiles.endpoint"
:attribution="attribution"
>
</l-tile-layer>
<v-locatecontrol :options="{ icon: 'mdi mdi-map-marker' }" />
<l-marker
:lat-lng="[lat, lon]"
@ -17,7 +21,9 @@
:draggable="!readOnly"
>
<l-popup v-if="popupMultiLine">
<span v-for="line in popupMultiLine" :key="line">{{ line }}<br /></span>
<span v-for="line in popupMultiLine" :key="line"
>{{ line }}<br
/></span>
</l-popup>
</l-marker>
</l-map>
@ -51,12 +57,15 @@ export default class Map extends Vue {
@Prop({ type: String, required: true }) coords!: string;
@Prop({ type: Object, required: false }) marker!: { text: string | string[]; icon: string };
@Prop({ type: Object, required: false }) marker!: {
text: string | string[];
icon: string;
};
@Prop({ type: Object, required: false }) options!: object;
@Prop({ type: Object, required: false }) options!: Record<string, unknown>;
@Prop({ type: Function, required: false })
updateDraggableMarkerCallback!: Function;
updateDraggableMarkerCallback!: (latlng: LatLng, zoom: number) => void;
defaultOptions: {
zoom: number;
@ -86,45 +95,48 @@ export default class Map extends Vue {
}
/* eslint-enable */
openPopup(event: LeafletEvent) {
openPopup(event: LeafletEvent): void {
this.$nextTick(() => {
event.target.openPopup();
});
}
get mergedOptions(): object {
get mergedOptions(): Record<string, unknown> {
return { ...this.defaultOptions, ...this.options };
}
get lat() {
get lat(): number {
return this.$props.coords.split(";")[1];
}
get lon() {
get lon(): number {
return this.$props.coords.split(";")[0];
}
get popupMultiLine() {
get popupMultiLine(): Array<string> {
if (Array.isArray(this.marker.text)) {
return this.marker.text;
}
return [this.marker.text];
}
clickMap(event: LeafletMouseEvent) {
clickMap(event: LeafletMouseEvent): void {
this.updateDraggableMarkerPosition(event.latlng);
}
updateDraggableMarkerPosition(e: LatLng) {
updateDraggableMarkerPosition(e: LatLng): void {
this.updateDraggableMarkerCallback(e, this.zoom);
}
updateZoom(zoom: number) {
updateZoom(zoom: number): void {
this.zoom = zoom;
}
get attribution() {
return this.config.maps.tiles.attribution || this.$t("© The OpenStreetMap Contributors");
get attribution(): string {
return (
this.config.maps.tiles.attribution ||
(this.$t("© The OpenStreetMap Contributors") as string)
);
}
}
</script>

View File

@ -17,13 +17,16 @@ import { Component, Prop, Vue } from "vue-property-decorator";
@Component({
beforeDestroy() {
// eslint-disable-next-line @typescript-eslint/ban-ts-ignore
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.parentContainer.removeLayer(this);
},
})
export default class Vue2LeafletLocateControl extends Vue {
@Prop({ type: Object, default: () => ({}) }) options!: object;
@Prop({ type: Object, default: () => ({}) }) options!: Record<
string,
unknown
>;
@Prop({ type: Boolean, default: true }) visible = true;
@ -33,7 +36,7 @@ export default class Vue2LeafletLocateControl extends Vue {
parentContainer: any;
mounted() {
mounted(): void {
this.mapObject = L.control.locate(this.options);
DomEvent.on(this.mapObject, this.$listeners as any);
propsBinder(this, this.mapObject, this.$props);
@ -42,7 +45,7 @@ export default class Vue2LeafletLocateControl extends Vue {
this.mapObject.addTo(this.parentContainer.mapObject, !this.visible);
}
public locate() {
public locate(): void {
this.mapObject.start();
}
}

View File

@ -1,7 +1,15 @@
<template>
<b-navbar type="is-secondary" wrapper-class="container" :active.sync="mobileNavbarActive">
<b-navbar
type="is-secondary"
wrapper-class="container"
:active.sync="mobileNavbarActive"
>
<template slot="brand">
<b-navbar-item tag="router-link" :to="{ name: RouteName.HOME }" :aria-label="$t('Home')">
<b-navbar-item
tag="router-link"
:to="{ name: RouteName.HOME }"
:aria-label="$t('Home')"
>
<logo />
</b-navbar-item>
</template>
@ -19,9 +27,12 @@
>{{ $t("My groups") }}</b-navbar-item
>
<b-navbar-item tag="span" v-if="config && config.features.eventCreation">
<b-button tag="router-link" :to="{ name: RouteName.CREATE_EVENT }" type="is-primary">{{
$t("Create")
}}</b-button>
<b-button
tag="router-link"
:to="{ name: RouteName.CREATE_EVENT }"
type="is-primary"
>{{ $t("Create") }}</b-button
>
</b-navbar-item>
</template>
<template slot="end">
@ -30,9 +41,17 @@
</b-navbar-item>
<b-navbar-dropdown v-if="currentActor.id && currentUser.isLoggedIn" right>
<template slot="label" v-if="currentActor" class="navbar-dropdown-profile">
<template
slot="label"
v-if="currentActor"
class="navbar-dropdown-profile"
>
<figure class="image is-32x32" v-if="currentActor.avatar">
<img class="is-rounded" alt="avatarUrl" :src="currentActor.avatar.url" />
<img
class="is-rounded"
alt="avatarUrl"
:src="currentActor.avatar.url"
/>
</figure>
<b-icon v-else icon="account-circle" />
</template>
@ -65,9 +84,11 @@
<hr class="navbar-divider" />
</b-navbar-item>
<b-navbar-item tag="router-link" :to="{ name: RouteName.UPDATE_IDENTITY }">{{
$t("My account")
}}</b-navbar-item>
<b-navbar-item
tag="router-link"
:to="{ name: RouteName.UPDATE_IDENTITY }"
>{{ $t("My account") }}</b-navbar-item
>
<!-- <b-navbar-item tag="router-link" :to="{ name: RouteName.CREATE_GROUP }">-->
<!-- {{ $t('Create group') }}-->
@ -95,9 +116,11 @@
<strong>{{ $t("Sign up") }}</strong>
</router-link>
<router-link class="button is-light" :to="{ name: RouteName.LOGIN }">{{
$t("Log in")
}}</router-link>
<router-link
class="button is-light"
:to="{ name: RouteName.LOGIN }"
>{{ $t("Log in") }}</router-link
>
</div>
</b-navbar-item>
</template>
@ -109,13 +132,18 @@ import { Component, Vue, Watch } from "vue-property-decorator";
import Logo from "@/components/Logo.vue";
import { GraphQLError } from "graphql";
import { loadLanguageAsync } from "@/utils/i18n";
import { ICurrentUserRole } from "@/types/enums";
import { CURRENT_USER_CLIENT, USER_SETTINGS } from "../graphql/user";
import { changeIdentity, logout } from "../utils/auth";
import { CURRENT_ACTOR_CLIENT, IDENTITIES, UPDATE_DEFAULT_ACTOR } from "../graphql/actor";
import {
CURRENT_ACTOR_CLIENT,
IDENTITIES,
UPDATE_DEFAULT_ACTOR,
} from "../graphql/actor";
import { IPerson, Person } from "../types/actor";
import { CONFIG } from "../graphql/config";
import { IConfig } from "../types/config.model";
import { ICurrentUser, ICurrentUserRole, IUser } from "../types/current-user.model";
import { ICurrentUser, IUser } from "../types/current-user.model";
import SearchField from "./SearchField.vue";
import RouteName from "../router/name";
@ -130,7 +158,9 @@ import RouteName from "../router/name";
identities: {
query: IDENTITIES,
update: ({ identities }) =>
identities ? identities.map((identity: IPerson) => new Person(identity)) : [],
identities
? identities.map((identity: IPerson) => new Person(identity))
: [],
skip() {
return this.currentUser.isLoggedIn === false;
},
@ -201,7 +231,8 @@ export default class NavBar extends Vue {
async handleErrors(errors: GraphQLError[]): Promise<void> {
if (
errors.length > 0 &&
errors[0].message === "You need to be logged-in to view your list of identities"
errors[0].message ===
"You need to be logged-in to view your list of identities"
) {
await this.logout();
}

View File

@ -1,9 +1,14 @@
<template>
<section class="section container">
<h1 class="title" v-if="loading">{{ $t("Your participation request is being validated") }}</h1>
<h1 class="title" v-if="loading">
{{ $t("Your participation request is being validated") }}
</h1>
<div v-else>
<div v-if="failed">
<b-message :title="$t('Error while validating participation request')" type="is-danger">
<b-message
:title="$t('Error while validating participation request')"
type="is-danger"
>
{{
$t(
"Either the participation request has already been validated, either the validation token is incorrect."
@ -12,9 +17,16 @@
</b-message>
</div>
<div v-else>
<h1 class="title">{{ $t("Your participation request has been validated") }}</h1>
<p class="content" v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED">
{{ $t("Your participation still has to be approved by the organisers.") }}
<h1 class="title">
{{ $t("Your participation request has been validated") }}
</h1>
<p
class="content"
v-if="participation.event.joinOptions == EventJoinOptions.RESTRICTED"
>
{{
$t("Your participation still has to be approved by the organisers.")
}}
</p>
<div class="columns has-text-centered">
<div class="column">
@ -38,9 +50,9 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { confirmLocalAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
import { EventJoinOptions } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
import RouteName from "../../router/name";
import { EventJoinOptions } from "../../types/event.model";
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
@Component

View File

@ -1,5 +1,9 @@
<template>
<redirect-with-account :uri="uri" :pathAfterLogin="`/events/${uuid}`" :sentence="sentence" />
<redirect-with-account
:uri="uri"
:pathAfterLogin="`/events/${uuid}`"
:sentence="sentence"
/>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@ -21,6 +25,8 @@ export default class ParticipationWithAccount extends Vue {
}`;
}
sentence = this.$t("We will redirect you to your instance in order to interact with this event");
sentence = this.$t(
"We will redirect you to your instance in order to interact with this event"
);
}
</script>

View File

@ -33,7 +33,13 @@
)
}}
</p>
<p v-else>{{ $t("If you want, you may send a message to the event organizer here.") }}</p>
<p v-else>
{{
$t(
"If you want, you may send a message to the event organizer here."
)
}}
</p>
<b-field :label="$t('Message')">
<b-input
type="textarea"
@ -54,18 +60,29 @@
</p>
</b-checkbox>
</b-field>
<b-button :disabled="sendingForm" type="is-primary" native-type="submit">{{
$t("Send email")
}}</b-button>
<b-button
:disabled="sendingForm"
type="is-primary"
native-type="submit"
>{{ $t("Send email") }}</b-button
>
<div class="has-text-centered">
<b-button native-type="button" tag="a" type="is-text" @click="$router.go(-1)">{{
$t("Back to previous page")
}}</b-button>
<b-button
native-type="button"
tag="a"
type="is-text"
@click="$router.go(-1)"
>{{ $t("Back to previous page") }}</b-button
>
</div>
</form>
<div v-else>
<h1 class="title">{{ $t("Request for participation confirmation sent") }}</h1>
<p class="content">{{ $t("Check your inbox (and your junk mail folder).") }}</p>
<h1 class="title">
{{ $t("Request for participation confirmation sent") }}
</h1>
<p class="content">
{{ $t("Check your inbox (and your junk mail folder).") }}
</p>
<p class="content">{{ $t("You may now close this window.") }}</p>
</div>
</div>
@ -74,12 +91,13 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { EventModel, IEvent, EventJoinOptions } from "@/types/event.model";
import { EventModel, IEvent } from "@/types/event.model";
import { FETCH_EVENT, JOIN_EVENT } from "@/graphql/event";
import { IConfig } from "@/types/config.model";
import { CONFIG } from "@/graphql/config";
import { addLocalUnconfirmedAnonymousParticipation } from "@/services/AnonymousParticipationStorage";
import { IParticipant, ParticipantRole } from "../../types/participant.model";
import { EventJoinOptions, ParticipantRole } from "@/types/enums";
import { IParticipant } from "../../types/participant.model";
@Component({
apollo: {
@ -101,7 +119,11 @@ import { IParticipant, ParticipantRole } from "../../types/participant.model";
export default class ParticipationWithoutAccount extends Vue {
@Prop({ type: String, required: true }) uuid!: string;
anonymousParticipation: { email: string; message: string; saveParticipation: boolean } = {
anonymousParticipation: {
email: string;
message: string;
saveParticipation: boolean;
} = {
email: "",
message: "",
saveParticipation: true,
@ -133,7 +155,9 @@ export default class ParticipationWithoutAccount extends Vue {
},
update: (store, { data: updateData }) => {
if (updateData == null) {
console.error("Cannot update event participant cache, because of data null value.");
console.error(
"Cannot update event participant cache, because of data null value."
);
return;
}
@ -142,12 +166,16 @@ export default class ParticipationWithoutAccount extends Vue {
variables: { uuid: this.event.uuid },
});
if (cachedData == null) {
console.error("Cannot update event participant cache, because of cached null value.");
console.error(
"Cannot update event participant cache, because of cached null value."
);
return;
}
const { event } = cachedData;
if (event === null) {
console.error("Cannot update event participant cache, because of null value.");
console.error(
"Cannot update event participant cache, because of null value."
);
return;
}

View File

@ -2,22 +2,34 @@
<section class="section container hero">
<div class="hero-body" v-if="event">
<div class="container">
<subtitle>{{ $t("You wish to participate to the following event") }}</subtitle>
<subtitle>{{
$t("You wish to participate to the following event")
}}</subtitle>
<EventListViewCard v-if="event" :event="event" />
<div class="columns has-text-centered">
<div class="column">
<router-link :to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }">
<router-link
:to="{ name: RouteName.EVENT_PARTICIPATE_WITH_ACCOUNT }"
>
<figure class="image is-128x128">
<img src="../../assets/undraw_profile.svg" alt="Profile illustration" />
<img
src="../../assets/undraw_profile.svg"
alt="Profile illustration"
/>
</figure>
<b-button type="is-primary">{{ $t("I have a Mobilizon account") }}</b-button>
<b-button type="is-primary">{{
$t("I have a Mobilizon account")
}}</b-button>
</router-link>
<p>
<small>
{{
$t("Either on the {instance} instance or on another instance.", {
instance: host,
})
$t(
"Either on the {instance} instance or on another instance.",
{
instance: host,
}
)
}}
</small>
<b-tooltip
@ -32,25 +44,41 @@
</b-tooltip>
</p>
</div>
<vertical-divider :content="$t('Or')" v-if="anonymousParticipationAllowed" />
<vertical-divider
:content="$t('Or')"
v-if="anonymousParticipationAllowed"
/>
<div
class="column"
v-if="anonymousParticipationAllowed && hasAnonymousEmailParticipationMethod"
v-if="
anonymousParticipationAllowed &&
hasAnonymousEmailParticipationMethod
"
>
<router-link
:to="{ name: RouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT }"
v-if="event.local"
>
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
<img
src="../../assets/undraw_mail_2.svg"
alt="Privacy illustration"
/>
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
<b-button type="is-primary">{{
$t("I don't have a Mobilizon account")
}}</b-button>
</router-link>
<a :href="`${event.url}/participate/without-account`" v-else>
<figure class="image is-128x128">
<img src="../../assets/undraw_mail_2.svg" alt="Privacy illustration" />
<img
src="../../assets/undraw_mail_2.svg"
alt="Privacy illustration"
/>
</figure>
<b-button type="is-primary">{{ $t("I don't have a Mobilizon account") }}</b-button>
<b-button type="is-primary">{{
$t("I don't have a Mobilizon account")
}}</b-button>
</a>
<p>
<small>{{ $t("Participate using your email address") }}</small>

View File

@ -69,7 +69,11 @@ export default class PictureUpload extends Vue {
@Prop({ type: Object, required: false }) defaultImage!: IMedia;
@Prop({ type: String, required: false, default: "image/gif,image/png,image/jpeg,image/webp" })
@Prop({
type: String,
required: false,
default: "image/gif,image/png,image/jpeg,image/webp",
})
accept!: string;
@Prop({
@ -95,13 +99,11 @@ export default class PictureUpload extends Vue {
@Watch("pictureFile")
onPictureFileChanged(val: File): void {
console.log("onPictureFileChanged", val);
this.updatePreview(val);
}
@Watch("defaultImage")
onDefaultImageChange(defaultImage: IMedia): void {
console.log("onDefaultImageChange", defaultImage);
this.imageSrc = defaultImage ? defaultImage.url : null;
}

View File

@ -14,26 +14,46 @@
<div class="media-content">
<p class="post-minimalist-title">{{ post.title }}</p>
<div class="metadata">
<b-tag type="is-warning" size="is-small" v-if="post.draft">{{ $t("Draft") }}</b-tag>
<b-tag type="is-warning" size="is-small" v-if="post.draft">{{
$t("Draft")
}}</b-tag>
<small
v-if="post.visibility === PostVisibility.PUBLIC && isCurrentActorMember"
v-if="
post.visibility === PostVisibility.PUBLIC &&
isCurrentActorMember
"
class="has-text-grey"
>
<b-icon icon="earth" size="is-small" />{{ $t("Public") }}</small
>
<small v-else-if="post.visibility === PostVisibility.UNLISTED" class="has-text-grey">
<b-icon icon="link" size="is-small" />{{ $t("Accessible through link") }}</small
<small
v-else-if="post.visibility === PostVisibility.UNLISTED"
class="has-text-grey"
>
<b-icon icon="link" size="is-small" />{{
$t("Accessible through link")
}}</small
>
<small
v-else-if="post.visibility === PostVisibility.PRIVATE"
class="has-text-grey"
>
<small v-else-if="post.visibility === PostVisibility.PRIVATE" class="has-text-grey">
<b-icon icon="lock" size="is-small" />{{
$t("Accessible only to members", { group: post.attributedTo.name })
$t("Accessible only to members", {
group: post.attributedTo.name,
})
}}</small
>
<small class="has-text-grey">{{
$options.filters.formatDateTimeString(new Date(post.insertedAt), false)
$options.filters.formatDateTimeString(
new Date(post.insertedAt),
false
)
}}</small>
<small class="has-text-grey" v-if="isCurrentActorMember">{{
$t("Created by {username}", { username: `@${usernameWithDomain(post.author)}` })
$t("Created by {username}", {
username: `@${usernameWithDomain(post.author)}`,
})
}}</small>
</div>
</div>
@ -43,15 +63,17 @@
</template>
<script lang="ts">
import { usernameWithDomain } from "@/types/actor";
import { PostVisibility } from "@/types/enums";
import { Component, Prop, Vue } from "vue-property-decorator";
import RouteName from "../../router/name";
import { IPost, PostVisibility } from "../../types/post.model";
import { IPost } from "../../types/post.model";
@Component
export default class PostElementItem extends Vue {
@Prop({ required: true, type: Object }) post!: IPost;
@Prop({ required: false, type: Boolean, default: false }) isCurrentActorMember!: boolean;
@Prop({ required: false, type: Boolean, default: false })
isCurrentActorMember!: boolean;
RouteName = RouteName;
@ -74,7 +96,8 @@ export default class PostElementItem extends Vue {
.post-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1rem;
font-weight: 700;
overflow: hidden;

View File

@ -43,7 +43,8 @@ export default class PostListItem extends Vue {
.post-minimalist-title {
color: #3c376e;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-size: 1rem;
font-weight: 700;
overflow: hidden;

View File

@ -22,10 +22,18 @@
<div class="content columns">
<div class="column is-one-quarter-desktop">
<span v-if="report.reporter.type === ActorType.APPLICATION">
{{ $t("Reported by someone on {domain}", { domain: report.reporter.domain }) }}
{{
$t("Reported by someone on {domain}", {
domain: report.reporter.domain,
})
}}
</span>
<span v-else>
{{ $t("Reported by {reporter}", { reporter: report.reporter.preferredUsername }) }}
{{
$t("Reported by {reporter}", {
reporter: report.reporter.preferredUsername,
})
}}
</span>
</div>
<div class="column" v-if="report.content" v-html="report.content" />
@ -36,7 +44,7 @@
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { IReport } from "@/types/report.model";
import { ActorType } from "@/types/actor";
import { ActorType } from "@/types/enums";
@Component
export default class ReportCard extends Vue {

View File

@ -4,7 +4,10 @@
<p class="modal-card-title">{{ title }}</p>
</header>
<section class="modal-card-body is-flex" :class="{ 'is-titleless': !title }">
<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" />
@ -16,7 +19,12 @@
<figure class="image is-48x48" v-if="comment.actor.avatar">
<img :src="comment.actor.avatar.url" alt="Image" />
</figure>
<b-icon class="media-left" v-else size="is-large" icon="account-circle" />
<b-icon
class="media-left"
v-else
size="is-large"
icon="account-circle"
/>
</div>
<div class="media-content">
<div class="content">
@ -82,7 +90,10 @@ import { IComment } from "../../types/comment.model";
},
})
export default class ReportModal extends Vue {
@Prop({ type: Function }) onConfirm!: Function;
@Prop({ type: Function }) onConfirm!: (
content: string,
forward: boolean
) => void;
@Prop({ type: String }) title!: string;

View File

@ -14,7 +14,9 @@
</div>
<div class="body">
<h3>{{ resource.title }}</h3>
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span>
<span class="host" v-if="inline">{{
resource.updatedAt | formatDateTimeString
}}</span>
</div>
<draggable
v-if="!inline"
@ -93,21 +95,27 @@ export default class FolderItem extends Mixins(ResourceMixin) {
async moveResource(resource: IResource): Promise<IResource | undefined> {
try {
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>({
mutation: UPDATE_RESOURCE,
variables: {
id: resource.id,
path: `${this.resource.path}/${resource.title}`,
parentId: this.resource.id,
},
});
const { data } = await this.$apollo.mutate<{ updateResource: IResource }>(
{
mutation: UPDATE_RESOURCE,
variables: {
id: resource.id,
path: `${this.resource.path}/${resource.title}`,
parentId: this.resource.id,
},
}
);
if (!data) {
console.error("Error while updating resource");
return undefined;
}
return data.updateResource;
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
return undefined;
}
}

View File

@ -17,7 +17,7 @@
</b-dropdown>
</template>
<script lang="ts">
import { Component, Vue, Prop } from "vue-property-decorator";
import { Component, Vue } from "vue-property-decorator";
@Component
export default class ResourceDropdown extends Vue {}

View File

@ -2,7 +2,12 @@
<div class="resource-wrapper">
<a :href="resource.resourceUrl" target="_blank">
<div class="preview">
<div v-if="resource.type && Object.keys(mapServiceTypeToIcon).includes(resource.type)">
<div
v-if="
resource.type &&
Object.keys(mapServiceTypeToIcon).includes(resource.type)
"
>
<b-icon :icon="mapServiceTypeToIcon[resource.type]" size="is-large" />
</div>
<div
@ -21,7 +26,9 @@
:src="resource.metadata.faviconUrl"
/>
<h3>{{ resource.title }}</h3>
<span class="host" v-if="inline">{{ resource.updatedAt | formatDateTimeString }}</span>
<span class="host" v-if="inline">{{
resource.updatedAt | formatDateTimeString
}}</span>
<span class="host" v-else>{{ urlHostname }}</span>
</div>
</a>

View File

@ -2,9 +2,15 @@
<div v-if="resource">
<article class="panel is-primary">
<p class="panel-heading">
{{ $t('Move "{resourceName}"', { resourceName: initialResource.title }) }}
{{
$t('Move "{resourceName}"', { resourceName: initialResource.title })
}}
</p>
<a class="panel-block clickable" @click="resource = resource.parent" v-if="resource.parent">
<a
class="panel-block clickable"
@click="resource = resource.parent"
v-if="resource.parent"
>
<span class="panel-icon">
<b-icon icon="chevron-up" size="is-small" />
</span>
@ -23,12 +29,19 @@
<a
class="panel-block"
v-for="element in resource.children.elements"
:class="{ clickable: element.type === 'folder' && element.id !== initialResource.id }"
:class="{
clickable:
element.type === 'folder' && element.id !== initialResource.id,
}"
:key="element.id"
@click="goDown(element)"
>
<span class="panel-icon">
<b-icon icon="folder" size="is-small" v-if="element.type === 'folder'" />
<b-icon
icon="folder"
size="is-small"
v-if="element.type === 'folder'"
/>
<b-icon icon="link" size="is-small" v-else />
</span>
{{ element.title }}
@ -44,10 +57,17 @@
{{ $t("No resources in this folder") }}
</p>
</article>
<b-button type="is-primary" @click="updateResource" :disabled="moveDisabled">{{
$t("Move resource to {folder}", { folder: resource.title })
<b-button
type="is-primary"
@click="updateResource"
:disabled="moveDisabled"
>{{
$t("Move resource to {folder}", { folder: resource.title })
}}</b-button
>
<b-button type="is-text" @click="$emit('close-move-modal')">{{
$t("Cancel")
}}</b-button>
<b-button type="is-text" @click="$emit('closeMoveModal')">{{ $t("Cancel") }}</b-button>
</div>
</template>
<script lang="ts">
@ -81,31 +101,34 @@ export default class ResourceSelector extends Vue {
resource: IResource | undefined = this.initialResource.parent;
goDown(element: IResource) {
goDown(element: IResource): void {
if (element.type === "folder" && element.id !== this.initialResource.id) {
this.resource = element;
}
}
updateResource() {
updateResource(): void {
this.$emit(
"updateResource",
"update-resource",
{
id: this.initialResource.id,
title: this.initialResource.title,
parent: this.resource && this.resource.path === "/" ? null : this.resource,
parent:
this.resource && this.resource.path === "/" ? null : this.resource,
path: this.initialResource.path,
},
this.initialResource.parent
);
}
get moveDisabled() {
get moveDisabled(): boolean | undefined {
return (
(this.initialResource.parent &&
this.resource &&
this.initialResource.parent.path === this.resource.path) ||
(this.initialResource.parent == undefined && this.resource && this.resource.path === "/")
(this.initialResource.parent === undefined &&
this.resource &&
this.resource.path === "/")
);
}
}

View File

@ -23,9 +23,9 @@ export default class SearchField extends Vue {
search = "";
enter() {
async enter(): Promise<void> {
this.$emit("navbar-search");
this.$router.push({
await this.$router.push({
name: RouteName.SEARCH,
query: { term: this.search },
});

View File

@ -15,16 +15,27 @@
</p>
</div>
<div class="field">
<b-checkbox v-model="notificationOnDay" @input="updateSetting({ notificationOnDay })">
<b-checkbox
v-model="notificationOnDay"
@input="updateSetting({ notificationOnDay })"
>
<strong>{{ $t("Notification on the day of the event") }}</strong>
<p>
{{
$t("We'll use your timezone settings to send a recap of the morning of the event.")
$t(
"We'll use your timezone settings to send a recap of the morning of the event."
)
}}
</p>
</b-checkbox>
</div>
<p>{{ $t("To activate more notifications, head over to the notification settings.") }}</p>
<p>
{{
$t(
"To activate more notifications, head over to the notification settings."
)
}}
</p>
</section>
</div>
</template>
@ -50,7 +61,11 @@ export default class NotificationsOnboarding extends mixins(Onboarding) {
try {
this.doUpdateSetting(variables);
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -16,7 +16,7 @@ export default class SettingMenuItem extends Vue {
@Prop({ required: true, type: Object }) to!: Route;
get isActive() {
get isActive(): boolean {
if (!this.to) return false;
if (this.to.name === this.$route.name) {
if (this.to.params) {

View File

@ -20,11 +20,12 @@ export default class SettingMenuSection extends Vue {
@Prop({ required: true, type: Object }) to!: Route;
get sectionActive() {
get sectionActive(): boolean {
if (this.$slots.default) {
return this.$slots.default.some(
({
componentOptions: {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
propsData: { to },
},

View File

@ -1,18 +1,27 @@
<template>
<aside>
<ul>
<SettingMenuSection :title="$t('Account')" :to="{ name: RouteName.ACCOUNT_SETTINGS }">
<SettingMenuSection
:title="$t('Account')"
:to="{ name: RouteName.ACCOUNT_SETTINGS }"
>
<SettingMenuItem
:title="this.$t('General')"
:to="{ name: RouteName.ACCOUNT_SETTINGS_GENERAL }"
/>
<SettingMenuItem :title="$t('Preferences')" :to="{ name: RouteName.PREFERENCES }" />
<SettingMenuItem
:title="$t('Preferences')"
:to="{ name: RouteName.PREFERENCES }"
/>
<SettingMenuItem
:title="this.$t('Email notifications')"
:to="{ name: RouteName.NOTIFICATIONS }"
/>
</SettingMenuSection>
<SettingMenuSection :title="$t('Profiles')" :to="{ name: RouteName.IDENTITIES }">
<SettingMenuSection
:title="$t('Profiles')"
:to="{ name: RouteName.IDENTITIES }"
>
<SettingMenuItem
v-for="profile in identities"
:key="profile.preferredUsername"
@ -22,7 +31,10 @@
params: { identityName: profile.preferredUsername },
}"
/>
<SettingMenuItem :title="$t('New profile')" :to="{ name: RouteName.CREATE_IDENTITY }" />
<SettingMenuItem
:title="$t('New profile')"
:to="{ name: RouteName.CREATE_IDENTITY }"
/>
</SettingMenuSection>
<SettingMenuSection
v-if="
@ -33,35 +45,54 @@
:title="$t('Moderation')"
:to="{ name: RouteName.MODERATION }"
>
<SettingMenuItem :title="$t('Reports')" :to="{ name: RouteName.REPORTS }" />
<SettingMenuItem :title="$t('Moderation log')" :to="{ name: RouteName.REPORT_LOGS }" />
<SettingMenuItem
:title="$t('Reports')"
:to="{ name: RouteName.REPORTS }"
/>
<SettingMenuItem
:title="$t('Moderation log')"
:to="{ name: RouteName.REPORT_LOGS }"
/>
<SettingMenuItem :title="$t('Users')" :to="{ name: RouteName.USERS }" />
<SettingMenuItem :title="$t('Profiles')" :to="{ name: RouteName.PROFILES }" />
<SettingMenuItem :title="$t('Groups')" :to="{ name: RouteName.ADMIN_GROUPS }" />
<SettingMenuItem
:title="$t('Profiles')"
:to="{ name: RouteName.PROFILES }"
/>
<SettingMenuItem
:title="$t('Groups')"
:to="{ name: RouteName.ADMIN_GROUPS }"
/>
</SettingMenuSection>
<SettingMenuSection
v-if="this.currentUser.role == ICurrentUserRole.ADMINISTRATOR"
:title="$t('Admin')"
:to="{ name: RouteName.ADMIN }"
>
<SettingMenuItem :title="$t('Dashboard')" :to="{ name: RouteName.ADMIN_DASHBOARD }" />
<SettingMenuItem
:title="$t('Dashboard')"
:to="{ name: RouteName.ADMIN_DASHBOARD }"
/>
<SettingMenuItem
:title="$t('Instance settings')"
:to="{ name: RouteName.ADMIN_SETTINGS }"
/>
<SettingMenuItem :title="$t('Federation')" :to="{ name: RouteName.RELAYS }" />
<SettingMenuItem
:title="$t('Federation')"
:to="{ name: RouteName.RELAYS }"
/>
</SettingMenuSection>
</ul>
</aside>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Component, Vue } from "vue-property-decorator";
import { ICurrentUserRole } from "@/types/enums";
import SettingMenuSection from "./SettingMenuSection.vue";
import SettingMenuItem from "./SettingMenuItem.vue";
import { IDENTITIES } from "../../graphql/actor";
import { IPerson, Person } from "../../types/actor";
import { CURRENT_USER_CLIENT } from "../../graphql/user";
import { ICurrentUser, ICurrentUserRole } from "../../types/current-user.model";
import { ICurrentUser } from "../../types/current-user.model";
import RouteName from "../../router/name";
@ -70,7 +101,8 @@ import RouteName from "../../router/name";
apollo: {
identities: {
query: IDENTITIES,
update: (data) => data.identities.map((identity: IPerson) => new Person(identity)),
update: (data) =>
data.identities.map((identity: IPerson) => new Person(identity)),
},
currentUser: CURRENT_USER_CLIENT,
},

View File

@ -39,7 +39,10 @@
timezone,
})
}}
<b-message type="is-danger" v-if="!$apollo.loading && !supportedTimezone">
<b-message
type="is-danger"
v-if="!$apollo.loading && !supportedTimezone"
>
{{ $t("Your timezone {timezone} isn't supported.", { timezone }) }}
</b-message>
</p>

View File

@ -2,9 +2,10 @@
<div class="card" v-if="todo">
<div class="card-content">
<b-checkbox v-model="status" />
<router-link :to="{ name: RouteName.TODO, params: { todoId: todo.id } }">{{
todo.title
}}</router-link>
<router-link
:to="{ name: RouteName.TODO, params: { todoId: todo.id } }"
>{{ todo.title }}</router-link
>
<span class="details has-text-grey">
<span v-if="todo.dueDate" class="due_date">
<b-icon icon="calendar" />
@ -13,7 +14,9 @@
<span v-if="todo.assignedTo" class="assigned_to">
<b-icon icon="account" />
{{ `@${todo.assignedTo.preferredUsername}` }}
<span v-if="todo.assignedTo.domain">{{ `@${todo.assignedTo.domain}` }}</span>
<span v-if="todo.assignedTo.domain">{{
`@${todo.assignedTo.domain}`
}}</span>
</span>
</span>
</div>
@ -53,7 +56,11 @@ export default class Todo extends Vue {
});
this.editMode = false;
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -18,7 +18,7 @@
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { debounce } from "lodash";
import { debounce, DebouncedFunc } from "lodash";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { ITodo } from "../../types/todos";
import RouteName from "../../router/name";
@ -36,7 +36,9 @@ export default class Todo extends Vue {
editMode = false;
debounceUpdateTodo!: Function;
debounceUpdateTodo!: DebouncedFunc<
(obj: Record<string, unknown>) => Promise<void>
>;
// We put this in data because of issues like
// https://github.com/vuejs/vue-class-component/issues/263
@ -89,7 +91,11 @@ export default class Todo extends Vue {
});
this.editMode = false;
} catch (e) {
Snackbar.open({ message: e.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: e.message,
type: "is-danger",
position: "is-bottom",
});
}
}
}

View File

@ -7,7 +7,11 @@
<b-icon :icon="oauthProvider.id" />
<span>{{ SELECTED_PROVIDERS[oauthProvider.id] }}</span></a
>
<a class="button is-light" :href="`/auth/${oauthProvider.id}`" v-else-if="isProviderSelected">
<a
class="button is-light"
:href="`/auth/${oauthProvider.id}`"
v-else-if="isProviderSelected"
>
<b-icon icon="lock" />
<span>{{ oauthProvider.label }}</span>
</a>

View File

@ -20,7 +20,9 @@
</div>
<vertical-divider :content="$t('Or')" />
<div class="column">
<subtitle>{{ $t("I have an account on another Mobilizon instance.") }}</subtitle>
<subtitle>{{
$t("I have an account on another Mobilizon instance.")
}}</subtitle>
<p>{{ $t("Other software may also support this.") }}</p>
<p>{{ sentence }}</p>
<form @submit.prevent="redirectToInstance">
@ -34,7 +36,9 @@
:placeholder="$t('profile@instance')"
></b-input>
<p class="control">
<button class="button is-primary" type="submit">{{ $t("Go") }}</button>
<button class="button is-primary" type="submit">
{{ $t("Go") }}
</button>
</p>
</b-field>
</b-field>
@ -54,7 +58,7 @@
import { Component, Prop, Vue } from "vue-property-decorator";
import VerticalDivider from "@/components/Utils/VerticalDivider.vue";
import Subtitle from "@/components/Utils/Subtitle.vue";
import { LoginErrorCode } from "@/types/login-error-code.model";
import { LoginErrorCode } from "@/types/enums";
import RouteName from "../../router/name";
@Component({
@ -80,14 +84,22 @@ export default class RedirectWithAccount extends Vue {
async redirectToInstance(): Promise<void> {
const [, host] = this.remoteActorAddress.split("@", 2);
const remoteInteractionURI = await this.webFingerFetch(host, this.remoteActorAddress);
const remoteInteractionURI = await this.webFingerFetch(
host,
this.remoteActorAddress
);
window.open(remoteInteractionURI);
}
private async webFingerFetch(hostname: string, identity: string): Promise<string> {
private async webFingerFetch(
hostname: string,
identity: string
): Promise<string> {
const scheme = process.env.NODE_ENV === "production" ? "https" : "http";
const data = await (
await fetch(`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`)
await fetch(
`${scheme}://${hostname}/.well-known/webfinger?resource=acct:${identity}`
)
).json();
if (data && Array.isArray(data.links)) {
const link: { template: string } = data.links.find(

View File

@ -21,7 +21,8 @@ h2 {
display: inline;
padding: 3px 8px;
color: #3a384c;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial, serif;
font-family: "Liberation Sans", "Helvetica Neue", Roboto, Helvetica, Arial,
serif;
font-weight: 400;
font-size: 32px;
}

View File

@ -14,7 +14,10 @@ function formatDateString(value: string): string {
}
function formatTimeString(value: string): string {
return parseDateTime(value).toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" });
return parseDateTime(value).toLocaleTimeString(undefined, {
hour: "numeric",
minute: "numeric",
});
}
function formatDateTimeString(value: string, showTime = true): string {

View File

@ -1,5 +1,9 @@
import nl2br from "@/filters/utils";
import { formatDateString, formatTimeString, formatDateTimeString } from "./datetime";
import {
formatDateString,
formatTimeString,
formatDateTimeString,
} from "./datetime";
export default {
install(vue: any): void {

View File

@ -65,7 +65,10 @@ export const GET_PERSON = gql`
feedTokens {
token
}
organizedEvents(page: $organizedEventsPage, limit: $organizedEventsLimit) {
organizedEvents(
page: $organizedEventsPage
limit: $organizedEventsLimit
) {
total
elements {
id
@ -442,7 +445,12 @@ export const CREATE_PERSON = gql`
`;
export const UPDATE_PERSON = gql`
mutation UpdatePerson($id: ID!, $name: String, $summary: String, $avatar: MediaInput) {
mutation UpdatePerson(
$id: ID!
$name: String
$summary: String
$avatar: MediaInput
) {
updatePerson(id: $id, name: $name, summary: $summary, avatar: $avatar) {
id
preferredUsername
@ -469,7 +477,12 @@ export const DELETE_PERSON = gql`
* Prefer CREATE_PERSON when creating another identity
*/
export const REGISTER_PERSON = gql`
mutation($preferredUsername: String!, $name: String!, $summary: String!, $email: String!) {
mutation(
$preferredUsername: String!
$name: String!
$summary: String!
$email: String!
) {
registerPerson(
preferredUsername: $preferredUsername
name: $name

View File

@ -68,8 +68,16 @@ export const COMMENTS_THREADS = gql`
`;
export const CREATE_COMMENT_FROM_EVENT = gql`
mutation CreateCommentFromEvent($eventId: ID!, $text: String!, $inReplyToCommentId: ID) {
createComment(eventId: $eventId, text: $text, inReplyToCommentId: $inReplyToCommentId) {
mutation CreateCommentFromEvent(
$eventId: ID!
$text: String!
$inReplyToCommentId: ID
) {
createComment(
eventId: $eventId
text: $text
inReplyToCommentId: $inReplyToCommentId
) {
...CommentRecursive
}
}

View File

@ -84,7 +84,6 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
id
url
}
mediaSize
organizedEvents(
afterDatetime: $afterDateTime
beforeDatetime: $beforeDateTime
@ -96,6 +95,7 @@ export const GROUP_FIELDS_FRAGMENTS = gql`
uuid
title
beginsOn
draft
options {
maximumAttendeeCapacity
}
@ -213,6 +213,7 @@ export const GET_GROUP = gql`
$organisedEventslimit: Int
) {
getGroup(id: $id) {
mediaSize
...GroupFullFields
}
}

View File

@ -8,6 +8,7 @@ export const POST_FRAGMENT = gql`
slug
url
body
draft
author {
id
preferredUsername

View File

@ -42,8 +42,20 @@ export const SEARCH_EVENTS = gql`
`;
export const SEARCH_GROUPS = gql`
query SearchGroups($term: String, $location: String, $radius: Float, $page: Int, $limit: Int) {
searchGroups(term: $term, location: $location, radius: $radius, page: $page, limit: $limit) {
query SearchGroups(
$term: String
$location: String
$radius: Float
$page: Int
$limit: Int
) {
searchGroups(
term: $term
location: $location
radius: $radius
page: $page
limit: $limit
) {
total
elements {
id

View File

@ -108,7 +108,12 @@ export const UPDATE_CURRENT_USER_CLIENT = gql`
$isLoggedIn: Boolean!
$role: UserRole!
) {
updateCurrentUser(id: $id, email: $email, isLoggedIn: $isLoggedIn, role: $role) @client
updateCurrentUser(
id: $id
email: $email
isLoggedIn: $isLoggedIn
role: $role
) @client
}
`;

View File

@ -800,6 +800,5 @@
"Mobilizon is a federated software, meaning you can interact - depending on your admin's federation settings - with content from other instances, such as joining groups or events that were created elsewhere.": "Mobilizon is a federated software, meaning you can interact - depending on your admin federation settings - with content from other instances, such as joining groups or events that were created elsewhere.",
"This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.": "This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.",
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:",
"Uploaded media size": "Uploaded media size",
"chapril services description": "gather your Chapril tools in this place"
"Uploaded media size": "Uploaded media size"
}

View File

@ -888,6 +888,5 @@
"Mobilizon is a federated software, meaning you can interact - depending on your admin's federation settings - with content from other instances, such as joining groups or events that were created elsewhere.": "Mobilizon est un logiciel fédéré, ce qui signifie que vous pouvez interagir - en fonction des paramètres de fédération de votre administrateur·ice - avec du contenu d'autres instances, comme par exemple rejoindre des groupes ou des événements ayant été créés ailleurs.",
"This instance, <b>{instanceName} ({domain})</b>, hosts your profile, so remember its name.": "Cette instance, <b>{instanceName} ({domain})</b>, héberge votre profil, donc notez bien son nom.",
"If you are being asked for your federated indentity, it's composed of your username and your instance. For instance, the federated identity for your first profile is:": "Si l'on vous demande votre identité fédérée, elle est composée de votre nom d'utilisateur·ice et de votre instance. Par exemple, l'identité fédérée de votre premier profil est :",
"Uploaded media size": "Taille des médias téléversés",
"chapril services description": "utilisez les outils du Chapril pour votre groupe, et regroupez les liens dans cet espace de partage"
"Uploaded media size": "Taille des médias téléversés"
}

View File

@ -4,9 +4,11 @@ import { Component, Vue } from "vue-property-decorator";
@Component
export default class ActorMixin extends Vue {
static actorIsOrganizer(actor: IActor, event: IEvent) {
static actorIsOrganizer(actor: IActor, event: IEvent): boolean {
console.log("actorIsOrganizer actor", actor.id);
console.log("actorIsOrganizer event", event);
return event.organizerActor && actor.id === event.organizerActor.id;
return (
event.organizerActor !== undefined && actor.id === event.organizerActor.id
);
}
}

View File

@ -1,7 +1,8 @@
import { mixins } from "vue-class-component";
import { Component, Vue } from "vue-property-decorator";
import { SnackbarProgrammatic as Snackbar } from "buefy";
import { IParticipant, ParticipantRole } from "../types/participant.model";
import { ParticipantRole } from "@/types/enums";
import { IParticipant } from "../types/participant.model";
import { IEvent } from "../types/event.model";
import {
DELETE_EVENT,
@ -20,7 +21,9 @@ export default class EventMixin extends mixins(Vue) {
anonymousParticipationConfirmed: boolean | null = null
): Promise<void> {
try {
const { data: resultData } = await this.$apollo.mutate<{ leaveEvent: IParticipant }>({
const { data: resultData } = await this.$apollo.mutate<{
leaveEvent: IParticipant;
}>({
mutation: LEAVE_EVENT,
variables: {
eventId: event.id,
@ -32,14 +35,18 @@ export default class EventMixin extends mixins(Vue) {
let participation;
if (!token) {
const participationCachedData = store.readQuery<{ person: IPerson }>({
const participationCachedData = store.readQuery<{
person: IPerson;
}>({
query: EVENT_PERSON_PARTICIPATION,
variables: { eventId: event.id, actorId },
});
if (participationCachedData == null) return;
const { person } = participationCachedData;
if (person === null) {
console.error("Cannot update participation cache, because of null value.");
console.error(
"Cannot update participation cache, because of null value."
);
return;
}
[participation] = person.participations.elements;
@ -62,7 +69,10 @@ export default class EventMixin extends mixins(Vue) {
console.error("Cannot update event cache, because of null value.");
return;
}
if (participation && participation.role === ParticipantRole.NOT_APPROVED) {
if (
participation &&
participation.role === ParticipantRole.NOT_APPROVED
) {
eventCached.participantStats.notApproved -= 1;
} else if (anonymousParticipationConfirmed === false) {
eventCached.participantStats.notConfirmed -= 1;
@ -81,13 +91,19 @@ export default class EventMixin extends mixins(Vue) {
this.participationCancelledMessage();
}
} catch (error) {
Snackbar.open({ message: error.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: error.message,
type: "is-danger",
position: "is-bottom",
});
console.error(error);
}
}
private participationCancelledMessage() {
this.$notifier.success(this.$t("You have cancelled your participation") as string);
this.$notifier.success(
this.$t("You have cancelled your participation") as string
);
}
protected async openDeleteEventModal(event: IEvent): Promise<void> {
@ -96,21 +112,29 @@ export default class EventMixin extends mixins(Vue) {
}
const participantsLength = event.participantStats.participant;
const prefix = participantsLength
? this.$tc("There are {participants} participants.", event.participantStats.participant, {
participants: event.participantStats.participant,
})
? this.$tc(
"There are {participants} participants.",
event.participantStats.participant,
{
participants: event.participantStats.participant,
}
)
: "";
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.")}
${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: event.title,
})}`,
confirmText: this.$t("Delete {eventTitle}", { eventTitle: event.title }) as string,
confirmText: this.$t("Delete {eventTitle}", {
eventTitle: event.title,
}) as string,
inputAttrs: {
placeholder: event.title,
pattern: escapeRegExp(event.title),
@ -138,13 +162,19 @@ export default class EventMixin extends mixins(Vue) {
this.$emit("event-deleted", event.id);
this.$buefy.notification.open({
message: this.$t("Event {eventTitle} deleted", { eventTitle }) as string,
message: this.$t("Event {eventTitle} deleted", {
eventTitle,
}) as string,
type: "is-success",
position: "is-bottom-right",
duration: 5000,
});
} catch (error) {
Snackbar.open({ message: error.message, type: "is-danger", position: "is-bottom" });
Snackbar.open({
message: error.message,
type: "is-danger",
position: "is-bottom",
});
console.error(error);
}

View File

@ -2,7 +2,8 @@ import { PERSON_MEMBERSHIPS, CURRENT_ACTOR_CLIENT } from "@/graphql/actor";
import { GROUP_MEMBERSHIP_SUBSCRIPTION_CHANGED } from "@/graphql/event";
import { FETCH_GROUP } from "@/graphql/group";
import RouteName from "@/router/name";
import { Group, IActor, IGroup, IPerson, MemberRole } from "@/types/actor";
import { Group, IActor, IGroup, IPerson } from "@/types/actor";
import { MemberRole } from "@/types/enums";
import { Component, Vue } from "vue-property-decorator";
@Component({
@ -60,7 +61,10 @@ export default class GroupMixin extends Vue {
}
get isCurrentActorAGroupModerator(): boolean {
return this.hasCurrentActorThisRole([MemberRole.MODERATOR, MemberRole.ADMINISTRATOR]);
return this.hasCurrentActorThisRole([
MemberRole.MODERATOR,
MemberRole.ADMINISTRATOR,
]);
}
hasCurrentActorThisRole(givenRole: string | string[]): boolean {
@ -68,7 +72,8 @@ export default class GroupMixin extends Vue {
return (
this.person &&
this.person.memberships.elements.some(
({ parent: { id }, role }) => id === this.group.id && roles.includes(role)
({ parent: { id }, role }) =>
id === this.group.id && roles.includes(role)
)
);
}

View File

@ -9,10 +9,14 @@ export default class IdentityEditionMixin extends Mixins(Vue) {
oldDisplayName: string | null = null;
autoUpdateUsername(newDisplayName: string | null): void {
const oldUsername = IdentityEditionMixin.convertToUsername(this.oldDisplayName);
const oldUsername = IdentityEditionMixin.convertToUsername(
this.oldDisplayName
);
if (this.identity.preferredUsername === oldUsername) {
this.identity.preferredUsername = IdentityEditionMixin.convertToUsername(newDisplayName);
this.identity.preferredUsername = IdentityEditionMixin.convertToUsername(
newDisplayName
);
}
this.oldDisplayName = newDisplayName;

View File

@ -13,7 +13,9 @@ export default class Onboarding extends Vue {
RouteName = RouteName;
protected async doUpdateSetting(variables: Record<string, unknown>): Promise<void> {
protected async doUpdateSetting(
variables: Record<string, unknown>
): Promise<void> {
await this.$apollo.mutate<{ setUserSettings: string }>({
mutation: SET_USER_SETTINGS,
variables,

View File

@ -1,8 +1,9 @@
import { Component, Vue, Ref } from "vue-property-decorator";
import { ActorType, IActor } from "@/types/actor";
import { IActor } from "@/types/actor";
import { IFollower } from "@/types/actor/follower.model";
import { RELAY_FOLLOWERS, RELAY_FOLLOWINGS } from "@/graphql/admin";
import { Paginate } from "@/types/paginate";
import { ActorType } from "@/types/enums";
@Component({
apollo: {
@ -62,7 +63,10 @@ export default class RelayMixin extends Vue {
relayFollowings: {
__typename: previousResult.relayFollowings.__typename,
total: previousResult.relayFollowings.total,
elements: [...previousResult.relayFollowings.elements, ...newFollowings],
elements: [
...previousResult.relayFollowings.elements,
...newFollowings,
],
},
};
},
@ -87,7 +91,10 @@ export default class RelayMixin extends Vue {
relayFollowers: {
__typename: previousResult.relayFollowers.__typename,
total: previousResult.relayFollowers.total,
elements: [...previousResult.relayFollowers.elements, ...newFollowers],
elements: [
...previousResult.relayFollowers.elements,
...newFollowers,
],
},
};
},
@ -100,7 +107,8 @@ export default class RelayMixin extends Vue {
static isInstance(actor: IActor): boolean {
return (
actor.type === ActorType.APPLICATION &&
(actor.preferredUsername === "relay" || actor.preferredUsername === actor.domain)
(actor.preferredUsername === "relay" ||
actor.preferredUsername === actor.domain)
);
}
}

View File

@ -7,7 +7,10 @@ declare module "vue/types/vue" {
}
}
export function DateFnsPlugin(vue: typeof VueInstance, { locale }: { locale: string }): void {
export function DateFnsPlugin(
vue: typeof VueInstance,
{ locale }: { locale: string }
): void {
import(`date-fns/locale/${locale}/index.js`).then((localeEntity) => {
VueInstance.prototype.$dateFnsLocale = localeEntity;
});

View File

@ -23,7 +23,9 @@ if (process.env.NODE_ENV === "production") {
console.log("New content is available; please refresh.");
},
offline() {
console.log("No internet connection found. App is running in offline mode.");
console.log(
"No internet connection found. App is running in offline mode."
);
},
error(error) {
console.error("Error during service worker registration:", error);

View File

@ -1,4 +1,5 @@
import { RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum ActorRouteName {
GROUP = "Group",
@ -11,20 +12,23 @@ export const actorRoutes: RouteConfig[] = [
{
path: "/groups/create",
name: ActorRouteName.CREATE_GROUP,
component: () => import(/* webpackChunkName: "CreateGroup" */ "@/views/Group/Create.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "CreateGroup" */ "@/views/Group/Create.vue"),
meta: { requiredAuth: true },
},
{
path: "/@:preferredUsername",
name: ActorRouteName.GROUP,
component: () => import(/* webpackChunkName: "Group" */ "@/views/Group/Group.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Group" */ "@/views/Group/Group.vue"),
props: true,
meta: { requiredAuth: false },
},
{
path: "/groups/me",
name: ActorRouteName.MY_GROUPS,
component: () => import(/* webpackChunkName: "MyGroups" */ "@/views/Group/MyGroups.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "MyGroups" */ "@/views/Group/MyGroups.vue"),
meta: { requiredAuth: true },
},
];

View File

@ -1,4 +1,5 @@
import { RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum DiscussionRouteName {
DISCUSSION_LIST = "DISCUSSION_LIST",
@ -10,24 +11,30 @@ export const discussionRoutes: RouteConfig[] = [
{
path: "/@:preferredUsername/discussions",
name: DiscussionRouteName.DISCUSSION_LIST,
component: () =>
import(/* webpackChunkName: "DiscussionsList" */ "@/views/Discussions/DiscussionsList.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "DiscussionsList" */ "@/views/Discussions/DiscussionsList.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "/@:preferredUsername/discussions/new",
name: DiscussionRouteName.CREATE_DISCUSSION,
component: () =>
import(/* webpackChunkName: "CreateDiscussion" */ "@/views/Discussions/Create.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "CreateDiscussion" */ "@/views/Discussions/Create.vue"
),
props: true,
meta: { requiredAuth: true },
},
{
path: "/@:preferredUsername/c/:slug/:comment_id?",
name: DiscussionRouteName.DISCUSSION,
component: () =>
import(/* webpackChunkName: "Discussion" */ "@/views/Discussions/Discussion.vue"),
component: (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "Discussion" */ "@/views/Discussions/Discussion.vue"
),
props: true,
meta: { requiredAuth: true },
},

View File

@ -1,5 +1,6 @@
import { beforeRegisterGuard } from "@/router/guards/register-guard";
import { RouteConfig } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum ErrorRouteName {
ERROR = "Error",
@ -9,7 +10,8 @@ export const errorRoutes: RouteConfig[] = [
{
path: "/error",
name: ErrorRouteName.ERROR,
component: () => import(/* webpackChunkName: "Error" */ "../views/Error.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Error" */ "../views/Error.vue"),
beforeEnter: beforeRegisterGuard,
},
];

View File

@ -1,10 +1,16 @@
import { RouteConfig, Route } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
const participations = () =>
import(/* webpackChunkName: "participations" */ "@/views/Event/Participants.vue");
const editEvent = () => import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue");
const event = () => import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue");
const myEvents = () => import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue");
const participations = (): Promise<EsModuleComponent> =>
import(
/* webpackChunkName: "participations" */ "@/views/Event/Participants.vue"
);
const editEvent = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "edit-event" */ "@/views/Event/Edit.vue");
const event = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "event" */ "@/views/Event/Event.vue");
const myEvents = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "my-events" */ "@/views/Event/MyEvents.vue");
export enum EventRouteName {
EVENT_LIST = "EventList",
@ -18,7 +24,6 @@ export enum EventRouteName {
EVENT_PARTICIPATE_WITHOUT_ACCOUNT = "EVENT_PARTICIPATE_WITHOUT_ACCOUNT",
EVENT_PARTICIPATE_LOGGED_OUT = "EVENT_PARTICIPATE_LOGGED_OUT",
EVENT_PARTICIPATE_CONFIRM = "EVENT_PARTICIPATE_CONFIRM",
LOCATION = "Location",
TAG = "Tag",
}
@ -26,7 +31,8 @@ export const eventRoutes: RouteConfig[] = [
{
path: "/events/list/:location?",
name: EventRouteName.EVENT_LIST,
component: () => import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"),
meta: { requiredAuth: false },
},
{
@ -46,14 +52,19 @@ export const eventRoutes: RouteConfig[] = [
name: EventRouteName.EDIT_EVENT,
component: editEvent,
meta: { requiredAuth: true },
props: (route: Route) => ({ ...route.params, ...{ isUpdate: true } }),
props: (route: Route): Record<string, unknown> => {
return { ...route.params, ...{ isUpdate: true } };
},
},
{
path: "/events/duplicate/:eventId",
name: EventRouteName.DUPLICATE_EVENT,
component: editEvent,
meta: { requiredAuth: true },
props: (route: Route) => ({ ...route.params, ...{ isDuplicate: true } }),
props: (route: Route): Record<string, unknown> => ({
...route.params,
...{ isDuplicate: true },
}),
},
{
path: "/events/:eventId/participations",
@ -62,12 +73,6 @@ export const eventRoutes: RouteConfig[] = [
meta: { requiredAuth: true },
props: true,
},
{
path: "/location/new",
name: EventRouteName.LOCATION,
component: () => import(/* webpackChunkName: "Location" */ "@/views/Location.vue"),
meta: { requiredAuth: true },
},
{
path: "/events/:uuid",
name: EventRouteName.EVENT,
@ -78,31 +83,36 @@ export const eventRoutes: RouteConfig[] = [
{
path: "/events/:uuid/participate",
name: EventRouteName.EVENT_PARTICIPATE_LOGGED_OUT,
component: () => import("../components/Participation/UnloggedParticipation.vue"),
component: (): Promise<EsModuleComponent> =>
import("../components/Participation/UnloggedParticipation.vue"),
props: true,
},
{
path: "/events/:uuid/participate/with-account",
name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
component: () => import("../components/Participation/ParticipationWithAccount.vue"),
component: (): Promise<EsModuleComponent> =>
import("../components/Participation/ParticipationWithAccount.vue"),
props: true,
},
{
path: "/events/:uuid/participate/without-account",
name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT,
component: () => import("../components/Participation/ParticipationWithoutAccount.vue"),
component: (): Promise<EsModuleComponent> =>
import("../components/Participation/ParticipationWithoutAccount.vue"),
props: true,
},
{
path: "/participation/email/confirm/:token",
name: EventRouteName.EVENT_PARTICIPATE_CONFIRM,
component: () => import("../components/Participation/ConfirmParticipation.vue"),
component: (): Promise<EsModuleComponent> =>
import("../components/Participation/ConfirmParticipation.vue"),
props: true,
},
{
path: "/tag/:tag",
name: EventRouteName.TAG,
component: () => import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
component: (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
props: true,
meta: { requiredAuth: false },
},

View File

@ -1,4 +1,5 @@
import { RouteConfig, Route } from "vue-router";
import { EsModuleComponent } from "vue/types/options";
export enum GroupsRouteName {
TODO_LISTS = "TODO_LISTS",
@ -18,29 +19,33 @@ export enum GroupsRouteName {
GROUP_JOIN = "GROUP_JOIN",
}
const resourceFolder = () => import("@/views/Resources/ResourceFolder.vue");
const groupEvents = () =>
const resourceFolder = (): Promise<EsModuleComponent> =>
import("@/views/Resources/ResourceFolder.vue");
const groupEvents = (): Promise<EsModuleComponent> =>
import(/* webpackChunkName: "groupEvents" */ "@/views/Event/GroupEvents.vue");
export const groupsRoutes: RouteConfig[] = [
{
path: "/@:preferredUsername/todo-lists",
name: GroupsRouteName.TODO_LISTS,
component: () => import("@/views/Todos/TodoLists.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/views/Todos/TodoLists.vue"),
props: true,
meta: { requiredAuth: true },
},
{
path: "/todo-lists/:id",
name: GroupsRouteName.TODO_LIST,
component: () => import("@/views/Todos/TodoList.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/views/Todos/TodoList.vue"),
props: true,
meta: { requiredAuth: true },
},
{
path: "/todo/:todoId",
name: GroupsRouteName.TODO,
component: () => import("@/views/Todos/Todo.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/views/Todos/Todo.vue"),
props: true,
meta: { requiredAuth: true },
},
@ -60,7 +65,8 @@ export const groupsRoutes: RouteConfig[] = [
},
{
path: "/@:preferredUsername/settings",
component: () => import("@/views/Group/Settings.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/views/Group/Settings.vue"),
props: true,
meta: { requiredAuth: true },
redirect: { name: GroupsRouteName.GROUP_PUBLIC_SETTINGS },
@ -69,40 +75,49 @@ export const groupsRoutes: RouteConfig[] = [
{
path: "public",
name: GroupsRouteName.GROUP_PUBLIC_SETTINGS,
component: () => import("../views/Group/GroupSettings.vue"),
component: (): Promise<EsModuleComponent> =>
import("../views/Group/GroupSettings.vue"),
},
{
path: "members",
name: GroupsRouteName.GROUP_MEMBERS_SETTINGS,
component: () => import("../views/Group/GroupMembers.vue"),
component: (): Promise<EsModuleComponent> =>
import("../views/Group/GroupMembers.vue"),
props: true,
},
],
},
{
path: "/@:preferredUsername/p/new",
component: () => import("@/views/Posts/Edit.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/views/Posts/Edit.vue"),
props: true,
name: GroupsRouteName.POST_CREATE,
meta: { requiredAuth: true },
},
{
path: "/p/:slug/edit",
component: () => import("@/views/Posts/Edit.vue"),
props: (route: Route) => ({ ...route.params, ...{ isUpdate: true } }),
component: (): Promise<EsModuleComponent> =>
import("@/views/Posts/Edit.vue"),
props: (route: Route): Record<string, unknown> => ({
...route.params,
...{ isUpdate: true },
}),
name: GroupsRouteName.POST_EDIT,
meta: { requiredAuth: true },
},
{
path: "/p/:slug",
component: () => import("@/views/Posts/Post.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/views/Posts/Post.vue"),
props: true,
name: GroupsRouteName.POST,
meta: { requiredAuth: false },
},
{
path: "/@:preferredUsername/p",
component: () => import("@/views/Posts/List.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/views/Posts/List.vue"),
props: true,
name: GroupsRouteName.POSTS,
meta: { requiredAuth: false },
@ -116,7 +131,8 @@ export const groupsRoutes: RouteConfig[] = [
},
{
path: "/@:preferredUsername/join",
component: () => import("@/components/Group/JoinGroupWithAccount.vue"),
component: (): Promise<EsModuleComponent> =>
import("@/components/Group/JoinGroupWithAccount.vue"),
props: true,
name: GroupsRouteName.GROUP_JOIN,
meta: { requiredAuth: false },

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