Merge branch 'timezone' into 'master'
Timezones ! See merge request framasoft/mobilizon!1077
This commit is contained in:
commit
76aeac2989
|
@ -101,23 +101,6 @@ deps:
|
||||||
needs:
|
needs:
|
||||||
- install
|
- install
|
||||||
|
|
||||||
exunit-1.11:
|
|
||||||
stage: test
|
|
||||||
image: tcitworld/mobilizon-ci:legacy
|
|
||||||
services:
|
|
||||||
- name: postgis/postgis:11-3.0
|
|
||||||
alias: postgres
|
|
||||||
variables:
|
|
||||||
MIX_ENV: test
|
|
||||||
before_script:
|
|
||||||
- mix deps.clean --all
|
|
||||||
- mix deps.get
|
|
||||||
- mix ecto.create
|
|
||||||
- mix ecto.migrate
|
|
||||||
script:
|
|
||||||
- mix coveralls
|
|
||||||
allow_failure: true
|
|
||||||
|
|
||||||
exunit:
|
exunit:
|
||||||
stage: test
|
stage: test
|
||||||
services:
|
services:
|
||||||
|
@ -127,6 +110,7 @@ exunit:
|
||||||
MIX_ENV: test
|
MIX_ENV: test
|
||||||
before_script:
|
before_script:
|
||||||
- mix deps.get
|
- mix deps.get
|
||||||
|
- mix tz_world.update
|
||||||
- mix ecto.create
|
- mix ecto.create
|
||||||
- mix ecto.migrate
|
- mix ecto.migrate
|
||||||
script:
|
script:
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
# We build Elixir manually to have the oldest acceptable version of OTP
|
|
||||||
FROM erlang:21
|
|
||||||
LABEL maintainer="Thomas Citharel <tcit@tcit.fr>"
|
|
||||||
|
|
||||||
# elixir expects utf8.
|
|
||||||
ENV ELIXIR_VERSION="v1.11.4" \
|
|
||||||
LANG=C.UTF-8
|
|
||||||
|
|
||||||
RUN set -xe \
|
|
||||||
&& ELIXIR_DOWNLOAD_URL="https://github.com/elixir-lang/elixir/archive/${ELIXIR_VERSION}.tar.gz" \
|
|
||||||
&& ELIXIR_DOWNLOAD_SHA256="85c7118a0db6007507313db5bddf370216d9394ed7911fe80f21e2fbf7f54d29" \
|
|
||||||
&& curl -fSL -o elixir-src.tar.gz $ELIXIR_DOWNLOAD_URL \
|
|
||||||
&& echo "$ELIXIR_DOWNLOAD_SHA256 elixir-src.tar.gz" | sha256sum -c - \
|
|
||||||
&& mkdir -p /usr/local/src/elixir \
|
|
||||||
&& tar -xzC /usr/local/src/elixir --strip-components=1 -f elixir-src.tar.gz \
|
|
||||||
&& rm elixir-src.tar.gz \
|
|
||||||
&& cd /usr/local/src/elixir \
|
|
||||||
&& make install clean
|
|
||||||
|
|
||||||
CMD ["iex"]
|
|
||||||
|
|
||||||
ENV REFRESHED_AT=2021-06-07
|
|
||||||
RUN apt-get update -yq && apt-get install -yq build-essential inotify-tools postgresql-client git curl gnupg xvfb libgtk-3-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 cmake exiftool
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_12.x | bash && apt-get install nodejs -yq
|
|
||||||
RUN npm install -g yarn wait-on
|
|
||||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
|
||||||
RUN mix local.hex --force && mix local.rebar --force
|
|
||||||
RUN curl https://dbip.mirror.framasoft.org/files/dbip-city-lite-latest.mmdb --output GeoLite2-City.mmdb -s && mkdir -p /usr/share/GeoIP && mv GeoLite2-City.mmdb /usr/share/GeoIP/
|
|
|
@ -29,6 +29,8 @@
|
||||||
"@tiptap/extension-underline": "^2.0.0-beta.7",
|
"@tiptap/extension-underline": "^2.0.0-beta.7",
|
||||||
"@tiptap/starter-kit": "^2.0.0-beta.37",
|
"@tiptap/starter-kit": "^2.0.0-beta.37",
|
||||||
"@tiptap/vue-2": "^2.0.0-beta.21",
|
"@tiptap/vue-2": "^2.0.0-beta.21",
|
||||||
|
"@vue-a11y/announcer": "^2.1.0",
|
||||||
|
"@vue-a11y/skip-to": "^2.1.2",
|
||||||
"@vue/apollo-option": "4.0.0-alpha.11",
|
"@vue/apollo-option": "4.0.0-alpha.11",
|
||||||
"apollo-absinthe-upload-link": "^1.5.0",
|
"apollo-absinthe-upload-link": "^1.5.0",
|
||||||
"blurhash": "^1.1.3",
|
"blurhash": "^1.1.3",
|
||||||
|
@ -36,6 +38,7 @@
|
||||||
"bulma-divider": "^0.2.0",
|
"bulma-divider": "^0.2.0",
|
||||||
"core-js": "^3.6.4",
|
"core-js": "^3.6.4",
|
||||||
"date-fns": "^2.16.0",
|
"date-fns": "^2.16.0",
|
||||||
|
"date-fns-tz": "^1.1.6",
|
||||||
"graphql": "^15.0.0",
|
"graphql": "^15.0.0",
|
||||||
"graphql-tag": "^2.10.3",
|
"graphql-tag": "^2.10.3",
|
||||||
"intersection-observer": "^0.12.0",
|
"intersection-observer": "^0.12.0",
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="mobilizon">
|
<div id="mobilizon">
|
||||||
|
<VueAnnouncer />
|
||||||
|
<VueSkipTo to="#main" :label="$t('Skip to main content')" />
|
||||||
<NavBar />
|
<NavBar />
|
||||||
<div v-if="config && config.demoMode">
|
<div v-if="config && config.demoMode">
|
||||||
<b-message
|
<b-message
|
||||||
|
@ -22,9 +24,9 @@
|
||||||
</div>
|
</div>
|
||||||
<error v-if="error" :error="error" />
|
<error v-if="error" :error="error" />
|
||||||
|
|
||||||
<main v-else>
|
<main id="main" v-else>
|
||||||
<transition name="fade" mode="out-in">
|
<transition name="fade" mode="out-in">
|
||||||
<router-view />
|
<router-view ref="routerView" />
|
||||||
</transition>
|
</transition>
|
||||||
</main>
|
</main>
|
||||||
<mobilizon-footer />
|
<mobilizon-footer />
|
||||||
|
@ -32,7 +34,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue } from "vue-property-decorator";
|
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
|
||||||
import NavBar from "./components/NavBar.vue";
|
import NavBar from "./components/NavBar.vue";
|
||||||
import {
|
import {
|
||||||
AUTH_ACCESS_TOKEN,
|
AUTH_ACCESS_TOKEN,
|
||||||
|
@ -52,6 +54,7 @@ import { IConfig } from "./types/config.model";
|
||||||
import { ICurrentUser } from "./types/current-user.model";
|
import { ICurrentUser } from "./types/current-user.model";
|
||||||
import jwt_decode, { JwtPayload } from "jwt-decode";
|
import jwt_decode, { JwtPayload } from "jwt-decode";
|
||||||
import { refreshAccessToken } from "./apollo/utils";
|
import { refreshAccessToken } from "./apollo/utils";
|
||||||
|
import { Route } from "vue-router";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
apollo: {
|
apollo: {
|
||||||
|
@ -82,6 +85,8 @@ export default class App extends Vue {
|
||||||
|
|
||||||
interval: number | undefined = undefined;
|
interval: number | undefined = undefined;
|
||||||
|
|
||||||
|
@Ref("routerView") routerView!: Vue;
|
||||||
|
|
||||||
async created(): Promise<void> {
|
async created(): Promise<void> {
|
||||||
if (await this.initializeCurrentUser()) {
|
if (await this.initializeCurrentUser()) {
|
||||||
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
await initializeCurrentActor(this.$apollo.provider.defaultClient);
|
||||||
|
@ -197,6 +202,39 @@ export default class App extends Vue {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
this.interval = undefined;
|
this.interval = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Watch("$route", { immediate: true })
|
||||||
|
updateAnnouncement(route: Route): void {
|
||||||
|
const pageTitle = this.extractPageTitleFromRoute(route);
|
||||||
|
if (pageTitle) {
|
||||||
|
this.$announcer.polite(
|
||||||
|
this.$t("Navigated to {pageTitle}", {
|
||||||
|
pageTitle,
|
||||||
|
}) as string
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Set the focus to the router view
|
||||||
|
// https://marcus.io/blog/accessible-routing-vuejs
|
||||||
|
setTimeout(() => {
|
||||||
|
const focusTarget = this.routerView.$el as HTMLElement;
|
||||||
|
// Make focustarget programmatically focussable
|
||||||
|
focusTarget.setAttribute("tabindex", "-1");
|
||||||
|
|
||||||
|
// Focus element
|
||||||
|
focusTarget.focus();
|
||||||
|
|
||||||
|
// Remove tabindex from focustarget.
|
||||||
|
// Reason: https://axesslab.com/skip-links/#update-3-a-comment-from-gov-uk
|
||||||
|
focusTarget.removeAttribute("tabindex");
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
extractPageTitleFromRoute(route: Route): string {
|
||||||
|
if (route.meta?.announcer?.message) {
|
||||||
|
return route.meta?.announcer?.message();
|
||||||
|
}
|
||||||
|
return document.title;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -218,4 +256,8 @@ $mdi-font-path: "~@mdi/font/fonts";
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.vue-skip-to {
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -135,3 +135,23 @@ a.list-item {
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@mixin focus() {
|
||||||
|
&:focus {
|
||||||
|
border: 2px solid black;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.menu-list > li,
|
||||||
|
p {
|
||||||
|
@include focus;
|
||||||
|
}
|
||||||
|
.navbar-item {
|
||||||
|
@include focus;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-dropdown span.navbar-item:hover {
|
||||||
|
background-color: whitesmoke;
|
||||||
|
color: #0a0a0a;
|
||||||
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<p class="content">
|
<p class="content">
|
||||||
<span>
|
<span>
|
||||||
{{
|
{{
|
||||||
|
@ -33,7 +33,7 @@
|
||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<p class="content">
|
<p class="content">
|
||||||
{{
|
{{
|
||||||
$t(
|
$t(
|
||||||
|
|
124
js/src/components/Address/AddressInfo.vue
Normal file
124
js/src/components/Address/AddressInfo.vue
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<template>
|
||||||
|
<address>
|
||||||
|
<b-icon
|
||||||
|
v-if="showIcon"
|
||||||
|
:icon="address.poiInfos.poiIcon.icon"
|
||||||
|
size="is-medium"
|
||||||
|
class="icon"
|
||||||
|
/>
|
||||||
|
<p>
|
||||||
|
<span
|
||||||
|
class="addressDescription"
|
||||||
|
:title="address.poiInfos.name"
|
||||||
|
v-if="address.poiInfos.name"
|
||||||
|
>
|
||||||
|
{{ address.poiInfos.name }}
|
||||||
|
</span>
|
||||||
|
<br v-if="address.poiInfos.name" />
|
||||||
|
<span class="has-text-grey-dark">
|
||||||
|
{{ address.poiInfos.alternativeName }}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<small
|
||||||
|
v-if="
|
||||||
|
userTimezoneDifferent &&
|
||||||
|
longShortTimezoneNamesDifferent &&
|
||||||
|
timezoneLongNameValid
|
||||||
|
"
|
||||||
|
class="has-text-grey-dark"
|
||||||
|
>
|
||||||
|
🌐
|
||||||
|
{{
|
||||||
|
$t("{timezoneLongName} ({timezoneShortName})", {
|
||||||
|
timezoneLongName,
|
||||||
|
timezoneShortName,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</small>
|
||||||
|
<small v-else-if="userTimezoneDifferent" class="has-text-grey-dark">
|
||||||
|
🌐 {{ timezoneShortName }}
|
||||||
|
</small>
|
||||||
|
</p>
|
||||||
|
</address>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { IAddress } from "@/types/address.model";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class AddressInfo extends Vue {
|
||||||
|
@Prop({ required: true, type: Object as PropType<IAddress> })
|
||||||
|
address!: IAddress;
|
||||||
|
|
||||||
|
@Prop({ required: false, default: false, type: Boolean }) showIcon!: boolean;
|
||||||
|
@Prop({ required: false, default: false, type: Boolean })
|
||||||
|
showTimezone!: boolean;
|
||||||
|
@Prop({ required: false, type: String }) userTimezone!: string;
|
||||||
|
|
||||||
|
get userTimezoneDifferent(): boolean {
|
||||||
|
return (
|
||||||
|
this.userTimezone != undefined &&
|
||||||
|
this.address.timezone != undefined &&
|
||||||
|
this.userTimezone !== this.address.timezone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get longShortTimezoneNamesDifferent(): boolean {
|
||||||
|
return (
|
||||||
|
this.timezoneLongName != undefined &&
|
||||||
|
this.timezoneShortName != undefined &&
|
||||||
|
this.timezoneLongName !== this.timezoneShortName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezoneLongName(): string | undefined {
|
||||||
|
return this.timezoneName("long");
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezoneShortName(): string | undefined {
|
||||||
|
return this.timezoneName("short");
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezoneLongNameValid(): boolean {
|
||||||
|
return (
|
||||||
|
this.timezoneLongName != undefined && !this.timezoneLongName.match(/UTC/)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private timezoneName(format: "long" | "short"): string | undefined {
|
||||||
|
return this.extractTimezone(
|
||||||
|
new Intl.DateTimeFormat(undefined, {
|
||||||
|
timeZoneName: format,
|
||||||
|
timeZone: this.address.timezone,
|
||||||
|
}).formatToParts()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractTimezone(
|
||||||
|
parts: Intl.DateTimeFormatPart[]
|
||||||
|
): string | undefined {
|
||||||
|
return parts.find((part) => part.type === "timeZoneName")?.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
address {
|
||||||
|
font-style: normal;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
|
||||||
|
span.addressDescription {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 4rem;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.icon {
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -137,6 +137,7 @@
|
||||||
ref="commentEditor"
|
ref="commentEditor"
|
||||||
v-model="newComment.text"
|
v-model="newComment.text"
|
||||||
mode="comment"
|
mode="comment"
|
||||||
|
:aria-label="$t('Comment body')"
|
||||||
/>
|
/>
|
||||||
<b-button
|
<b-button
|
||||||
:disabled="newComment.text.trim().length === 0"
|
:disabled="newComment.text.trim().length === 0"
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
ref="commenteditor"
|
ref="commenteditor"
|
||||||
mode="comment"
|
mode="comment"
|
||||||
v-model="newComment.text"
|
v-model="newComment.text"
|
||||||
|
:aria-label="$t('Comment body')"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<p class="help is-danger" v-if="emptyCommentError">
|
<p class="help is-danger" v-if="emptyCommentError">
|
||||||
|
@ -30,9 +31,11 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="field notify-participants" v-if="isEventOrganiser">
|
<div class="field notify-participants" v-if="isEventOrganiser">
|
||||||
<b-switch v-model="newComment.isAnnouncement">{{
|
<b-switch
|
||||||
$t("Notify participants")
|
aria-labelledby="notify-participants-toggle"
|
||||||
}}</b-switch>
|
v-model="newComment.isAnnouncement"
|
||||||
|
>{{ $t("Notify participants") }}</b-switch
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -88,7 +88,7 @@
|
||||||
{{ $t("[This comment has been deleted by it's author]") }}
|
{{ $t("[This comment has been deleted by it's author]") }}
|
||||||
</div>
|
</div>
|
||||||
<form v-else class="edition" @submit.prevent="updateComment">
|
<form v-else class="edition" @submit.prevent="updateComment">
|
||||||
<editor v-model="updatedComment" />
|
<editor v-model="updatedComment" :aria-label="$t('Comment body')" />
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<b-button
|
<b-button
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
|
|
|
@ -227,6 +227,8 @@ export default class EditorComponent extends Vue {
|
||||||
|
|
||||||
@Prop({ required: false, default: 100_000_000 }) maxSize!: number;
|
@Prop({ required: false, default: 100_000_000 }) maxSize!: number;
|
||||||
|
|
||||||
|
@Prop({ required: false }) ariaLabel!: string;
|
||||||
|
|
||||||
currentActor!: IPerson;
|
currentActor!: IPerson;
|
||||||
|
|
||||||
editor: Editor | null = null;
|
editor: Editor | null = null;
|
||||||
|
@ -256,6 +258,13 @@ export default class EditorComponent extends Vue {
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
this.editor = new Editor({
|
this.editor = new Editor({
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
"aria-multiline": this.isShortMode.toString(),
|
||||||
|
"aria-label": this.ariaLabel,
|
||||||
|
role: "textbox",
|
||||||
|
},
|
||||||
|
},
|
||||||
extensions: [
|
extensions: [
|
||||||
StarterKit,
|
StarterKit,
|
||||||
Document,
|
Document,
|
||||||
|
|
|
@ -103,9 +103,11 @@
|
||||||
:active="copied !== false"
|
:active="copied !== false"
|
||||||
always
|
always
|
||||||
>
|
>
|
||||||
<b-button @click="copyErrorToClipboard">{{
|
<b-button
|
||||||
$t("Copy details to clipboard")
|
@click="copyErrorToClipboard"
|
||||||
}}</b-button>
|
@keyup.enter="copyErrorToClipboard"
|
||||||
|
>{{ $t("Copy details to clipboard") }}</b-button
|
||||||
|
>
|
||||||
</b-tooltip>
|
</b-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
v-if="!gettingLocation"
|
v-if="!gettingLocation"
|
||||||
icon-right="target"
|
icon-right="target"
|
||||||
@click="locateMe"
|
@click="locateMe"
|
||||||
|
@keyup.enter="locateMe"
|
||||||
>{{ $t("Use my location") }}</b-button
|
>{{ $t("Use my location") }}</b-button
|
||||||
>
|
>
|
||||||
<span v-else>{{ $t("Getting location") }}</span>
|
<span v-else>{{ $t("Getting location") }}</span>
|
||||||
|
|
|
@ -18,64 +18,97 @@
|
||||||
</docs>
|
</docs>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<span v-if="!endsOn">{{
|
<p v-if="!endsOn">
|
||||||
beginsOn | formatDateTimeString(showStartTime)
|
<span>{{
|
||||||
}}</span>
|
formatDateTimeString(beginsOn, timezoneToShow, showStartTime)
|
||||||
<span v-else-if="isSameDay() && showStartTime && showEndTime">
|
}}</span>
|
||||||
{{
|
<br />
|
||||||
|
<b-switch
|
||||||
|
size="is-small"
|
||||||
|
v-model="showLocalTimezone"
|
||||||
|
v-if="differentFromUserTimezone"
|
||||||
|
>
|
||||||
|
{{ singleTimeZone }}
|
||||||
|
</b-switch>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="isSameDay() && showStartTime && showEndTime">
|
||||||
|
<span>{{
|
||||||
$t("On {date} from {startTime} to {endTime}", {
|
$t("On {date} from {startTime} to {endTime}", {
|
||||||
date: formatDate(beginsOn),
|
date: formatDate(beginsOn),
|
||||||
startTime: formatTime(beginsOn),
|
startTime: formatTime(beginsOn, timezoneToShow),
|
||||||
endTime: formatTime(endsOn),
|
endTime: formatTime(endsOn, timezoneToShow),
|
||||||
})
|
})
|
||||||
}}
|
}}</span>
|
||||||
</span>
|
<br />
|
||||||
<span v-else-if="isSameDay() && !showStartTime && showEndTime">
|
<b-switch
|
||||||
{{
|
size="is-small"
|
||||||
$t("On {date} ending at {endTime}", {
|
v-model="showLocalTimezone"
|
||||||
date: formatDate(beginsOn),
|
v-if="differentFromUserTimezone"
|
||||||
endTime: formatTime(endsOn),
|
>
|
||||||
})
|
{{ singleTimeZone }}
|
||||||
}}
|
</b-switch>
|
||||||
</span>
|
</p>
|
||||||
<span v-else-if="isSameDay() && showStartTime && !showEndTime">
|
<p v-else-if="isSameDay() && showStartTime && !showEndTime">
|
||||||
{{
|
{{
|
||||||
$t("On {date} starting at {startTime}", {
|
$t("On {date} starting at {startTime}", {
|
||||||
date: formatDate(beginsOn),
|
date: formatDate(beginsOn),
|
||||||
startTime: formatTime(beginsOn),
|
startTime: formatTime(beginsOn),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</p>
|
||||||
<span v-else-if="isSameDay()">{{
|
<p v-else-if="isSameDay()">
|
||||||
$t("On {date}", { date: formatDate(beginsOn) })
|
{{ $t("On {date}", { date: formatDate(beginsOn) }) }}
|
||||||
}}</span>
|
</p>
|
||||||
<span v-else-if="endsOn && showStartTime && showEndTime">
|
<p v-else-if="endsOn && showStartTime && showEndTime">
|
||||||
{{
|
<span>
|
||||||
$t("From the {startDate} at {startTime} to the {endDate} at {endTime}", {
|
{{
|
||||||
startDate: formatDate(beginsOn),
|
$t(
|
||||||
startTime: formatTime(beginsOn),
|
"From the {startDate} at {startTime} to the {endDate} at {endTime}",
|
||||||
endDate: formatDate(endsOn),
|
{
|
||||||
endTime: formatTime(endsOn),
|
startDate: formatDate(beginsOn),
|
||||||
})
|
startTime: formatTime(beginsOn, timezoneToShow),
|
||||||
}}
|
endDate: formatDate(endsOn),
|
||||||
</span>
|
endTime: formatTime(endsOn, timezoneToShow),
|
||||||
<span v-else-if="endsOn && showStartTime">
|
}
|
||||||
{{
|
)
|
||||||
$t("From the {startDate} at {startTime} to the {endDate}", {
|
}}
|
||||||
startDate: formatDate(beginsOn),
|
</span>
|
||||||
startTime: formatTime(beginsOn),
|
<br />
|
||||||
endDate: formatDate(endsOn),
|
<b-switch
|
||||||
})
|
size="is-small"
|
||||||
}}
|
v-model="showLocalTimezone"
|
||||||
</span>
|
v-if="differentFromUserTimezone"
|
||||||
<span v-else-if="endsOn">
|
>
|
||||||
|
{{ multipleTimeZones }}
|
||||||
|
</b-switch>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="endsOn && showStartTime">
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
$t("From the {startDate} at {startTime} to the {endDate}", {
|
||||||
|
startDate: formatDate(beginsOn),
|
||||||
|
startTime: formatTime(beginsOn, timezoneToShow),
|
||||||
|
endDate: formatDate(endsOn),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<b-switch
|
||||||
|
size="is-small"
|
||||||
|
v-model="showLocalTimezone"
|
||||||
|
v-if="differentFromUserTimezone"
|
||||||
|
>
|
||||||
|
{{ singleTimeZone }}
|
||||||
|
</b-switch>
|
||||||
|
</p>
|
||||||
|
<p v-else-if="endsOn">
|
||||||
{{
|
{{
|
||||||
$t("From the {startDate} to the {endDate}", {
|
$t("From the {startDate} to the {endDate}", {
|
||||||
startDate: formatDate(beginsOn),
|
startDate: formatDate(beginsOn),
|
||||||
endDate: formatDate(endsOn),
|
endDate: formatDate(endsOn),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
</span>
|
</p>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
@ -90,14 +123,47 @@ export default class EventFullDate extends Vue {
|
||||||
|
|
||||||
@Prop({ required: false, default: true }) showEndTime!: boolean;
|
@Prop({ required: false, default: true }) showEndTime!: boolean;
|
||||||
|
|
||||||
|
@Prop({ required: false }) timezone!: string;
|
||||||
|
|
||||||
|
@Prop({ required: false }) userTimezone!: string;
|
||||||
|
|
||||||
|
showLocalTimezone = true;
|
||||||
|
|
||||||
|
get timezoneToShow(): string {
|
||||||
|
if (this.showLocalTimezone) {
|
||||||
|
return this.timezone;
|
||||||
|
}
|
||||||
|
return this.userActualTimezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userActualTimezone(): string {
|
||||||
|
if (this.userTimezone) {
|
||||||
|
return this.userTimezone;
|
||||||
|
}
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
formatDate(value: Date): string | undefined {
|
formatDate(value: Date): string | undefined {
|
||||||
if (!this.$options.filters) return undefined;
|
if (!this.$options.filters) return undefined;
|
||||||
return this.$options.filters.formatDateString(value);
|
return this.$options.filters.formatDateString(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
formatTime(value: Date): string | undefined {
|
formatTime(value: Date, timezone: string): string | undefined {
|
||||||
if (!this.$options.filters) return undefined;
|
if (!this.$options.filters) return undefined;
|
||||||
return this.$options.filters.formatTimeString(value);
|
return this.$options.filters.formatTimeString(value, timezone || undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatDateTimeString(
|
||||||
|
value: Date,
|
||||||
|
timezone: string,
|
||||||
|
showTime: boolean
|
||||||
|
): string | undefined {
|
||||||
|
if (!this.$options.filters) return undefined;
|
||||||
|
return this.$options.filters.formatDateTimeString(
|
||||||
|
value,
|
||||||
|
timezone,
|
||||||
|
showTime
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
isSameDay(): boolean {
|
isSameDay(): boolean {
|
||||||
|
@ -106,5 +172,35 @@ export default class EventFullDate extends Vue {
|
||||||
new Date(this.endsOn).toDateString();
|
new Date(this.endsOn).toDateString();
|
||||||
return this.endsOn !== undefined && sameDay;
|
return this.endsOn !== undefined && sameDay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get differentFromUserTimezone(): boolean {
|
||||||
|
return (
|
||||||
|
!!this.timezone &&
|
||||||
|
!!this.userActualTimezone &&
|
||||||
|
this.timezone !== this.userActualTimezone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get singleTimeZone(): string {
|
||||||
|
if (this.showLocalTimezone) {
|
||||||
|
return this.$t("Local time ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
|
return this.$t("Time in your timezone ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
get multipleTimeZones(): string {
|
||||||
|
if (this.showLocalTimezone) {
|
||||||
|
return this.$t("Local time ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
|
return this.$t("Times in your timezone ({timezone})", {
|
||||||
|
timezone: this.timezoneToShow,
|
||||||
|
}) as string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
175
js/src/components/Event/EventMap.vue
Normal file
175
js/src/components/Event/EventMap.vue
Normal file
|
@ -0,0 +1,175 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal-card">
|
||||||
|
<header class="modal-card-head">
|
||||||
|
<button type="button" class="delete" @click="$emit('close')" />
|
||||||
|
</header>
|
||||||
|
<div class="modal-card-body">
|
||||||
|
<section class="map">
|
||||||
|
<map-leaflet
|
||||||
|
:coords="physicalAddress.geom"
|
||||||
|
:marker="{
|
||||||
|
text: physicalAddress.fullName,
|
||||||
|
icon: physicalAddress.poiInfos.poiIcon.icon,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<section class="columns is-centered map-footer">
|
||||||
|
<div class="column is-half has-text-centered">
|
||||||
|
<p class="address">
|
||||||
|
<i class="mdi mdi-map-marker"></i>
|
||||||
|
{{ physicalAddress.fullName }}
|
||||||
|
</p>
|
||||||
|
<p class="getting-there">{{ $t("Getting there") }}</p>
|
||||||
|
<div
|
||||||
|
class="buttons"
|
||||||
|
v-if="
|
||||||
|
addressLinkToRouteByCar ||
|
||||||
|
addressLinkToRouteByBike ||
|
||||||
|
addressLinkToRouteByFeet
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByFeet"
|
||||||
|
:href="addressLinkToRouteByFeet"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-walk"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByBike"
|
||||||
|
:href="addressLinkToRouteByBike"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-bike"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByTransit"
|
||||||
|
:href="addressLinkToRouteByTransit"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-bus"></i
|
||||||
|
></a>
|
||||||
|
<a
|
||||||
|
class="button"
|
||||||
|
target="_blank"
|
||||||
|
v-if="addressLinkToRouteByCar"
|
||||||
|
:href="addressLinkToRouteByCar"
|
||||||
|
>
|
||||||
|
<i class="mdi mdi-car"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { Address, IAddress } from "@/types/address.model";
|
||||||
|
import { RoutingTransportationType, RoutingType } from "@/types/enums";
|
||||||
|
import { PropType } from "vue";
|
||||||
|
import { Component, Vue, Prop } from "vue-property-decorator";
|
||||||
|
|
||||||
|
const RoutingParamType = {
|
||||||
|
[RoutingType.OPENSTREETMAP]: {
|
||||||
|
[RoutingTransportationType.FOOT]: "engine=fossgis_osrm_foot",
|
||||||
|
[RoutingTransportationType.BIKE]: "engine=fossgis_osrm_bike",
|
||||||
|
[RoutingTransportationType.TRANSIT]: null,
|
||||||
|
[RoutingTransportationType.CAR]: "engine=fossgis_osrm_car",
|
||||||
|
},
|
||||||
|
[RoutingType.GOOGLE_MAPS]: {
|
||||||
|
[RoutingTransportationType.FOOT]: "dirflg=w",
|
||||||
|
[RoutingTransportationType.BIKE]: "dirflg=b",
|
||||||
|
[RoutingTransportationType.TRANSIT]: "dirflg=r",
|
||||||
|
[RoutingTransportationType.CAR]: "driving",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
"map-leaflet": () =>
|
||||||
|
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class EventMap extends Vue {
|
||||||
|
@Prop({ type: Object as PropType<IAddress> }) address!: IAddress;
|
||||||
|
@Prop({ type: String }) routingType!: RoutingType;
|
||||||
|
|
||||||
|
get physicalAddress(): Address | null {
|
||||||
|
if (!this.address) return null;
|
||||||
|
|
||||||
|
return new Address(this.address);
|
||||||
|
}
|
||||||
|
|
||||||
|
makeNavigationPath(
|
||||||
|
transportationType: RoutingTransportationType
|
||||||
|
): string | undefined {
|
||||||
|
const geometry = this.physicalAddress?.geom;
|
||||||
|
if (geometry) {
|
||||||
|
/**
|
||||||
|
* build urls to routing map
|
||||||
|
*/
|
||||||
|
if (!RoutingParamType[this.routingType][transportationType]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlGeometry = geometry.split(";").reverse().join(",");
|
||||||
|
|
||||||
|
switch (this.routingType) {
|
||||||
|
case RoutingType.GOOGLE_MAPS:
|
||||||
|
return `https://maps.google.com/?saddr=Current+Location&daddr=${urlGeometry}&${
|
||||||
|
RoutingParamType[this.routingType][transportationType]
|
||||||
|
}`;
|
||||||
|
case RoutingType.OPENSTREETMAP:
|
||||||
|
default: {
|
||||||
|
const bboxX = geometry.split(";").reverse()[0];
|
||||||
|
const bboxY = geometry.split(";").reverse()[1];
|
||||||
|
return `https://www.openstreetmap.org/directions?from=&to=${urlGeometry}&${
|
||||||
|
RoutingParamType[this.routingType][transportationType]
|
||||||
|
}#map=14/${bboxX}/${bboxY}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByCar(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.CAR);
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByBike(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.BIKE);
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByFeet(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.FOOT);
|
||||||
|
}
|
||||||
|
|
||||||
|
get addressLinkToRouteByTransit(): undefined | string {
|
||||||
|
return this.makeNavigationPath(RoutingTransportationType.TRANSIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.modal-card-head {
|
||||||
|
justify-content: flex-end;
|
||||||
|
button.delete {
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section.map {
|
||||||
|
height: calc(100% - 8rem);
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
section.map-footer {
|
||||||
|
p.address {
|
||||||
|
margin: 1rem auto;
|
||||||
|
}
|
||||||
|
div.buttons {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -7,25 +7,15 @@
|
||||||
<div class="address-wrapper">
|
<div class="address-wrapper">
|
||||||
<span v-if="!physicalAddress">{{ $t("No address defined") }}</span>
|
<span v-if="!physicalAddress">{{ $t("No address defined") }}</span>
|
||||||
<div class="address" v-if="physicalAddress">
|
<div class="address" v-if="physicalAddress">
|
||||||
<div>
|
<address-info :address="physicalAddress" />
|
||||||
<address>
|
<b-button
|
||||||
<p
|
type="is-text"
|
||||||
class="addressDescription"
|
|
||||||
:title="physicalAddress.poiInfos.name"
|
|
||||||
>
|
|
||||||
{{ physicalAddress.poiInfos.name }}
|
|
||||||
</p>
|
|
||||||
<p class="has-text-grey-dark">
|
|
||||||
{{ physicalAddress.poiInfos.alternativeName }}
|
|
||||||
</p>
|
|
||||||
</address>
|
|
||||||
</div>
|
|
||||||
<span
|
|
||||||
class="map-show-button"
|
class="map-show-button"
|
||||||
@click="showMap = !showMap"
|
@click="$emit('showMapModal', true)"
|
||||||
v-if="physicalAddress.geom"
|
v-if="physicalAddress.geom"
|
||||||
>{{ $t("Show map") }}</span
|
|
||||||
>
|
>
|
||||||
|
{{ $t("Show map") }}
|
||||||
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</event-metadata-block>
|
</event-metadata-block>
|
||||||
|
@ -34,6 +24,8 @@
|
||||||
:beginsOn="event.beginsOn"
|
:beginsOn="event.beginsOn"
|
||||||
:show-start-time="event.options.showStartTime"
|
:show-start-time="event.options.showStartTime"
|
||||||
:show-end-time="event.options.showEndTime"
|
:show-end-time="event.options.showEndTime"
|
||||||
|
:timezone="event.options.timezone"
|
||||||
|
:userTimezone="userTimezone"
|
||||||
:endsOn="event.endsOn"
|
:endsOn="event.endsOn"
|
||||||
/>
|
/>
|
||||||
</event-metadata-block>
|
</event-metadata-block>
|
||||||
|
@ -140,91 +132,12 @@
|
||||||
>
|
>
|
||||||
<span v-else>{{ extra.value }}</span>
|
<span v-else>{{ extra.value }}</span>
|
||||||
</event-metadata-block>
|
</event-metadata-block>
|
||||||
<b-modal
|
|
||||||
class="map-modal"
|
|
||||||
v-if="physicalAddress && physicalAddress.geom"
|
|
||||||
:active.sync="showMap"
|
|
||||||
has-modal-card
|
|
||||||
full-screen
|
|
||||||
>
|
|
||||||
<div class="modal-card">
|
|
||||||
<header class="modal-card-head">
|
|
||||||
<button type="button" class="delete" @click="showMap = false" />
|
|
||||||
</header>
|
|
||||||
<div class="modal-card-body">
|
|
||||||
<section class="map">
|
|
||||||
<map-leaflet
|
|
||||||
:coords="physicalAddress.geom"
|
|
||||||
:marker="{
|
|
||||||
text: physicalAddress.fullName,
|
|
||||||
icon: physicalAddress.poiInfos.poiIcon.icon,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section class="columns is-centered map-footer">
|
|
||||||
<div class="column is-half has-text-centered">
|
|
||||||
<p class="address">
|
|
||||||
<i class="mdi mdi-map-marker"></i>
|
|
||||||
{{ physicalAddress.fullName }}
|
|
||||||
</p>
|
|
||||||
<p class="getting-there">{{ $t("Getting there") }}</p>
|
|
||||||
<div
|
|
||||||
class="buttons"
|
|
||||||
v-if="
|
|
||||||
addressLinkToRouteByCar ||
|
|
||||||
addressLinkToRouteByBike ||
|
|
||||||
addressLinkToRouteByFeet
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByFeet"
|
|
||||||
:href="addressLinkToRouteByFeet"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-walk"></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByBike"
|
|
||||||
:href="addressLinkToRouteByBike"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-bike"></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByTransit"
|
|
||||||
:href="addressLinkToRouteByTransit"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-bus"></i
|
|
||||||
></a>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
target="_blank"
|
|
||||||
v-if="addressLinkToRouteByCar"
|
|
||||||
:href="addressLinkToRouteByCar"
|
|
||||||
>
|
|
||||||
<i class="mdi mdi-car"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</b-modal>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Address } from "@/types/address.model";
|
import { Address } from "@/types/address.model";
|
||||||
import { IConfig } from "@/types/config.model";
|
import { IConfig } from "@/types/config.model";
|
||||||
import {
|
import { EventMetadataKeyType, EventMetadataType } from "@/types/enums";
|
||||||
EventMetadataKeyType,
|
|
||||||
EventMetadataType,
|
|
||||||
RoutingTransportationType,
|
|
||||||
RoutingType,
|
|
||||||
} from "@/types/enums";
|
|
||||||
import { IEvent } from "@/types/event.model";
|
import { IEvent } from "@/types/event.model";
|
||||||
import { PropType } from "vue";
|
import { PropType } from "vue";
|
||||||
import { Component, Prop, Vue } from "vue-property-decorator";
|
import { Component, Prop, Vue } from "vue-property-decorator";
|
||||||
|
@ -234,11 +147,13 @@ import EventMetadataBlock from "./EventMetadataBlock.vue";
|
||||||
import EventFullDate from "./EventFullDate.vue";
|
import EventFullDate from "./EventFullDate.vue";
|
||||||
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
import PopoverActorCard from "../Account/PopoverActorCard.vue";
|
||||||
import ActorCard from "../../components/Account/ActorCard.vue";
|
import ActorCard from "../../components/Account/ActorCard.vue";
|
||||||
|
import AddressInfo from "../../components/Address/AddressInfo.vue";
|
||||||
import {
|
import {
|
||||||
IEventMetadata,
|
IEventMetadata,
|
||||||
IEventMetadataDescription,
|
IEventMetadataDescription,
|
||||||
} from "@/types/event-metadata";
|
} from "@/types/event-metadata";
|
||||||
import { eventMetaDataList } from "../../services/EventMetadata";
|
import { eventMetaDataList } from "../../services/EventMetadata";
|
||||||
|
import { IUser } from "@/types/current-user.model";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: {
|
components: {
|
||||||
|
@ -246,15 +161,14 @@ import { eventMetaDataList } from "../../services/EventMetadata";
|
||||||
EventFullDate,
|
EventFullDate,
|
||||||
PopoverActorCard,
|
PopoverActorCard,
|
||||||
ActorCard,
|
ActorCard,
|
||||||
"map-leaflet": () =>
|
AddressInfo,
|
||||||
import(/* webpackChunkName: "map" */ "../../components/Map.vue"),
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class EventMetadataSidebar extends Vue {
|
export default class EventMetadataSidebar extends Vue {
|
||||||
@Prop({ type: Object as PropType<IEvent>, required: true }) event!: IEvent;
|
@Prop({ type: Object as PropType<IEvent>, required: true }) event!: IEvent;
|
||||||
@Prop({ type: Object as PropType<IConfig>, required: true }) config!: IConfig;
|
@Prop({ type: Object as PropType<IConfig>, required: true }) config!: IConfig;
|
||||||
|
@Prop({ required: true }) user!: IUser | undefined;
|
||||||
showMap = false;
|
@Prop({ required: false, default: false }) showMap!: boolean;
|
||||||
|
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
||||||
|
@ -265,21 +179,6 @@ export default class EventMetadataSidebar extends Vue {
|
||||||
EventMetadataType = EventMetadataType;
|
EventMetadataType = EventMetadataType;
|
||||||
EventMetadataKeyType = EventMetadataKeyType;
|
EventMetadataKeyType = EventMetadataKeyType;
|
||||||
|
|
||||||
RoutingParamType = {
|
|
||||||
[RoutingType.OPENSTREETMAP]: {
|
|
||||||
[RoutingTransportationType.FOOT]: "engine=fossgis_osrm_foot",
|
|
||||||
[RoutingTransportationType.BIKE]: "engine=fossgis_osrm_bike",
|
|
||||||
[RoutingTransportationType.TRANSIT]: null,
|
|
||||||
[RoutingTransportationType.CAR]: "engine=fossgis_osrm_car",
|
|
||||||
},
|
|
||||||
[RoutingType.GOOGLE_MAPS]: {
|
|
||||||
[RoutingTransportationType.FOOT]: "dirflg=w",
|
|
||||||
[RoutingTransportationType.BIKE]: "dirflg=b",
|
|
||||||
[RoutingTransportationType.TRANSIT]: "dirflg=r",
|
|
||||||
[RoutingTransportationType.CAR]: "driving",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
get physicalAddress(): Address | null {
|
get physicalAddress(): Address | null {
|
||||||
if (!this.event.physicalAddress) return null;
|
if (!this.event.physicalAddress) return null;
|
||||||
|
|
||||||
|
@ -296,50 +195,6 @@ export default class EventMetadataSidebar extends Vue {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
makeNavigationPath(
|
|
||||||
transportationType: RoutingTransportationType
|
|
||||||
): string | undefined {
|
|
||||||
const geometry = this.physicalAddress?.geom;
|
|
||||||
if (geometry) {
|
|
||||||
const routingType = this.config.maps.routing.type;
|
|
||||||
/**
|
|
||||||
* build urls to routing map
|
|
||||||
*/
|
|
||||||
if (!this.RoutingParamType[routingType][transportationType]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlGeometry = geometry.split(";").reverse().join(",");
|
|
||||||
|
|
||||||
switch (routingType) {
|
|
||||||
case RoutingType.GOOGLE_MAPS:
|
|
||||||
return `https://maps.google.com/?saddr=Current+Location&daddr=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}`;
|
|
||||||
case RoutingType.OPENSTREETMAP:
|
|
||||||
default: {
|
|
||||||
const bboxX = geometry.split(";").reverse()[0];
|
|
||||||
const bboxY = geometry.split(";").reverse()[1];
|
|
||||||
return `https://www.openstreetmap.org/directions?from=&to=${urlGeometry}&${this.RoutingParamType[routingType][transportationType]}#map=14/${bboxX}/${bboxY}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByCar(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.CAR);
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByBike(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.BIKE);
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByFeet(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.FOOT);
|
|
||||||
}
|
|
||||||
|
|
||||||
get addressLinkToRouteByTransit(): undefined | string {
|
|
||||||
return this.makeNavigationPath(RoutingTransportationType.TRANSIT);
|
|
||||||
}
|
|
||||||
|
|
||||||
urlToHostname(url: string): string | null {
|
urlToHostname(url: string): string | null {
|
||||||
try {
|
try {
|
||||||
return new URL(url).hostname;
|
return new URL(url).hostname;
|
||||||
|
@ -372,6 +227,10 @@ export default class EventMetadataSidebar extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get userTimezone(): string | undefined {
|
||||||
|
return this.user?.settings?.timezone;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -401,50 +260,6 @@ div.address-wrapper {
|
||||||
.map-show-button {
|
.map-show-button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
address {
|
|
||||||
font-style: normal;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-start;
|
|
||||||
|
|
||||||
span.addressDescription {
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
flex: 1 0 auto;
|
|
||||||
min-width: 100%;
|
|
||||||
max-width: 4rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
:not(.addressDescription) {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-modal {
|
|
||||||
.modal-card-head {
|
|
||||||
justify-content: flex-end;
|
|
||||||
button.delete {
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section.map {
|
|
||||||
height: calc(100% - 8rem);
|
|
||||||
width: calc(100% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
section.map-footer {
|
|
||||||
p.address {
|
|
||||||
margin: 1rem auto;
|
|
||||||
}
|
|
||||||
div.buttons {
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,72 +1,89 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="address-autocomplete">
|
<div class="address-autocomplete columns is-desktop">
|
||||||
<b-field
|
<div class="column">
|
||||||
:label-for="id"
|
<b-field
|
||||||
expanded
|
:label-for="id"
|
||||||
:message="fieldErrors"
|
|
||||||
:type="{ 'is-danger': fieldErrors.length }"
|
|
||||||
>
|
|
||||||
<template slot="label">
|
|
||||||
{{ actualLabel }}
|
|
||||||
<b-button
|
|
||||||
v-if="canShowLocateMeButton && !gettingLocation"
|
|
||||||
size="is-small"
|
|
||||||
icon-right="map-marker"
|
|
||||||
@click="locateMe"
|
|
||||||
:title="$t('Use my location')"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="is-size-6 has-text-weight-normal"
|
|
||||||
v-else-if="gettingLocation"
|
|
||||||
>{{ $t("Getting location") }}</span
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<b-autocomplete
|
|
||||||
:data="addressData"
|
|
||||||
v-model="queryText"
|
|
||||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
|
||||||
field="fullName"
|
|
||||||
:loading="isFetching"
|
|
||||||
@typing="fetchAsyncData"
|
|
||||||
icon="map-marker"
|
|
||||||
expanded
|
expanded
|
||||||
@select="updateSelected"
|
:message="fieldErrors"
|
||||||
v-bind="$attrs"
|
:type="{ 'is-danger': fieldErrors.length }"
|
||||||
:id="id"
|
|
||||||
>
|
>
|
||||||
<template #default="{ option }">
|
<template slot="label">
|
||||||
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
{{ actualLabel }}
|
||||||
<b>{{ option.poiInfos.name }}</b
|
<b-button
|
||||||
><br />
|
v-if="canShowLocateMeButton && !gettingLocation"
|
||||||
<small>{{ option.poiInfos.alternativeName }}</small>
|
size="is-small"
|
||||||
|
icon-right="map-marker"
|
||||||
|
@click="locateMe"
|
||||||
|
:title="$t('Use my location')"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="is-size-6 has-text-weight-normal"
|
||||||
|
v-else-if="gettingLocation"
|
||||||
|
>{{ $t("Getting location") }}</span
|
||||||
|
>
|
||||||
</template>
|
</template>
|
||||||
<template slot="empty">
|
<b-autocomplete
|
||||||
<span v-if="isFetching">{{ $t("Searching…") }}</span>
|
:data="addressData"
|
||||||
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
v-model="queryText"
|
||||||
<span>{{ $t('No results for "{queryText}"', { queryText }) }}</span>
|
:placeholder="$t('e.g. 10 Rue Jangot')"
|
||||||
<span>{{
|
field="fullName"
|
||||||
$t(
|
:loading="isFetching"
|
||||||
"You can try another search term or drag and drop the marker on the map",
|
@typing="fetchAsyncData"
|
||||||
{
|
icon="map-marker"
|
||||||
queryText,
|
expanded
|
||||||
}
|
@select="updateSelected"
|
||||||
)
|
v-bind="$attrs"
|
||||||
}}</span>
|
:id="id"
|
||||||
<!-- <p class="control" @click="openNewAddressModal">-->
|
>
|
||||||
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
<template #default="{ option }">
|
||||||
<!-- </p>-->
|
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||||
</div>
|
<b>{{ option.poiInfos.name }}</b
|
||||||
</template>
|
><br />
|
||||||
</b-autocomplete>
|
<small>{{ option.poiInfos.alternativeName }}</small>
|
||||||
<b-button
|
</template>
|
||||||
:disabled="!queryText"
|
<template slot="empty">
|
||||||
@click="resetAddress"
|
<span v-if="isFetching">{{ $t("Searching…") }}</span>
|
||||||
class="reset-area"
|
<div v-else-if="queryText.length >= 3" class="is-enabled">
|
||||||
icon-left="close"
|
<span>{{
|
||||||
:title="$t('Clear address field')"
|
$t('No results for "{queryText}"', { queryText })
|
||||||
/>
|
}}</span>
|
||||||
</b-field>
|
<span>{{
|
||||||
<div class="map" v-if="selected && selected.geom && selected.poiInfos">
|
$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>-->
|
||||||
|
<!-- </p>-->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</b-autocomplete>
|
||||||
|
<b-button
|
||||||
|
:disabled="!queryText"
|
||||||
|
@click="resetAddress"
|
||||||
|
class="reset-area"
|
||||||
|
icon-left="close"
|
||||||
|
:title="$t('Clear address field')"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
|
<div class="card" v-if="selected.originId || selected.url">
|
||||||
|
<div class="card-content">
|
||||||
|
<address-info
|
||||||
|
:address="selected"
|
||||||
|
:show-icon="true"
|
||||||
|
:show-timezone="true"
|
||||||
|
:user-timezone="userTimezone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="map column"
|
||||||
|
v-if="selected && selected.geom && selected.poiInfos"
|
||||||
|
>
|
||||||
<map-leaflet
|
<map-leaflet
|
||||||
:coords="selected.geom"
|
:coords="selected.geom"
|
||||||
:marker="{
|
:marker="{
|
||||||
|
@ -126,14 +143,19 @@ import { Component, Prop, Watch, Mixins } from "vue-property-decorator";
|
||||||
import { LatLng } from "leaflet";
|
import { LatLng } from "leaflet";
|
||||||
import { Address, IAddress } from "../../types/address.model";
|
import { Address, IAddress } from "../../types/address.model";
|
||||||
import AddressAutoCompleteMixin from "../../mixins/AddressAutoCompleteMixin";
|
import AddressAutoCompleteMixin from "../../mixins/AddressAutoCompleteMixin";
|
||||||
|
import AddressInfo from "../../components/Address/AddressInfo.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
|
components: {
|
||||||
|
AddressInfo,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class FullAddressAutoComplete extends Mixins(
|
export default class FullAddressAutoComplete extends Mixins(
|
||||||
AddressAutoCompleteMixin
|
AddressAutoCompleteMixin
|
||||||
) {
|
) {
|
||||||
@Prop({ required: false, default: "" }) label!: string;
|
@Prop({ required: false, default: "" }) label!: string;
|
||||||
|
@Prop({ required: false }) userTimezone!: string;
|
||||||
|
|
||||||
addressModalActive = false;
|
addressModalActive = false;
|
||||||
|
|
||||||
|
|
|
@ -30,18 +30,22 @@ A button to set your participation
|
||||||
position="is-bottom-left"
|
position="is-bottom-left"
|
||||||
v-if="participation && participation.role === ParticipantRole.PARTICIPANT"
|
v-if="participation && participation.role === ParticipantRole.PARTICIPANT"
|
||||||
>
|
>
|
||||||
<button class="button is-success is-large" type="button" slot="trigger">
|
<template #trigger="{ active }">
|
||||||
<b-icon icon="check" />
|
<b-button
|
||||||
<template>
|
type="is-success"
|
||||||
<span>{{ $t("I participate") }}</span>
|
size="is-large"
|
||||||
</template>
|
icon-left="check"
|
||||||
<b-icon icon="menu-down" />
|
:icon-right="active ? 'menu-up' : 'menu-down'"
|
||||||
</button>
|
>
|
||||||
|
{{ $t("I participate") }}
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
:value="false"
|
:value="false"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="confirmLeave"
|
@click="confirmLeave"
|
||||||
|
@keyup.enter="confirmLeave"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
>{{ $t("Cancel my participation…") }}</b-dropdown-item
|
>{{ $t("Cancel my participation…") }}</b-dropdown-item
|
||||||
>
|
>
|
||||||
|
@ -73,6 +77,7 @@ A button to set your participation
|
||||||
:value="false"
|
:value="false"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="confirmLeave"
|
@click="confirmLeave"
|
||||||
|
@keyup.enter="confirmLeave"
|
||||||
class="has-text-danger"
|
class="has-text-danger"
|
||||||
>{{ $t("Cancel my participation request…") }}</b-dropdown-item
|
>{{ $t("Cancel my participation request…") }}</b-dropdown-item
|
||||||
>
|
>
|
||||||
|
@ -101,17 +106,21 @@ A button to set your participation
|
||||||
position="is-bottom-left"
|
position="is-bottom-left"
|
||||||
v-else-if="!participation && currentActor.id"
|
v-else-if="!participation && currentActor.id"
|
||||||
>
|
>
|
||||||
<button class="button is-primary is-large" type="button" slot="trigger">
|
<template #trigger="{ active }">
|
||||||
<template>
|
<b-button
|
||||||
<span>{{ $t("Participate") }}</span>
|
type="is-primary"
|
||||||
</template>
|
size="is-large"
|
||||||
<b-icon icon="menu-down" />
|
:icon-right="active ? 'menu-up' : 'menu-down'"
|
||||||
</button>
|
>
|
||||||
|
{{ $t("Participate") }}
|
||||||
|
</b-button>
|
||||||
|
</template>
|
||||||
|
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
:value="true"
|
:value="true"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="joinEvent(currentActor)"
|
@click="joinEvent(currentActor)"
|
||||||
|
@keyup.enter="joinEvent(currentActor)"
|
||||||
>
|
>
|
||||||
<div class="media">
|
<div class="media">
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
|
@ -136,6 +145,7 @@ A button to set your participation
|
||||||
:value="false"
|
:value="false"
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="joinModal"
|
@click="joinModal"
|
||||||
|
@keyup.enter="joinModal"
|
||||||
v-if="identities.length > 1"
|
v-if="identities.length > 1"
|
||||||
>{{ $t("with another identity…") }}</b-dropdown-item
|
>{{ $t("with another identity…") }}</b-dropdown-item
|
||||||
>
|
>
|
||||||
|
|
|
@ -25,7 +25,12 @@
|
||||||
v-model="locale"
|
v-model="locale"
|
||||||
:placeholder="$t('Select a language')"
|
:placeholder="$t('Select a language')"
|
||||||
>
|
>
|
||||||
<option v-for="(language, lang) in langs" :value="lang" :key="lang">
|
<option
|
||||||
|
v-for="(language, lang) in langs"
|
||||||
|
:value="lang"
|
||||||
|
:key="lang"
|
||||||
|
:selected="isLangSelected(lang)"
|
||||||
|
>
|
||||||
{{ language }}
|
{{ language }}
|
||||||
</option>
|
</option>
|
||||||
</b-select>
|
</b-select>
|
||||||
|
@ -48,6 +53,9 @@
|
||||||
{{ $t("License") }}
|
{{ $t("License") }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#navbar">{{ $t("Back to top") }}</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="content has-text-centered">
|
<div class="content has-text-centered">
|
||||||
<i18n
|
<i18n
|
||||||
|
@ -101,6 +109,10 @@ export default class Footer extends Vue {
|
||||||
this.locale = locale;
|
this.locale = locale;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isLangSelected(lang: string): boolean {
|
||||||
|
return lang === this.locale;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
@ -145,6 +157,13 @@ footer.footer {
|
||||||
color: $white;
|
color: $white;
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-color: $secondary;
|
text-decoration-color: $secondary;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
outline: 3px solid #000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep span.select {
|
::v-deep span.select {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<b-navbar
|
<b-navbar
|
||||||
|
id="navbar"
|
||||||
type="is-secondary"
|
type="is-secondary"
|
||||||
wrapper-class="container"
|
wrapper-class="container"
|
||||||
:active.sync="mobileNavbarActive"
|
:active.sync="mobileNavbarActive"
|
||||||
|
@ -58,6 +59,7 @@
|
||||||
href="https://mediation.koena.net/framasoft/mobilizon/"
|
href="https://mediation.koena.net/framasoft/mobilizon/"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
|
hreflang="fr"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/img/koena-a11y.svg"
|
src="/img/koena-a11y.svg"
|
||||||
|
@ -75,6 +77,10 @@
|
||||||
v-if="currentActor.id && currentUser.isLoggedIn"
|
v-if="currentActor.id && currentUser.isLoggedIn"
|
||||||
right
|
right
|
||||||
collapsible
|
collapsible
|
||||||
|
ref="user-dropdown"
|
||||||
|
tabindex="0"
|
||||||
|
tag="span"
|
||||||
|
@keyup.enter="toggleMenu"
|
||||||
>
|
>
|
||||||
<template
|
<template
|
||||||
slot="label"
|
slot="label"
|
||||||
|
@ -109,8 +115,11 @@
|
||||||
v-else
|
v-else
|
||||||
:active="identity.id === currentActor.id"
|
:active="identity.id === currentActor.id"
|
||||||
:key="identity.id"
|
:key="identity.id"
|
||||||
|
tabindex="0"
|
||||||
|
@click="setIdentity(identity)"
|
||||||
|
@keyup.enter="setIdentity(identity)"
|
||||||
>
|
>
|
||||||
<span @click="setIdentity(identity)">
|
<span>
|
||||||
<div class="media-left">
|
<div class="media-left">
|
||||||
<figure class="image is-32x32" v-if="identity.avatar">
|
<figure class="image is-32x32" v-if="identity.avatar">
|
||||||
<img
|
<img
|
||||||
|
@ -131,7 +140,7 @@
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<hr class="navbar-divider" />
|
<hr class="navbar-divider" role="presentation" />
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
|
|
||||||
<b-navbar-item
|
<b-navbar-item
|
||||||
|
@ -146,8 +155,13 @@
|
||||||
>{{ $t("Administration") }}</b-navbar-item
|
>{{ $t("Administration") }}</b-navbar-item
|
||||||
>
|
>
|
||||||
|
|
||||||
<b-navbar-item tag="span">
|
<b-navbar-item
|
||||||
<span @click="logout">{{ $t("Log out") }}</span>
|
tag="span"
|
||||||
|
tabindex="0"
|
||||||
|
@click="logout"
|
||||||
|
@keyup.enter="logout"
|
||||||
|
>
|
||||||
|
<span>{{ $t("Log out") }}</span>
|
||||||
</b-navbar-item>
|
</b-navbar-item>
|
||||||
</b-navbar-dropdown>
|
</b-navbar-dropdown>
|
||||||
|
|
||||||
|
@ -173,7 +187,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Vue, Watch } from "vue-property-decorator";
|
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
|
||||||
import Logo from "@/components/Logo.vue";
|
import Logo from "@/components/Logo.vue";
|
||||||
import { GraphQLError } from "graphql";
|
import { GraphQLError } from "graphql";
|
||||||
import { loadLanguageAsync } from "@/utils/i18n";
|
import { loadLanguageAsync } from "@/utils/i18n";
|
||||||
|
@ -245,6 +259,13 @@ export default class NavBar extends Vue {
|
||||||
|
|
||||||
displayName = displayName;
|
displayName = displayName;
|
||||||
|
|
||||||
|
@Ref("user-dropdown") userDropDown!: any;
|
||||||
|
|
||||||
|
toggleMenu(): void {
|
||||||
|
console.debug("called toggleMenu");
|
||||||
|
this.userDropDown.showMenu();
|
||||||
|
}
|
||||||
|
|
||||||
@Watch("currentActor")
|
@Watch("currentActor")
|
||||||
async initializeListOfIdentities(): Promise<void> {
|
async initializeListOfIdentities(): Promise<void> {
|
||||||
if (!this.currentUser.isLoggedIn) return;
|
if (!this.currentUser.isLoggedIn) return;
|
||||||
|
|
|
@ -71,7 +71,13 @@ import { IParticipant } from "../../types/participant.model";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
import { CONFIRM_PARTICIPATION } from "../../graphql/event";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Confirm participation") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class ConfirmParticipation extends Vue {
|
export default class ConfirmParticipation extends Vue {
|
||||||
@Prop({ type: String, required: true }) token!: string;
|
@Prop({ type: String, required: true }) token!: string;
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,11 @@ import { IEvent } from "@/types/event.model";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Participation with account") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class ParticipationWithAccount extends Vue {
|
export default class ParticipationWithAccount extends Vue {
|
||||||
@Prop({ type: String, required: true }) uuid!: string;
|
@Prop({ type: String, required: true }) uuid!: string;
|
||||||
|
|
|
@ -155,6 +155,11 @@ import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||||
},
|
},
|
||||||
config: CONFIG,
|
config: CONFIG,
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Participation without account") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class ParticipationWithoutAccount extends Vue {
|
export default class ParticipationWithoutAccount extends Vue {
|
||||||
@Prop({ type: String, required: true }) uuid!: string;
|
@Prop({ type: String, required: true }) uuid!: string;
|
||||||
|
|
|
@ -130,6 +130,11 @@ import RouteName from "../../router/name";
|
||||||
},
|
},
|
||||||
config: CONFIG,
|
config: CONFIG,
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Unlogged participation") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class UnloggedParticipation extends Vue {
|
export default class UnloggedParticipation extends Vue {
|
||||||
@Prop({ type: String, required: true }) uuid!: string;
|
@Prop({ type: String, required: true }) uuid!: string;
|
||||||
|
|
|
@ -38,7 +38,12 @@
|
||||||
</span>
|
</span>
|
||||||
</b-upload>
|
</b-upload>
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-button type="is-text" v-if="imageSrc" @click="removeOrClearPicture">
|
<b-button
|
||||||
|
type="is-text"
|
||||||
|
v-if="imageSrc"
|
||||||
|
@click="removeOrClearPicture"
|
||||||
|
@keyup.enter="removeOrClearPicture"
|
||||||
|
>
|
||||||
{{ $t("Clear") }}
|
{{ $t("Clear") }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
<small class="has-text-grey-dark">{{
|
<small class="has-text-grey-dark">{{
|
||||||
$options.filters.formatDateTimeString(
|
$options.filters.formatDateTimeString(
|
||||||
new Date(post.insertedAt),
|
new Date(post.insertedAt),
|
||||||
|
undefined,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}}</small>
|
}}</small>
|
||||||
|
|
|
@ -14,10 +14,11 @@ function formatDateString(value: string): string {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatTimeString(value: string): string {
|
function formatTimeString(value: string, timeZone: string): string {
|
||||||
return parseDateTime(value).toLocaleTimeString(locale(), {
|
return parseDateTime(value).toLocaleTimeString(locale(), {
|
||||||
hour: "numeric",
|
hour: "numeric",
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
|
timeZone,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,6 +56,7 @@ const SHORT_TIME_FORMAT_OPTIONS: DateTimeFormatOptions = {
|
||||||
|
|
||||||
function formatDateTimeString(
|
function formatDateTimeString(
|
||||||
value: string,
|
value: string,
|
||||||
|
timeZone: string | undefined = undefined,
|
||||||
showTime = true,
|
showTime = true,
|
||||||
dateFormat = "long"
|
dateFormat = "long"
|
||||||
): string {
|
): string {
|
||||||
|
@ -66,6 +68,7 @@ function formatDateTimeString(
|
||||||
options = {
|
options = {
|
||||||
...options,
|
...options,
|
||||||
...(isLongFormat ? LONG_TIME_FORMAT_OPTIONS : SHORT_TIME_FORMAT_OPTIONS),
|
...(isLongFormat ? LONG_TIME_FORMAT_OPTIONS : SHORT_TIME_FORMAT_OPTIONS),
|
||||||
|
timeZone,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const format = new Intl.DateTimeFormat(locale(), options);
|
const format = new Intl.DateTimeFormat(locale(), options);
|
||||||
|
|
|
@ -13,6 +13,7 @@ export const ADDRESS_FRAGMENT = gql`
|
||||||
type
|
type
|
||||||
url
|
url
|
||||||
originId
|
originId
|
||||||
|
timezone
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,31 @@ export const CONFIG = gql`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const CONFIG_EDIT_EVENT = gql`
|
||||||
|
query EditEventConfig {
|
||||||
|
config {
|
||||||
|
timezones
|
||||||
|
features {
|
||||||
|
groups
|
||||||
|
}
|
||||||
|
anonymous {
|
||||||
|
participation {
|
||||||
|
allowed
|
||||||
|
validation {
|
||||||
|
email {
|
||||||
|
enabled
|
||||||
|
confirmationRequired
|
||||||
|
}
|
||||||
|
captcha {
|
||||||
|
enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const TERMS = gql`
|
export const TERMS = gql`
|
||||||
query Terms($locale: String) {
|
query Terms($locale: String) {
|
||||||
config {
|
config {
|
||||||
|
|
|
@ -46,6 +46,7 @@ const EVENT_OPTIONS_FRAGMENT = gql`
|
||||||
anonymousParticipation
|
anonymousParticipation
|
||||||
showStartTime
|
showStartTime
|
||||||
showEndTime
|
showEndTime
|
||||||
|
timezone
|
||||||
offers {
|
offers {
|
||||||
price
|
price
|
||||||
priceCurrency
|
priceCurrency
|
||||||
|
|
|
@ -147,6 +147,17 @@ export const USER_SETTINGS = gql`
|
||||||
${USER_SETTINGS_FRAGMENT}
|
${USER_SETTINGS_FRAGMENT}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const LOGGED_USER_TIMEZONE = gql`
|
||||||
|
query LoggedUserTimezone {
|
||||||
|
loggedUser {
|
||||||
|
id
|
||||||
|
settings {
|
||||||
|
timezone
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
export const SET_USER_SETTINGS = gql`
|
export const SET_USER_SETTINGS = gql`
|
||||||
mutation SetUserSettings(
|
mutation SetUserSettings(
|
||||||
$timezone: String
|
$timezone: String
|
||||||
|
|
|
@ -1158,5 +1158,49 @@
|
||||||
"Who can post a comment?": "Who can post a comment?",
|
"Who can post a comment?": "Who can post a comment?",
|
||||||
"Does the event needs to be confirmed later or is it cancelled?": "Does the event needs to be confirmed later or is it cancelled?",
|
"Does the event needs to be confirmed later or is it cancelled?": "Does the event needs to be confirmed later or is it cancelled?",
|
||||||
"When the post is private, you'll need to share the link around.": "When the post is private, you'll need to share the link around.",
|
"When the post is private, you'll need to share the link around.": "When the post is private, you'll need to share the link around.",
|
||||||
"Reset": "Reset"
|
"Reset": "Reset",
|
||||||
|
"Local time ({timezone})": "Local time ({timezone})",
|
||||||
|
"Time in your timezone ({timezone})": "Time in your timezone ({timezone})",
|
||||||
|
"Export": "Export",
|
||||||
|
"Times in your timezone ({timezone})": "Times in your timezone ({timezone})",
|
||||||
|
"Comment body": "Comment body",
|
||||||
|
"Event description body": "Event description body",
|
||||||
|
"Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting.": "Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting.",
|
||||||
|
"Clear timezone field": "Clear timezone field",
|
||||||
|
"Group description body": "Group description body",
|
||||||
|
"Moderation logs": "Moderation logs",
|
||||||
|
"Post body": "Post body",
|
||||||
|
"{group} posts": "{group} posts",
|
||||||
|
"{group}'s todolists": "{group}'s todolists",
|
||||||
|
"Validating email": "Validating email",
|
||||||
|
"Redirecting to Mobilizon": "Redirecting to Mobilizon",
|
||||||
|
"Reset password": "Reset password",
|
||||||
|
"First steps": "First steps",
|
||||||
|
"Validating account": "Validating account",
|
||||||
|
"Navigated to {pageTitle}": "Navigated to {pageTitle}",
|
||||||
|
"Confirm participation": "Confirm participation",
|
||||||
|
"Participation with account": "Participation with account",
|
||||||
|
"Participation without account": "Participation without account",
|
||||||
|
"Unlogged participation": "Unlogged participation",
|
||||||
|
"Discussions list": "Discussions list",
|
||||||
|
"Create discussion": "Create discussion",
|
||||||
|
"Tag search": "Tag search",
|
||||||
|
"Homepage": "Homepage",
|
||||||
|
"About instance": "About instance",
|
||||||
|
"Privacy": "Privacy",
|
||||||
|
"Interact": "Interact",
|
||||||
|
"Account settings": "Account settings",
|
||||||
|
"Admin dashboard": "Admin dashboard",
|
||||||
|
"Admin settings": "Admin settings",
|
||||||
|
"Group profiles": "Group profiles",
|
||||||
|
"Reports list": "Reports list",
|
||||||
|
"Create identity": "Create identity",
|
||||||
|
"Resent confirmation email": "Resent confirmation email",
|
||||||
|
"Send password reset": "Send password reset",
|
||||||
|
"Email validate": "Email validate",
|
||||||
|
"Skip to main content": "Skip to main content",
|
||||||
|
"{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})",
|
||||||
|
"Back to top": "Back to top",
|
||||||
|
"Powered by Mobilizon": "Powered by Mobilizon",
|
||||||
|
"Instance follows": "Instance follows"
|
||||||
}
|
}
|
|
@ -1262,5 +1262,51 @@
|
||||||
"{profile} updated the member {member}.": "{profile} a mis à jour le ou la membre {member}.",
|
"{profile} updated the member {member}.": "{profile} a mis à jour le ou la membre {member}.",
|
||||||
"{title} ({count} todos)": "{title} ({count} todos)",
|
"{title} ({count} todos)": "{title} ({count} todos)",
|
||||||
"{username} was invited to {group}": "{username} a été invité à {group}",
|
"{username} was invited to {group}": "{username} a été invité à {group}",
|
||||||
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap"
|
"© The OpenStreetMap Contributors": "© Les Contributeur⋅ices OpenStreetMap",
|
||||||
|
"Local time ({timezone})": "Heure locale ({timezone})",
|
||||||
|
"Time in your timezone ({timezone})": "Heure dans votre fuseau horaire ({timezone})",
|
||||||
|
"Export": "Export",
|
||||||
|
"Times in your timezone ({timezone})": "Heures dans votre fuseau horaire ({timezone})",
|
||||||
|
"has loaded": "a chargé",
|
||||||
|
"Skip to main": "",
|
||||||
|
"Navigated to {pageTitle}": "Navigué vers {pageTitle}",
|
||||||
|
"Comment body": "Corps du commentaire",
|
||||||
|
"Confirm participation": "Confirmer la participation",
|
||||||
|
"Participation with account": "Participation avec compte",
|
||||||
|
"Participation without account": "Participation sans compte",
|
||||||
|
"Unlogged participation": "Participation non connecté⋅e",
|
||||||
|
"Discussions list": "Liste des discussions",
|
||||||
|
"Create discussion": "Créer une discussion",
|
||||||
|
"Tag search": "Recherche par tag",
|
||||||
|
"Homepage": "Page d'accueil",
|
||||||
|
"About instance": "À propos de l'instance",
|
||||||
|
"Privacy": "Vie privée",
|
||||||
|
"Interact": "Interagir",
|
||||||
|
"Redirecting to Mobilizon": "Redirection vers Mobilizon",
|
||||||
|
"First steps": "Premiers pas",
|
||||||
|
"Account settings": "Paramètres du compte",
|
||||||
|
"Admin dashboard": "Tableau de bord admin",
|
||||||
|
"Admin settings": "Paramètres admin",
|
||||||
|
"Group profiles": "Profils des groupes",
|
||||||
|
"Reports list": "Liste des signalements",
|
||||||
|
"Moderation logs": "Journaux de modération",
|
||||||
|
"Create identity": "Créer une identité",
|
||||||
|
"Resent confirmation email": "Réenvoi de l'email de confirmation",
|
||||||
|
"Send password reset": "Envoi de la réinitalisation du mot de passe",
|
||||||
|
"Email validate": "Validation de l'email",
|
||||||
|
"Validating account": "Validation du compte",
|
||||||
|
"Event description body": "Corps de la description de l'événement",
|
||||||
|
"Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting.": "Le fuseau horaire de l'événement sera mis par défaut au fuseau horaire de l'addresse de l'événement s'il y en a une, ou bien à votre propre paramètre de fuseau horaire.",
|
||||||
|
"Clear timezone field": "Vider le champ du fuseau horaire",
|
||||||
|
"Group description body": "Corps de la description du groupe",
|
||||||
|
"Post body": "Corps du billet",
|
||||||
|
"{group} posts": "Billets de {group}",
|
||||||
|
"{group}'s todolists": "Liste de tâches de {group}",
|
||||||
|
"Validating email": "Validation de l'email",
|
||||||
|
"Reset password": "Réinitaliser le mot de passe",
|
||||||
|
"Skip to main content": "Passer au contenu principal",
|
||||||
|
"{timezoneLongName} ({timezoneShortName})": "{timezoneLongName} ({timezoneShortName})",
|
||||||
|
"Back to top": "Retour en haut",
|
||||||
|
"Powered by Mobilizon": "Propulsé par Mobilizon",
|
||||||
|
"Instance follows": "Abonnements de l'instance"
|
||||||
}
|
}
|
|
@ -4,6 +4,8 @@ import Component from "vue-class-component";
|
||||||
import VueScrollTo from "vue-scrollto";
|
import VueScrollTo from "vue-scrollto";
|
||||||
import VueMeta from "vue-meta";
|
import VueMeta from "vue-meta";
|
||||||
import VTooltip from "v-tooltip";
|
import VTooltip from "v-tooltip";
|
||||||
|
import VueAnnouncer from "@vue-a11y/announcer";
|
||||||
|
import VueSkipTo from "@vue-a11y/skip-to";
|
||||||
import App from "./App.vue";
|
import App from "./App.vue";
|
||||||
import router from "./router";
|
import router from "./router";
|
||||||
import { NotifierPlugin } from "./plugins/notifier";
|
import { NotifierPlugin } from "./plugins/notifier";
|
||||||
|
@ -20,6 +22,8 @@ Vue.use(filters);
|
||||||
Vue.use(VueMeta);
|
Vue.use(VueMeta);
|
||||||
Vue.use(VueScrollTo);
|
Vue.use(VueScrollTo);
|
||||||
Vue.use(VTooltip);
|
Vue.use(VTooltip);
|
||||||
|
Vue.use(VueAnnouncer);
|
||||||
|
Vue.use(VueSkipTo);
|
||||||
|
|
||||||
// Register the router hooks with their names
|
// Register the router hooks with their names
|
||||||
Component.registerHooks([
|
Component.registerHooks([
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { RouteConfig } from "vue-router";
|
import { RouteConfig } from "vue-router";
|
||||||
import { ImportedComponent } from "vue/types/options";
|
import { ImportedComponent } from "vue/types/options";
|
||||||
|
import { i18n } from "@/utils/i18n";
|
||||||
|
|
||||||
export enum ActorRouteName {
|
export enum ActorRouteName {
|
||||||
GROUP = "Group",
|
GROUP = "Group",
|
||||||
|
@ -14,7 +15,10 @@ export const actorRoutes: RouteConfig[] = [
|
||||||
name: ActorRouteName.CREATE_GROUP,
|
name: ActorRouteName.CREATE_GROUP,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "CreateGroup" */ "@/views/Group/Create.vue"),
|
import(/* webpackChunkName: "CreateGroup" */ "@/views/Group/Create.vue"),
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("Create group") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername",
|
path: "/@:preferredUsername",
|
||||||
|
@ -22,13 +26,16 @@ export const actorRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Group" */ "@/views/Group/Group.vue"),
|
import(/* webpackChunkName: "Group" */ "@/views/Group/Group.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/groups/me",
|
path: "/groups/me",
|
||||||
name: ActorRouteName.MY_GROUPS,
|
name: ActorRouteName.MY_GROUPS,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "MyGroups" */ "@/views/Group/MyGroups.vue"),
|
import(/* webpackChunkName: "MyGroups" */ "@/views/Group/MyGroups.vue"),
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("My groups") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { RouteConfig } from "vue-router";
|
import { RouteConfig } from "vue-router";
|
||||||
import { ImportedComponent } from "vue/types/options";
|
import { ImportedComponent } from "vue/types/options";
|
||||||
|
import { i18n } from "@/utils/i18n";
|
||||||
|
|
||||||
export enum DiscussionRouteName {
|
export enum DiscussionRouteName {
|
||||||
DISCUSSION_LIST = "DISCUSSION_LIST",
|
DISCUSSION_LIST = "DISCUSSION_LIST",
|
||||||
|
@ -16,7 +17,12 @@ export const discussionRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "DiscussionsList" */ "@/views/Discussions/DiscussionsList.vue"
|
/* webpackChunkName: "DiscussionsList" */ "@/views/Discussions/DiscussionsList.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Discussions list") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/discussions/new",
|
path: "/@:preferredUsername/discussions/new",
|
||||||
|
@ -26,7 +32,12 @@ export const discussionRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "CreateDiscussion" */ "@/views/Discussions/Create.vue"
|
/* webpackChunkName: "CreateDiscussion" */ "@/views/Discussions/Create.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Create discussion") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/c/:slug/:comment_id?",
|
path: "/@:preferredUsername/c/:slug/:comment_id?",
|
||||||
|
@ -36,6 +47,6 @@ export const discussionRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "Discussion" */ "@/views/Discussions/Discussion.vue"
|
/* webpackChunkName: "Discussion" */ "@/views/Discussions/Discussion.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { RouteConfig } from "vue-router";
|
import { RouteConfig } from "vue-router";
|
||||||
import { ImportedComponent } from "vue/types/options";
|
import { ImportedComponent } from "vue/types/options";
|
||||||
|
import { i18n } from "@/utils/i18n";
|
||||||
|
|
||||||
export enum ErrorRouteName {
|
export enum ErrorRouteName {
|
||||||
ERROR = "Error",
|
ERROR = "Error",
|
||||||
|
@ -11,5 +12,8 @@ export const errorRoutes: RouteConfig[] = [
|
||||||
name: ErrorRouteName.ERROR,
|
name: ErrorRouteName.ERROR,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Error" */ "../views/Error.vue"),
|
import(/* webpackChunkName: "Error" */ "../views/Error.vue"),
|
||||||
|
meta: {
|
||||||
|
announcer: { message: (): string => i18n.t("Error") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { RouteConfig, Route } from "vue-router";
|
import { RouteConfig, Route } from "vue-router";
|
||||||
import { ImportedComponent } from "vue/types/options";
|
import { ImportedComponent } from "vue/types/options";
|
||||||
|
import { i18n } from "@/utils/i18n";
|
||||||
|
|
||||||
const participations = (): Promise<ImportedComponent> =>
|
const participations = (): Promise<ImportedComponent> =>
|
||||||
import(
|
import(
|
||||||
|
@ -33,25 +34,34 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
name: EventRouteName.EVENT_LIST,
|
name: EventRouteName.EVENT_LIST,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"),
|
import(/* webpackChunkName: "EventList" */ "@/views/Event/EventList.vue"),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Event list") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/events/create",
|
path: "/events/create",
|
||||||
name: EventRouteName.CREATE_EVENT,
|
name: EventRouteName.CREATE_EVENT,
|
||||||
component: editEvent,
|
component: editEvent,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("Create event") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/events/me",
|
path: "/events/me",
|
||||||
name: EventRouteName.MY_EVENTS,
|
name: EventRouteName.MY_EVENTS,
|
||||||
component: myEvents,
|
component: myEvents,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("My events") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/events/edit/:eventId",
|
path: "/events/edit/:eventId",
|
||||||
name: EventRouteName.EDIT_EVENT,
|
name: EventRouteName.EDIT_EVENT,
|
||||||
component: editEvent,
|
component: editEvent,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
props: (route: Route): Record<string, unknown> => {
|
props: (route: Route): Record<string, unknown> => {
|
||||||
return { ...route.params, ...{ isUpdate: true } };
|
return { ...route.params, ...{ isUpdate: true } };
|
||||||
},
|
},
|
||||||
|
@ -60,7 +70,7 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
path: "/events/duplicate/:eventId",
|
path: "/events/duplicate/:eventId",
|
||||||
name: EventRouteName.DUPLICATE_EVENT,
|
name: EventRouteName.DUPLICATE_EVENT,
|
||||||
component: editEvent,
|
component: editEvent,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announce: { skip: true } },
|
||||||
props: (route: Route): Record<string, unknown> => ({
|
props: (route: Route): Record<string, unknown> => ({
|
||||||
...route.params,
|
...route.params,
|
||||||
...{ isDuplicate: true },
|
...{ isDuplicate: true },
|
||||||
|
@ -70,7 +80,7 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
path: "/events/:eventId/participations",
|
path: "/events/:eventId/participations",
|
||||||
name: EventRouteName.PARTICIPATIONS,
|
name: EventRouteName.PARTICIPATIONS,
|
||||||
component: participations,
|
component: participations,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -78,7 +88,7 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
name: EventRouteName.EVENT,
|
name: EventRouteName.EVENT,
|
||||||
component: event,
|
component: event,
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/events/:uuid/participate",
|
path: "/events/:uuid/participate",
|
||||||
|
@ -86,12 +96,22 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("../components/Participation/UnloggedParticipation.vue"),
|
import("../components/Participation/UnloggedParticipation.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: {
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Unlogged participation") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/events/:uuid/participate/with-account",
|
path: "/events/:uuid/participate/with-account",
|
||||||
name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
|
name: EventRouteName.EVENT_PARTICIPATE_WITH_ACCOUNT,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("../components/Participation/ParticipationWithAccount.vue"),
|
import("../components/Participation/ParticipationWithAccount.vue"),
|
||||||
|
meta: {
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Participation with account") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -99,6 +119,12 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT,
|
name: EventRouteName.EVENT_PARTICIPATE_WITHOUT_ACCOUNT,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("../components/Participation/ParticipationWithoutAccount.vue"),
|
import("../components/Participation/ParticipationWithoutAccount.vue"),
|
||||||
|
meta: {
|
||||||
|
announcer: {
|
||||||
|
message: (): string =>
|
||||||
|
i18n.t("Participation without account") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -106,6 +132,11 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
name: EventRouteName.EVENT_PARTICIPATE_CONFIRM,
|
name: EventRouteName.EVENT_PARTICIPATE_CONFIRM,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("../components/Participation/ConfirmParticipation.vue"),
|
import("../components/Participation/ConfirmParticipation.vue"),
|
||||||
|
meta: {
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Confirm participation") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -114,6 +145,9 @@ export const eventRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
|
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Tag search") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -33,7 +33,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("@/views/Todos/TodoLists.vue"),
|
import("@/views/Todos/TodoLists.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/todo-lists/:id",
|
path: "/todo-lists/:id",
|
||||||
|
@ -41,7 +41,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("@/views/Todos/TodoList.vue"),
|
import("@/views/Todos/TodoList.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/todo/:todoId",
|
path: "/todo/:todoId",
|
||||||
|
@ -49,21 +49,21 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("@/views/Todos/Todo.vue"),
|
import("@/views/Todos/Todo.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/resources",
|
path: "/@:preferredUsername/resources",
|
||||||
name: GroupsRouteName.RESOURCE_FOLDER_ROOT,
|
name: GroupsRouteName.RESOURCE_FOLDER_ROOT,
|
||||||
component: resourceFolder,
|
component: resourceFolder,
|
||||||
props: { path: "/" },
|
props: { path: "/" },
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/resources/:path+",
|
path: "/@:preferredUsername/resources/:path+",
|
||||||
name: GroupsRouteName.RESOURCE_FOLDER,
|
name: GroupsRouteName.RESOURCE_FOLDER,
|
||||||
component: resourceFolder,
|
component: resourceFolder,
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/settings",
|
path: "/@:preferredUsername/settings",
|
||||||
|
@ -79,6 +79,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
name: GroupsRouteName.GROUP_PUBLIC_SETTINGS,
|
name: GroupsRouteName.GROUP_PUBLIC_SETTINGS,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("../views/Group/GroupSettings.vue"),
|
import("../views/Group/GroupSettings.vue"),
|
||||||
|
meta: { announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "members",
|
path: "members",
|
||||||
|
@ -86,6 +87,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("../views/Group/GroupMembers.vue"),
|
import("../views/Group/GroupMembers.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "followers",
|
path: "followers",
|
||||||
|
@ -93,6 +95,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("../views/Group/GroupFollowers.vue"),
|
import("../views/Group/GroupFollowers.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
|
meta: { announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -102,7 +105,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
import("@/views/Posts/Edit.vue"),
|
import("@/views/Posts/Edit.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
name: GroupsRouteName.POST_CREATE,
|
name: GroupsRouteName.POST_CREATE,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/p/:slug/edit",
|
path: "/p/:slug/edit",
|
||||||
|
@ -113,7 +116,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
...{ isUpdate: true },
|
...{ isUpdate: true },
|
||||||
}),
|
}),
|
||||||
name: GroupsRouteName.POST_EDIT,
|
name: GroupsRouteName.POST_EDIT,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/p/:slug",
|
path: "/p/:slug",
|
||||||
|
@ -121,7 +124,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
import("@/views/Posts/Post.vue"),
|
import("@/views/Posts/Post.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
name: GroupsRouteName.POST,
|
name: GroupsRouteName.POST,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/p",
|
path: "/@:preferredUsername/p",
|
||||||
|
@ -129,14 +132,14 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
import("@/views/Posts/List.vue"),
|
import("@/views/Posts/List.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
name: GroupsRouteName.POSTS,
|
name: GroupsRouteName.POSTS,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/events",
|
path: "/@:preferredUsername/events",
|
||||||
component: groupEvents,
|
component: groupEvents,
|
||||||
props: true,
|
props: true,
|
||||||
name: GroupsRouteName.GROUP_EVENTS,
|
name: GroupsRouteName.GROUP_EVENTS,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/join",
|
path: "/@:preferredUsername/join",
|
||||||
|
@ -144,7 +147,7 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
import("@/components/Group/JoinGroupWithAccount.vue"),
|
import("@/components/Group/JoinGroupWithAccount.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
name: GroupsRouteName.GROUP_JOIN,
|
name: GroupsRouteName.GROUP_JOIN,
|
||||||
meta: { requiredAuth: false },
|
meta: { requiredAuth: false, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/@:preferredUsername/timeline",
|
path: "/@:preferredUsername/timeline",
|
||||||
|
@ -152,6 +155,6 @@ export const groupsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import("@/views/Group/Timeline.vue"),
|
import("@/views/Group/Timeline.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { groupsRoutes } from "./groups";
|
||||||
import { discussionRoutes } from "./discussion";
|
import { discussionRoutes } from "./discussion";
|
||||||
import { userRoutes } from "./user";
|
import { userRoutes } from "./user";
|
||||||
import RouteName from "./name";
|
import RouteName from "./name";
|
||||||
|
import { i18n } from "@/utils/i18n";
|
||||||
|
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
|
@ -49,13 +50,19 @@ export const routes = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
|
import(/* webpackChunkName: "Search" */ "@/views/Search.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Search") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
name: RouteName.HOME,
|
name: RouteName.HOME,
|
||||||
component: Home,
|
component: Home,
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Homepage") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/about",
|
path: "/about",
|
||||||
|
@ -72,27 +79,41 @@ export const routes = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue"
|
/* webpackChunkName: "about" */ "@/views/About/AboutInstance.vue"
|
||||||
),
|
),
|
||||||
|
meta: {
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("About instance") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/terms",
|
path: "/terms",
|
||||||
name: RouteName.TERMS,
|
name: RouteName.TERMS,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"),
|
import(/* webpackChunkName: "cookies" */ "@/views/About/Terms.vue"),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Terms") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/privacy",
|
path: "/privacy",
|
||||||
name: RouteName.PRIVACY,
|
name: RouteName.PRIVACY,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"),
|
import(/* webpackChunkName: "cookies" */ "@/views/About/Privacy.vue"),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Privacy") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/rules",
|
path: "/rules",
|
||||||
name: RouteName.RULES,
|
name: RouteName.RULES,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"),
|
import(/* webpackChunkName: "cookies" */ "@/views/About/Rules.vue"),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Rules") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/glossary",
|
path: "/glossary",
|
||||||
|
@ -101,7 +122,10 @@ export const routes = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue"
|
/* webpackChunkName: "cookies" */ "@/views/About/Glossary.vue"
|
||||||
),
|
),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Glossary") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -110,7 +134,10 @@ export const routes = [
|
||||||
name: RouteName.INTERACT,
|
name: RouteName.INTERACT,
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "interact" */ "@/views/Interact.vue"),
|
import(/* webpackChunkName: "interact" */ "@/views/Interact.vue"),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Interact") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/auth/:provider/callback",
|
path: "/auth/:provider/callback",
|
||||||
|
@ -119,6 +146,11 @@ export const routes = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue"
|
/* webpackChunkName: "ProviderValidation" */ "@/views/User/ProviderValidation.vue"
|
||||||
),
|
),
|
||||||
|
meta: {
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Redirecting to Mobilizon") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/welcome/:step?",
|
path: "/welcome/:step?",
|
||||||
|
@ -127,7 +159,10 @@ export const routes = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue"
|
/* webpackChunkName: "WelcomeScreen" */ "@/views/User/SettingsOnboard.vue"
|
||||||
),
|
),
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("First steps") as string },
|
||||||
|
},
|
||||||
props: (route: Route): Record<string, unknown> => {
|
props: (route: Route): Record<string, unknown> => {
|
||||||
const step = Number.parseInt(route.params.step, 10);
|
const step = Number.parseInt(route.params.step, 10);
|
||||||
if (Number.isNaN(step)) {
|
if (Number.isNaN(step)) {
|
||||||
|
@ -143,7 +178,10 @@ export const routes = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "PageNotFound" */ "../views/PageNotFound.vue"
|
/* webpackChunkName: "PageNotFound" */ "../views/PageNotFound.vue"
|
||||||
),
|
),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Page not found") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "*",
|
path: "*",
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Route, RouteConfig } from "vue-router";
|
import { Route, RouteConfig } from "vue-router";
|
||||||
import { ImportedComponent } from "vue/types/options";
|
import { ImportedComponent } from "vue/types/options";
|
||||||
|
import { i18n } from "@/utils/i18n";
|
||||||
|
|
||||||
export enum SettingsRouteName {
|
export enum SettingsRouteName {
|
||||||
SETTINGS = "SETTINGS",
|
SETTINGS = "SETTINGS",
|
||||||
|
@ -34,7 +35,7 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"),
|
import(/* webpackChunkName: "Settings" */ "@/views/Settings.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS },
|
redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS },
|
||||||
name: SettingsRouteName.SETTINGS,
|
name: SettingsRouteName.SETTINGS,
|
||||||
children: [
|
children: [
|
||||||
|
@ -42,7 +43,10 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
path: "account",
|
path: "account",
|
||||||
name: SettingsRouteName.ACCOUNT_SETTINGS,
|
name: SettingsRouteName.ACCOUNT_SETTINGS,
|
||||||
redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL },
|
redirect: { name: SettingsRouteName.ACCOUNT_SETTINGS_GENERAL },
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { skip: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "account/general",
|
path: "account/general",
|
||||||
|
@ -52,7 +56,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue"
|
/* webpackChunkName: "AccountSettings" */ "@/views/Settings/AccountSettings.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Account settings") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "preferences",
|
path: "preferences",
|
||||||
|
@ -62,7 +71,10 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue"
|
/* webpackChunkName: "Preferences" */ "@/views/Settings/Preferences.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("Preferences") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "notifications",
|
path: "notifications",
|
||||||
|
@ -72,13 +84,18 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue"
|
/* webpackChunkName: "Notifications" */ "@/views/Settings/Notifications.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Notifications") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin",
|
path: "admin",
|
||||||
name: SettingsRouteName.ADMIN,
|
name: SettingsRouteName.ADMIN,
|
||||||
redirect: { name: SettingsRouteName.ADMIN_DASHBOARD },
|
redirect: { name: SettingsRouteName.ADMIN_DASHBOARD },
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/dashboard",
|
path: "admin/dashboard",
|
||||||
|
@ -87,7 +104,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "Dashboard" */ "@/views/Admin/Dashboard.vue"
|
/* webpackChunkName: "Dashboard" */ "@/views/Admin/Dashboard.vue"
|
||||||
),
|
),
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Admin dashboard") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/settings",
|
path: "admin/settings",
|
||||||
|
@ -97,7 +119,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue"
|
/* webpackChunkName: "AdminSettings" */ "@/views/Admin/Settings.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Admin settings") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/users",
|
path: "admin/users",
|
||||||
|
@ -105,7 +132,10 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"),
|
import(/* webpackChunkName: "Users" */ "@/views/Admin/Users.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("Users") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/users/:id",
|
path: "admin/users/:id",
|
||||||
|
@ -115,7 +145,10 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue"
|
/* webpackChunkName: "AdminUserProfile" */ "@/views/Admin/AdminUserProfile.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { skip: true },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/profiles",
|
path: "admin/profiles",
|
||||||
|
@ -125,7 +158,10 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue"
|
/* webpackChunkName: "AdminProfiles" */ "@/views/Admin/Profiles.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("Profiles") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/profiles/:id",
|
path: "admin/profiles/:id",
|
||||||
|
@ -135,7 +171,7 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue"
|
/* webpackChunkName: "AdminProfile" */ "@/views/Admin/AdminProfile.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/groups",
|
path: "admin/groups",
|
||||||
|
@ -145,7 +181,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue"
|
/* webpackChunkName: "GroupProfiles" */ "@/views/Admin/GroupProfiles.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Group profiles") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/groups/:id",
|
path: "admin/groups/:id",
|
||||||
|
@ -155,7 +196,7 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue"
|
/* webpackChunkName: "AdminGroupProfile" */ "@/views/Admin/AdminGroupProfile.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "admin/relays",
|
path: "admin/relays",
|
||||||
|
@ -163,7 +204,7 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS },
|
redirect: { name: SettingsRouteName.RELAY_FOLLOWINGS },
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"),
|
import(/* webpackChunkName: "Follows" */ "@/views/Admin/Follows.vue"),
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: "followings",
|
path: "followings",
|
||||||
|
@ -172,7 +213,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"
|
/* webpackChunkName: "Followings" */ "@/components/Admin/Followings.vue"
|
||||||
),
|
),
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Followings") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "followers",
|
path: "followers",
|
||||||
|
@ -181,7 +227,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"
|
/* webpackChunkName: "Followers" */ "@/components/Admin/Followers.vue"
|
||||||
),
|
),
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Followers") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
props: true,
|
props: true,
|
||||||
|
@ -190,7 +241,7 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
path: "/moderation",
|
path: "/moderation",
|
||||||
name: SettingsRouteName.MODERATION,
|
name: SettingsRouteName.MODERATION,
|
||||||
redirect: { name: SettingsRouteName.REPORTS },
|
redirect: { name: SettingsRouteName.REPORTS },
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/moderation/reports/:filter?",
|
path: "/moderation/reports/:filter?",
|
||||||
|
@ -200,7 +251,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue"
|
/* webpackChunkName: "ReportList" */ "@/views/Moderation/ReportList.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Reports list") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/moderation/report/:reportId",
|
path: "/moderation/report/:reportId",
|
||||||
|
@ -210,7 +266,10 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue"
|
/* webpackChunkName: "Report" */ "@/views/Moderation/Report.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: { message: (): string => i18n.t("Report") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/moderation/logs",
|
path: "/moderation/logs",
|
||||||
|
@ -220,13 +279,18 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue"
|
/* webpackChunkName: "ModerationLogs" */ "@/views/Moderation/Logs.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Moderation logs") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/identity",
|
path: "/identity",
|
||||||
name: SettingsRouteName.IDENTITIES,
|
name: SettingsRouteName.IDENTITIES,
|
||||||
redirect: { name: SettingsRouteName.UPDATE_IDENTITY },
|
redirect: { name: SettingsRouteName.UPDATE_IDENTITY },
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/identity/create",
|
path: "/identity/create",
|
||||||
|
@ -239,7 +303,12 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
identityName: route.params.identityName,
|
identityName: route.params.identityName,
|
||||||
isUpdate: false,
|
isUpdate: false,
|
||||||
}),
|
}),
|
||||||
meta: { requiredAuth: true },
|
meta: {
|
||||||
|
requiredAuth: true,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Create identity") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/identity/update/:identityName?",
|
path: "/identity/update/:identityName?",
|
||||||
|
@ -252,7 +321,7 @@ export const settingsRoutes: RouteConfig[] = [
|
||||||
identityName: route.params.identityName,
|
identityName: route.params.identityName,
|
||||||
isUpdate: true,
|
isUpdate: true,
|
||||||
}),
|
}),
|
||||||
meta: { requiredAuth: true },
|
meta: { requiredAuth: true, announcer: { skip: true } },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { beforeRegisterGuard } from "@/router/guards/register-guard";
|
import { beforeRegisterGuard } from "@/router/guards/register-guard";
|
||||||
import { Route, RouteConfig } from "vue-router";
|
import { Route, RouteConfig } from "vue-router";
|
||||||
import { ImportedComponent } from "vue/types/options";
|
import { ImportedComponent } from "vue/types/options";
|
||||||
|
import { i18n } from "@/utils/i18n";
|
||||||
|
|
||||||
export enum UserRouteName {
|
export enum UserRouteName {
|
||||||
REGISTER = "Register",
|
REGISTER = "Register",
|
||||||
|
@ -22,7 +23,10 @@ export const userRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "RegisterUser" */ "@/views/User/Register.vue"
|
/* webpackChunkName: "RegisterUser" */ "@/views/User/Register.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Register") as string },
|
||||||
|
},
|
||||||
beforeEnter: beforeRegisterGuard,
|
beforeEnter: beforeRegisterGuard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -37,7 +41,10 @@ export const userRoutes: RouteConfig[] = [
|
||||||
email: route.params.email,
|
email: route.params.email,
|
||||||
userAlreadyActivated: route.params.userAlreadyActivated === "true",
|
userAlreadyActivated: route.params.userAlreadyActivated === "true",
|
||||||
}),
|
}),
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Register") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/resend-instructions",
|
path: "/resend-instructions",
|
||||||
|
@ -47,7 +54,12 @@ export const userRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "ResendConfirmation" */ "@/views/User/ResendConfirmation.vue"
|
/* webpackChunkName: "ResendConfirmation" */ "@/views/User/ResendConfirmation.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: false },
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Resent confirmation email") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/password-reset/send",
|
path: "/password-reset/send",
|
||||||
|
@ -57,7 +69,12 @@ export const userRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "SendPasswordReset" */ "@/views/User/SendPasswordReset.vue"
|
/* webpackChunkName: "SendPasswordReset" */ "@/views/User/SendPasswordReset.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: false },
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Send password reset") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/password-reset/:token",
|
path: "/password-reset/:token",
|
||||||
|
@ -66,7 +83,10 @@ export const userRoutes: RouteConfig[] = [
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "PasswordReset" */ "@/views/User/PasswordReset.vue"
|
/* webpackChunkName: "PasswordReset" */ "@/views/User/PasswordReset.vue"
|
||||||
),
|
),
|
||||||
meta: { requiresAuth: false },
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Password reset") as string },
|
||||||
|
},
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -77,7 +97,10 @@ export const userRoutes: RouteConfig[] = [
|
||||||
/* webpackChunkName: "EmailValidate" */ "@/views/User/EmailValidate.vue"
|
/* webpackChunkName: "EmailValidate" */ "@/views/User/EmailValidate.vue"
|
||||||
),
|
),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: false },
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Email validate") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/validate/:token",
|
path: "/validate/:token",
|
||||||
|
@ -85,7 +108,12 @@ export const userRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Validate" */ "@/views/User/Validate.vue"),
|
import(/* webpackChunkName: "Validate" */ "@/views/User/Validate.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiresAuth: false },
|
meta: {
|
||||||
|
requiresAuth: false,
|
||||||
|
announcer: {
|
||||||
|
message: (): string => i18n.t("Validating account") as string,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
|
@ -93,6 +121,9 @@ export const userRoutes: RouteConfig[] = [
|
||||||
component: (): Promise<ImportedComponent> =>
|
component: (): Promise<ImportedComponent> =>
|
||||||
import(/* webpackChunkName: "Login" */ "@/views/User/Login.vue"),
|
import(/* webpackChunkName: "Login" */ "@/views/User/Login.vue"),
|
||||||
props: true,
|
props: true,
|
||||||
meta: { requiredAuth: false },
|
meta: {
|
||||||
|
requiredAuth: false,
|
||||||
|
announcer: { message: (): string => i18n.t("Login") as string },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -13,6 +13,7 @@ export interface IAddress {
|
||||||
geom?: string;
|
geom?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
originId?: string;
|
originId?: string;
|
||||||
|
timezone?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPoiInfo {
|
export interface IPoiInfo {
|
||||||
|
@ -44,20 +45,23 @@ export class Address implements IAddress {
|
||||||
|
|
||||||
geom?: string = "";
|
geom?: string = "";
|
||||||
|
|
||||||
|
timezone?: string = "";
|
||||||
|
|
||||||
constructor(hash?: IAddress) {
|
constructor(hash?: IAddress) {
|
||||||
if (!hash) return;
|
if (!hash) return;
|
||||||
|
|
||||||
this.id = hash.id;
|
this.id = hash.id;
|
||||||
this.description = hash.description;
|
this.description = hash.description?.trim();
|
||||||
this.street = hash.street;
|
this.street = hash.street?.trim();
|
||||||
this.locality = hash.locality;
|
this.locality = hash.locality?.trim();
|
||||||
this.postalCode = hash.postalCode;
|
this.postalCode = hash.postalCode?.trim();
|
||||||
this.region = hash.region;
|
this.region = hash.region?.trim();
|
||||||
this.country = hash.country;
|
this.country = hash.country?.trim();
|
||||||
this.type = hash.type;
|
this.type = hash.type;
|
||||||
this.geom = hash.geom;
|
this.geom = hash.geom;
|
||||||
this.url = hash.url;
|
this.url = hash.url;
|
||||||
this.originId = hash.originId;
|
this.originId = hash.originId;
|
||||||
|
this.timezone = hash.timezone;
|
||||||
}
|
}
|
||||||
|
|
||||||
get poiInfos(): IPoiInfo {
|
get poiInfos(): IPoiInfo {
|
||||||
|
|
|
@ -26,6 +26,7 @@ export interface IEventOptions {
|
||||||
showParticipationPrice: boolean;
|
showParticipationPrice: boolean;
|
||||||
showStartTime: boolean;
|
showStartTime: boolean;
|
||||||
showEndTime: boolean;
|
showEndTime: boolean;
|
||||||
|
timezone: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EventOptions implements IEventOptions {
|
export class EventOptions implements IEventOptions {
|
||||||
|
@ -54,4 +55,6 @@ export class EventOptions implements IEventOptions {
|
||||||
showStartTime = true;
|
showStartTime = true;
|
||||||
|
|
||||||
showEndTime = true;
|
showEndTime = true;
|
||||||
|
|
||||||
|
timezone = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ interface IEventEditJSON {
|
||||||
id?: string;
|
id?: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
beginsOn: string;
|
beginsOn: string | null;
|
||||||
endsOn: string | null;
|
endsOn: string | null;
|
||||||
status: EventStatus;
|
status: EventStatus;
|
||||||
visibility: EventVisibility;
|
visibility: EventVisibility;
|
||||||
|
@ -82,7 +82,7 @@ export interface IEvent {
|
||||||
|
|
||||||
onlineAddress?: string;
|
onlineAddress?: string;
|
||||||
phoneAddress?: string;
|
phoneAddress?: string;
|
||||||
physicalAddress?: IAddress;
|
physicalAddress: IAddress | null;
|
||||||
|
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
options: IEventOptions;
|
options: IEventOptions;
|
||||||
|
@ -92,6 +92,9 @@ export interface IEvent {
|
||||||
toEditJSON(): IEventEditJSON;
|
toEditJSON(): IEventEditJSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IEditableEvent extends Omit<IEvent, "beginsOn"> {
|
||||||
|
beginsOn: Date | null;
|
||||||
|
}
|
||||||
export class EventModel implements IEvent {
|
export class EventModel implements IEvent {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|
||||||
|
@ -115,7 +118,7 @@ export class EventModel implements IEvent {
|
||||||
|
|
||||||
phoneAddress: string | undefined = "";
|
phoneAddress: string | undefined = "";
|
||||||
|
|
||||||
physicalAddress?: IAddress;
|
physicalAddress: IAddress | null = null;
|
||||||
|
|
||||||
picture: IMedia | null = null;
|
picture: IMedia | null = null;
|
||||||
|
|
||||||
|
@ -158,7 +161,7 @@ export class EventModel implements IEvent {
|
||||||
|
|
||||||
metadata: IEventMetadata[] = [];
|
metadata: IEventMetadata[] = [];
|
||||||
|
|
||||||
constructor(hash?: IEvent) {
|
constructor(hash?: IEvent | IEditableEvent) {
|
||||||
if (!hash) return;
|
if (!hash) return;
|
||||||
|
|
||||||
this.id = hash.id;
|
this.id = hash.id;
|
||||||
|
@ -170,8 +173,14 @@ export class EventModel implements IEvent {
|
||||||
this.slug = hash.slug;
|
this.slug = hash.slug;
|
||||||
this.description = hash.description || "";
|
this.description = hash.description || "";
|
||||||
|
|
||||||
this.beginsOn = new Date(hash.beginsOn);
|
if (hash.beginsOn) {
|
||||||
if (hash.endsOn) this.endsOn = new Date(hash.endsOn);
|
this.beginsOn = new Date(hash.beginsOn);
|
||||||
|
}
|
||||||
|
if (hash.endsOn) {
|
||||||
|
this.endsOn = new Date(hash.endsOn);
|
||||||
|
} else {
|
||||||
|
this.endsOn = null;
|
||||||
|
}
|
||||||
|
|
||||||
this.publishAt = new Date(hash.publishAt);
|
this.publishAt = new Date(hash.publishAt);
|
||||||
|
|
||||||
|
@ -192,7 +201,7 @@ export class EventModel implements IEvent {
|
||||||
this.phoneAddress = hash.phoneAddress;
|
this.phoneAddress = hash.phoneAddress;
|
||||||
this.physicalAddress = hash.physicalAddress
|
this.physicalAddress = hash.physicalAddress
|
||||||
? new Address(hash.physicalAddress)
|
? new Address(hash.physicalAddress)
|
||||||
: undefined;
|
: null;
|
||||||
this.participantStats = hash.participantStats;
|
this.participantStats = hash.participantStats;
|
||||||
|
|
||||||
this.contacts = hash.contacts;
|
this.contacts = hash.contacts;
|
||||||
|
@ -217,12 +226,12 @@ export function removeTypeName(entity: any): any {
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toEditJSON(event: IEvent): IEventEditJSON {
|
export function toEditJSON(event: IEditableEvent): IEventEditJSON {
|
||||||
return {
|
return {
|
||||||
id: event.id,
|
id: event.id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
description: event.description,
|
description: event.description,
|
||||||
beginsOn: event.beginsOn.toISOString(),
|
beginsOn: event.beginsOn ? event.beginsOn.toISOString() : null,
|
||||||
endsOn: event.endsOn ? event.endsOn.toISOString() : null,
|
endsOn: event.endsOn ? event.endsOn.toISOString() : null,
|
||||||
status: event.status,
|
status: event.status,
|
||||||
visibility: event.visibility,
|
visibility: event.visibility,
|
||||||
|
|
|
@ -1,28 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="hero intro is-small is-primary">
|
<section class="container">
|
||||||
<div class="hero-body">
|
|
||||||
<div class="container">
|
|
||||||
<h1 class="title">{{ $t("About Mobilizon") }}</h1>
|
|
||||||
<p>
|
|
||||||
{{
|
|
||||||
$t(
|
|
||||||
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising."
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</p>
|
|
||||||
<b-button
|
|
||||||
icon-left="open-in-new"
|
|
||||||
size="is-large"
|
|
||||||
type="is-secondary"
|
|
||||||
tag="a"
|
|
||||||
href="https://joinmobilizon.org"
|
|
||||||
>{{ $t("Learn more") }}</b-button
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<main class="container">
|
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-one-quarter-desktop">
|
<div class="column is-one-quarter-desktop">
|
||||||
<aside class="menu">
|
<aside class="menu">
|
||||||
|
@ -62,8 +40,29 @@
|
||||||
<router-view />
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</section>
|
||||||
|
<div class="hero intro is-small is-secondary">
|
||||||
|
<div class="hero-body">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="title">{{ $t("Powered by Mobilizon") }}</h1>
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"A user-friendly, emancipatory and ethical tool for gathering, organising, and mobilising."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<b-button
|
||||||
|
icon-left="open-in-new"
|
||||||
|
size="is-large"
|
||||||
|
type="is-primary"
|
||||||
|
tag="a"
|
||||||
|
href="https://joinmobilizon.org"
|
||||||
|
>{{ $t("Learn more") }}</b-button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class="hero register is-primary is-medium"
|
class="hero register is-primary is-medium"
|
||||||
v-if="!currentUser || !currentUser.id"
|
v-if="!currentUser || !currentUser.id"
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<section class="hero is-primary">
|
<section class="hero is-primary">
|
||||||
<div class="hero-body">
|
<div class="hero-body">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="title">{{ config.name }}</h2>
|
<h1 class="title">{{ config.name }}</h1>
|
||||||
<p>{{ config.description }}</p>
|
<p>{{ config.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
</i18n>
|
</i18n>
|
||||||
</div>
|
</div>
|
||||||
<div class="column contact">
|
<div class="column contact">
|
||||||
<h4>{{ $t("Contact") }}</h4>
|
<p class="has-text-weight-bold">{{ $t("Contact") }}</p>
|
||||||
<instance-contact-link
|
<instance-contact-link
|
||||||
v-if="config && config.contact"
|
v-if="config && config.contact"
|
||||||
:contact="config.contact"
|
:contact="config.contact"
|
||||||
|
@ -32,13 +32,13 @@
|
||||||
<p v-else>{{ $t("No information") }}</p>
|
<p v-else>{{ $t("No information") }}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<section class="long-description content">
|
<section class="long-description content">
|
||||||
<div v-html="config.longDescription" />
|
<div v-html="config.longDescription" />
|
||||||
</section>
|
</section>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<section class="config">
|
<section class="config">
|
||||||
<h3 class="subtitle">{{ $t("Instance configuration") }}</h3>
|
<h2 class="subtitle">{{ $t("Instance configuration") }}</h2>
|
||||||
<table class="table is-fullwidth">
|
<table class="table is-fullwidth">
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ $t("Instance languages") }}</td>
|
<td>{{ $t("Instance languages") }}</td>
|
||||||
|
@ -168,7 +168,7 @@ section {
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hero {
|
&.hero {
|
||||||
h2.title {
|
h1.title {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,7 +195,7 @@ section {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contact {
|
.contact {
|
||||||
h4 {
|
h3 {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
|
|
|
@ -307,6 +307,14 @@ const MEMBERSHIPS_PER_PAGE = 10;
|
||||||
ActorCard,
|
ActorCard,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { person } = this;
|
||||||
|
return {
|
||||||
|
title: person ? person.name || usernameWithDomain(person) : "",
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class AdminProfile extends Vue {
|
export default class AdminProfile extends Vue {
|
||||||
@Prop({ required: true }) id!: string;
|
@Prop({ required: true }) id!: string;
|
||||||
|
|
|
@ -96,6 +96,14 @@ import { IPerson } from "../../types/actor";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { user } = this;
|
||||||
|
return {
|
||||||
|
title: user.email,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class AdminUserProfile extends Vue {
|
export default class AdminUserProfile extends Vue {
|
||||||
@Prop({ required: true }) id!: string;
|
@Prop({ required: true }) id!: string;
|
||||||
|
|
|
@ -87,6 +87,11 @@ import RouteName from "../../router/name";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Instance follows") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class Follows extends Vue {
|
export default class Follows extends Vue {
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<b-field :label="$t('Text')">
|
<b-field :label="$t('Text')">
|
||||||
<editor v-model="discussion.text" />
|
<editor v-model="discussion.text" :aria-label="$t('Comment body')" />
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<button class="button is-primary" type="submit">
|
<button class="button is-primary" type="submit">
|
||||||
|
|
|
@ -125,7 +125,7 @@
|
||||||
>
|
>
|
||||||
<form @submit.prevent="reply" v-if="!error">
|
<form @submit.prevent="reply" v-if="!error">
|
||||||
<b-field :label="$t('Text')">
|
<b-field :label="$t('Text')">
|
||||||
<editor v-model="newComment" />
|
<editor v-model="newComment" :aria-label="$t('Comment body')" />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-button
|
<b-button
|
||||||
native-type="submit"
|
native-type="submit"
|
||||||
|
|
|
@ -44,9 +44,10 @@
|
||||||
:placeholder="$t('Type or select a date…')"
|
:placeholder="$t('Type or select a date…')"
|
||||||
icon="calendar-today"
|
icon="calendar-today"
|
||||||
:locale="$i18n.locale"
|
:locale="$i18n.locale"
|
||||||
v-model="event.beginsOn"
|
v-model="beginsOn"
|
||||||
horizontal-time-picker
|
horizontal-time-picker
|
||||||
editable
|
editable
|
||||||
|
:tz-offset="tzOffset(beginsOn)"
|
||||||
:datepicker="{
|
:datepicker="{
|
||||||
id: 'begins-on-field',
|
id: 'begins-on-field',
|
||||||
'aria-next-label': $t('Next month'),
|
'aria-next-label': $t('Next month'),
|
||||||
|
@ -62,9 +63,10 @@
|
||||||
:placeholder="$t('Type or select a date…')"
|
:placeholder="$t('Type or select a date…')"
|
||||||
icon="calendar-today"
|
icon="calendar-today"
|
||||||
:locale="$i18n.locale"
|
:locale="$i18n.locale"
|
||||||
v-model="event.endsOn"
|
v-model="endsOn"
|
||||||
horizontal-time-picker
|
horizontal-time-picker
|
||||||
:min-datetime="event.beginsOn"
|
:min-datetime="beginsOn"
|
||||||
|
:tz-offset="tzOffset(endsOn)"
|
||||||
editable
|
editable
|
||||||
:datepicker="{
|
:datepicker="{
|
||||||
id: 'ends-on-field',
|
id: 'ends-on-field',
|
||||||
|
@ -75,16 +77,21 @@
|
||||||
</b-datetimepicker>
|
</b-datetimepicker>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
|
||||||
<!-- <b-switch v-model="endsOnNull">{{ $t('No end date') }}</b-switch>-->
|
|
||||||
<b-button type="is-text" @click="dateSettingsIsOpen = true">
|
<b-button type="is-text" @click="dateSettingsIsOpen = true">
|
||||||
{{ $t("Date parameters") }}
|
{{ $t("Date parameters") }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
|
||||||
<full-address-auto-complete v-model="event.physicalAddress" />
|
<full-address-auto-complete
|
||||||
|
v-model="eventPhysicalAddress"
|
||||||
|
:user-timezone="userActualTimezone"
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ $t("Description") }}</label>
|
<label class="label">{{ $t("Description") }}</label>
|
||||||
<editor v-model="event.description" />
|
<editor
|
||||||
|
v-model="event.description"
|
||||||
|
:aria-label="$t('Event description body')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<b-field :label="$t('Website / URL')" label-for="website-url">
|
<b-field :label="$t('Website / URL')" label-for="website-url">
|
||||||
|
@ -329,9 +336,45 @@
|
||||||
<form action>
|
<form action>
|
||||||
<div class="modal-card" style="width: auto">
|
<div class="modal-card" style="width: auto">
|
||||||
<header class="modal-card-head">
|
<header class="modal-card-head">
|
||||||
<p class="modal-card-title">{{ $t("Date and time settings") }}</p>
|
<h3 class="modal-card-title">{{ $t("Date and time settings") }}</h3>
|
||||||
</header>
|
</header>
|
||||||
<section class="modal-card-body">
|
<section class="modal-card-body">
|
||||||
|
<p>
|
||||||
|
{{
|
||||||
|
$t(
|
||||||
|
"Event timezone will default to the timezone of the event's address if there is one, or to your own timezone setting."
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<b-field :label="$t('Timezone')" label-for="timezone" expanded>
|
||||||
|
<b-select
|
||||||
|
:placeholder="$t('Select a timezone')"
|
||||||
|
:loading="!config"
|
||||||
|
v-model="timezone"
|
||||||
|
id="timezone"
|
||||||
|
>
|
||||||
|
<optgroup
|
||||||
|
:label="group"
|
||||||
|
v-for="(groupTimezones, group) in timezones"
|
||||||
|
:key="group"
|
||||||
|
>
|
||||||
|
<option
|
||||||
|
v-for="timezone in groupTimezones"
|
||||||
|
:value="`${group}/${timezone}`"
|
||||||
|
:key="timezone"
|
||||||
|
>
|
||||||
|
{{ sanitizeTimezone(timezone) }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
</b-select>
|
||||||
|
<b-button
|
||||||
|
:disabled="!timezone"
|
||||||
|
@click="timezone = null"
|
||||||
|
class="reset-area"
|
||||||
|
icon-left="close"
|
||||||
|
:title="$t('Clear timezone field')"
|
||||||
|
/>
|
||||||
|
</b-field>
|
||||||
<b-field :label="$t('Event page settings')">
|
<b-field :label="$t('Event page settings')">
|
||||||
<b-switch v-model="eventOptions.showStartTime">{{
|
<b-switch v-model="eventOptions.showStartTime">{{
|
||||||
$t("Show the time when the event begins")
|
$t("Show the time when the event begins")
|
||||||
|
@ -511,6 +554,7 @@ section {
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
import { Component, Prop, Vue, Watch } from "vue-property-decorator";
|
||||||
|
import { getTimezoneOffset } from "date-fns-tz";
|
||||||
import PictureUpload from "@/components/PictureUpload.vue";
|
import PictureUpload from "@/components/PictureUpload.vue";
|
||||||
import EditorComponent from "@/components/Editor.vue";
|
import EditorComponent from "@/components/Editor.vue";
|
||||||
import TagInput from "@/components/Event/TagInput.vue";
|
import TagInput from "@/components/Event/TagInput.vue";
|
||||||
|
@ -538,6 +582,7 @@ import {
|
||||||
} from "../../graphql/event";
|
} from "../../graphql/event";
|
||||||
import {
|
import {
|
||||||
EventModel,
|
EventModel,
|
||||||
|
IEditableEvent,
|
||||||
IEvent,
|
IEvent,
|
||||||
removeTypeName,
|
removeTypeName,
|
||||||
toEditJSON,
|
toEditJSON,
|
||||||
|
@ -563,7 +608,7 @@ import {
|
||||||
} from "../../utils/image";
|
} from "../../utils/image";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import "intersection-observer";
|
import "intersection-observer";
|
||||||
import { CONFIG } from "../../graphql/config";
|
import { CONFIG_EDIT_EVENT } from "../../graphql/config";
|
||||||
import { IConfig } from "../../types/config.model";
|
import { IConfig } from "../../types/config.model";
|
||||||
import {
|
import {
|
||||||
ApolloCache,
|
ApolloCache,
|
||||||
|
@ -572,6 +617,9 @@ import {
|
||||||
} from "@apollo/client/core";
|
} from "@apollo/client/core";
|
||||||
import cloneDeep from "lodash/cloneDeep";
|
import cloneDeep from "lodash/cloneDeep";
|
||||||
import { IEventOptions } from "@/types/event-options.model";
|
import { IEventOptions } from "@/types/event-options.model";
|
||||||
|
import { USER_SETTINGS } from "@/graphql/user";
|
||||||
|
import { IUser } from "@/types/current-user.model";
|
||||||
|
import { IAddress } from "@/types/address.model";
|
||||||
|
|
||||||
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
||||||
|
|
||||||
|
@ -588,7 +636,8 @@ const DEFAULT_LIMIT_NUMBER_OF_PLACES = 10;
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
currentActor: CURRENT_ACTOR_CLIENT,
|
currentActor: CURRENT_ACTOR_CLIENT,
|
||||||
config: CONFIG,
|
loggedUser: USER_SETTINGS,
|
||||||
|
config: CONFIG_EDIT_EVENT,
|
||||||
identities: IDENTITIES,
|
identities: IDENTITIES,
|
||||||
event: {
|
event: {
|
||||||
query: FETCH_EVENT,
|
query: FETCH_EVENT,
|
||||||
|
@ -640,9 +689,11 @@ export default class EditEvent extends Vue {
|
||||||
|
|
||||||
currentActor!: IActor;
|
currentActor!: IActor;
|
||||||
|
|
||||||
event: IEvent = new EventModel();
|
loggedUser!: IUser;
|
||||||
|
|
||||||
unmodifiedEvent: IEvent = new EventModel();
|
event: IEditableEvent = new EventModel();
|
||||||
|
|
||||||
|
unmodifiedEvent: IEditableEvent = new EventModel();
|
||||||
|
|
||||||
identities: IActor[] = [];
|
identities: IActor[] = [];
|
||||||
|
|
||||||
|
@ -668,8 +719,6 @@ export default class EditEvent extends Vue {
|
||||||
|
|
||||||
dateSettingsIsOpen = false;
|
dateSettingsIsOpen = false;
|
||||||
|
|
||||||
endsOnNull = false;
|
|
||||||
|
|
||||||
saving = false;
|
saving = false;
|
||||||
|
|
||||||
displayNameAndUsername = displayNameAndUsername;
|
displayNameAndUsername = displayNameAndUsername;
|
||||||
|
@ -905,7 +954,7 @@ export default class EditEvent extends Vue {
|
||||||
*/
|
*/
|
||||||
private postCreateOrUpdate(store: any, updateEvent: IEvent) {
|
private postCreateOrUpdate(store: any, updateEvent: IEvent) {
|
||||||
const resultEvent: IEvent = { ...updateEvent };
|
const resultEvent: IEvent = { ...updateEvent };
|
||||||
console.log(resultEvent);
|
console.debug("resultEvent", resultEvent);
|
||||||
if (!updateEvent.draft) {
|
if (!updateEvent.draft) {
|
||||||
store.writeQuery({
|
store.writeQuery({
|
||||||
query: EVENT_PERSON_PARTICIPATION,
|
query: EVENT_PERSON_PARTICIPATION,
|
||||||
|
@ -981,6 +1030,23 @@ export default class EditEvent extends Vue {
|
||||||
...toEditJSON(new EventModel(this.event)),
|
...toEditJSON(new EventModel(this.event)),
|
||||||
options: this.eventOptions,
|
options: this.eventOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.debug(this.event.beginsOn?.toISOString());
|
||||||
|
|
||||||
|
// if (this.event.beginsOn && this.timezone) {
|
||||||
|
// console.debug(
|
||||||
|
// "begins on should be",
|
||||||
|
// zonedTimeToUtc(this.event.beginsOn, this.timezone).toISOString()
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if (this.event.beginsOn && this.timezone) {
|
||||||
|
// res.beginsOn = zonedTimeToUtc(
|
||||||
|
// this.event.beginsOn,
|
||||||
|
// this.timezone
|
||||||
|
// ).toISOString();
|
||||||
|
// }
|
||||||
|
|
||||||
const organizerActor = this.event.organizerActor?.id
|
const organizerActor = this.event.organizerActor?.id
|
||||||
? this.event.organizerActor
|
? this.event.organizerActor
|
||||||
: this.organizerActor;
|
: this.organizerActor;
|
||||||
|
@ -992,10 +1058,6 @@ export default class EditEvent extends Vue {
|
||||||
: null;
|
: null;
|
||||||
res = { ...res, attributedToId };
|
res = { ...res, attributedToId };
|
||||||
|
|
||||||
if (this.endsOnNull) {
|
|
||||||
res.endsOn = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.pictureFile) {
|
if (this.pictureFile) {
|
||||||
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
const pictureObj = buildFileVariable(this.pictureFile, "picture");
|
||||||
res = { ...res, ...pictureObj };
|
res = { ...res, ...pictureObj };
|
||||||
|
@ -1116,13 +1178,16 @@ export default class EditEvent extends Vue {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get beginsOn(): Date {
|
get beginsOn(): Date | null {
|
||||||
|
// if (this.timezone && this.event.beginsOn) {
|
||||||
|
// return utcToZonedTime(this.event.beginsOn, this.timezone);
|
||||||
|
// }
|
||||||
return this.event.beginsOn;
|
return this.event.beginsOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Watch("beginsOn", { deep: true })
|
set beginsOn(beginsOn: Date | null) {
|
||||||
onBeginsOnChanged(beginsOn: string): void {
|
this.event.beginsOn = beginsOn;
|
||||||
if (!this.event.endsOn) return;
|
if (!this.event.endsOn || !beginsOn) return;
|
||||||
const dateBeginsOn = new Date(beginsOn);
|
const dateBeginsOn = new Date(beginsOn);
|
||||||
const dateEndsOn = new Date(this.event.endsOn);
|
const dateEndsOn = new Date(this.event.endsOn);
|
||||||
if (dateEndsOn < dateBeginsOn) {
|
if (dateEndsOn < dateBeginsOn) {
|
||||||
|
@ -1134,13 +1199,94 @@ export default class EditEvent extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get endsOn(): Date | null {
|
||||||
* In event endsOn datepicker, we lock starting with the day before the beginsOn date
|
// if (this.event.endsOn && this.timezone) {
|
||||||
*/
|
// return utcToZonedTime(this.event.endsOn, this.timezone);
|
||||||
get minDateForEndsOn(): Date {
|
// }
|
||||||
const minDate = new Date(this.event.beginsOn);
|
return this.event.endsOn;
|
||||||
minDate.setDate(minDate.getDate() - 1);
|
}
|
||||||
return minDate;
|
|
||||||
|
set endsOn(endsOn: Date | null) {
|
||||||
|
this.event.endsOn = endsOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezones(): Record<string, string[]> {
|
||||||
|
if (!this.config || !this.config.timezones) return {};
|
||||||
|
return this.config.timezones.reduce(
|
||||||
|
(acc: { [key: string]: Array<string> }, val: string) => {
|
||||||
|
const components = val.split("/");
|
||||||
|
const [prefix, suffix] = [
|
||||||
|
components.shift() as string,
|
||||||
|
components.join("/"),
|
||||||
|
];
|
||||||
|
const pushOrCreate = (
|
||||||
|
acc2: { [key: string]: Array<string> },
|
||||||
|
prefix2: string,
|
||||||
|
suffix2: string
|
||||||
|
) => {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
(acc2[prefix2] = acc2[prefix2] || []).push(suffix2);
|
||||||
|
return acc2;
|
||||||
|
};
|
||||||
|
if (suffix) {
|
||||||
|
return pushOrCreate(acc, prefix, suffix);
|
||||||
|
}
|
||||||
|
return pushOrCreate(acc, this.$t("Other") as string, prefix);
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
|
sanitizeTimezone(timezone: string): string {
|
||||||
|
return timezone
|
||||||
|
.split("_")
|
||||||
|
.join(" ")
|
||||||
|
.replace("St ", "St. ")
|
||||||
|
.split("/")
|
||||||
|
.join(" - ");
|
||||||
|
}
|
||||||
|
|
||||||
|
get timezone(): string | null {
|
||||||
|
return this.event.options.timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
set timezone(timezone: string | null) {
|
||||||
|
this.event.options = {
|
||||||
|
...this.event.options,
|
||||||
|
timezone,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
get userTimezone(): string | undefined {
|
||||||
|
return this.loggedUser?.settings?.timezone;
|
||||||
|
}
|
||||||
|
|
||||||
|
get userActualTimezone(): string {
|
||||||
|
if (this.userTimezone) {
|
||||||
|
return this.userTimezone;
|
||||||
|
}
|
||||||
|
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
|
tzOffset(date: Date): number {
|
||||||
|
if (this.timezone && date) {
|
||||||
|
const eventUTCOffset = getTimezoneOffset(this.timezone, date);
|
||||||
|
const localUTCOffset = getTimezoneOffset(this.userActualTimezone);
|
||||||
|
return (eventUTCOffset - localUTCOffset) / (60 * 1000);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventPhysicalAddress(): IAddress | null {
|
||||||
|
return this.event.physicalAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
set eventPhysicalAddress(address: IAddress | null) {
|
||||||
|
if (address && address.timezone) {
|
||||||
|
this.timezone = address.timezone;
|
||||||
|
}
|
||||||
|
this.event.physicalAddress = address;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -245,12 +245,14 @@
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
v-if="canManageEvent || event.draft"
|
v-if="canManageEvent || event.draft"
|
||||||
@click="openDeleteEventModalWrapper"
|
@click="openDeleteEventModalWrapper"
|
||||||
|
@keyup.enter="openDeleteEventModalWrapper"
|
||||||
>
|
>
|
||||||
{{ $t("Delete") }}
|
{{ $t("Delete") }}
|
||||||
<b-icon icon="delete" />
|
<b-icon icon="delete" />
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
|
|
||||||
<hr
|
<hr
|
||||||
|
role="presentation"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
aria-role="menuitem"
|
aria-role="menuitem"
|
||||||
v-if="canManageEvent || event.draft"
|
v-if="canManageEvent || event.draft"
|
||||||
|
@ -259,6 +261,7 @@
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
v-if="!event.draft"
|
v-if="!event.draft"
|
||||||
@click="triggerShare()"
|
@click="triggerShare()"
|
||||||
|
@keyup.enter="triggerShare()"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ $t("Share this event") }}
|
{{ $t("Share this event") }}
|
||||||
|
@ -268,6 +271,7 @@
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
@click="downloadIcsEvent()"
|
@click="downloadIcsEvent()"
|
||||||
|
@keyup.enter="downloadIcsEvent()"
|
||||||
v-if="!event.draft"
|
v-if="!event.draft"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
|
@ -279,6 +283,7 @@
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
v-if="ableToReport"
|
v-if="ableToReport"
|
||||||
@click="isReportModalActive = true"
|
@click="isReportModalActive = true"
|
||||||
|
@keyup.enter="isReportModalActive = true"
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{{ $t("Report") }}
|
{{ $t("Report") }}
|
||||||
|
@ -298,6 +303,8 @@
|
||||||
v-if="event && config"
|
v-if="event && config"
|
||||||
:event="event"
|
:event="event"
|
||||||
:config="config"
|
:config="config"
|
||||||
|
:user="loggedUser"
|
||||||
|
@showMapModal="showMap = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
@ -379,6 +386,7 @@
|
||||||
class="button"
|
class="button"
|
||||||
ref="cancelButton"
|
ref="cancelButton"
|
||||||
@click="isJoinModalActive = false"
|
@click="isJoinModalActive = false"
|
||||||
|
@keyup.enter="isJoinModalActive = false"
|
||||||
>
|
>
|
||||||
{{ $t("Cancel") }}
|
{{ $t("Cancel") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -390,6 +398,11 @@
|
||||||
? joinEventWithConfirmation(identity)
|
? joinEventWithConfirmation(identity)
|
||||||
: joinEvent(identity)
|
: joinEvent(identity)
|
||||||
"
|
"
|
||||||
|
@keyup.enter="
|
||||||
|
event.joinOptions === EventJoinOptions.RESTRICTED
|
||||||
|
? joinEventWithConfirmation(identity)
|
||||||
|
: joinEvent(identity)
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ $t("Confirm my particpation") }}
|
{{ $t("Confirm my particpation") }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -436,6 +449,7 @@
|
||||||
class="button"
|
class="button"
|
||||||
ref="cancelButton"
|
ref="cancelButton"
|
||||||
@click="isJoinConfirmationModalActive = false"
|
@click="isJoinConfirmationModalActive = false"
|
||||||
|
@keyup.enter="isJoinConfirmationModalActive = false"
|
||||||
>{{ $t("Cancel") }}
|
>{{ $t("Cancel") }}
|
||||||
</b-button>
|
</b-button>
|
||||||
<b-button type="is-primary" native-type="submit">
|
<b-button type="is-primary" native-type="submit">
|
||||||
|
@ -446,6 +460,22 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</b-modal>
|
</b-modal>
|
||||||
|
<b-modal
|
||||||
|
class="map-modal"
|
||||||
|
v-if="event.physicalAddress && event.physicalAddress.geom"
|
||||||
|
:active.sync="showMap"
|
||||||
|
has-modal-card
|
||||||
|
full-screen
|
||||||
|
:can-cancel="['escape', 'outside']"
|
||||||
|
>
|
||||||
|
<template #default="props">
|
||||||
|
<event-map
|
||||||
|
:routingType="routingType"
|
||||||
|
:address="event.physicalAddress"
|
||||||
|
@close="props.close"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</b-modal>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -496,11 +526,14 @@ import Subtitle from "../../components/Utils/Subtitle.vue";
|
||||||
import Tag from "../../components/Tag.vue";
|
import Tag from "../../components/Tag.vue";
|
||||||
import EventMetadataSidebar from "../../components/Event/EventMetadataSidebar.vue";
|
import EventMetadataSidebar from "../../components/Event/EventMetadataSidebar.vue";
|
||||||
import EventBanner from "../../components/Event/EventBanner.vue";
|
import EventBanner from "../../components/Event/EventBanner.vue";
|
||||||
|
import EventMap from "../../components/Event/EventMap.vue";
|
||||||
import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
|
import PopoverActorCard from "../../components/Account/PopoverActorCard.vue";
|
||||||
import { IParticipant } from "../../types/participant.model";
|
import { IParticipant } from "../../types/participant.model";
|
||||||
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
import { ApolloCache, FetchResult } from "@apollo/client/core";
|
||||||
import { IEventMetadataDescription } from "@/types/event-metadata";
|
import { IEventMetadataDescription } from "@/types/event-metadata";
|
||||||
import { eventMetaDataList } from "../../services/EventMetadata";
|
import { eventMetaDataList } from "../../services/EventMetadata";
|
||||||
|
import { USER_SETTINGS } from "@/graphql/user";
|
||||||
|
import { IUser } from "@/types/current-user.model";
|
||||||
|
|
||||||
// noinspection TypeScriptValidateTypes
|
// noinspection TypeScriptValidateTypes
|
||||||
@Component({
|
@Component({
|
||||||
|
@ -517,6 +550,7 @@ import { eventMetaDataList } from "../../services/EventMetadata";
|
||||||
PopoverActorCard,
|
PopoverActorCard,
|
||||||
EventBanner,
|
EventBanner,
|
||||||
EventMetadataSidebar,
|
EventMetadataSidebar,
|
||||||
|
EventMap,
|
||||||
ShareEventModal: () =>
|
ShareEventModal: () =>
|
||||||
import(
|
import(
|
||||||
/* webpackChunkName: "shareEventModal" */ "../../components/Event/ShareEventModal.vue"
|
/* webpackChunkName: "shareEventModal" */ "../../components/Event/ShareEventModal.vue"
|
||||||
|
@ -555,9 +589,8 @@ import { eventMetaDataList } from "../../services/EventMetadata";
|
||||||
this.handleErrors(graphQLErrors);
|
this.handleErrors(graphQLErrors);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
currentActor: {
|
currentActor: CURRENT_ACTOR_CLIENT,
|
||||||
query: CURRENT_ACTOR_CLIENT,
|
loggedUser: USER_SETTINGS,
|
||||||
},
|
|
||||||
participations: {
|
participations: {
|
||||||
query: EVENT_PERSON_PARTICIPATION,
|
query: EVENT_PERSON_PARTICIPATION,
|
||||||
fetchPolicy: "cache-and-network",
|
fetchPolicy: "cache-and-network",
|
||||||
|
@ -634,6 +667,8 @@ export default class Event extends EventMixin {
|
||||||
|
|
||||||
person!: IPerson;
|
person!: IPerson;
|
||||||
|
|
||||||
|
loggedUser!: IUser;
|
||||||
|
|
||||||
participations: IParticipant[] = [];
|
participations: IParticipant[] = [];
|
||||||
|
|
||||||
oldParticipationRole!: string;
|
oldParticipationRole!: string;
|
||||||
|
@ -1118,6 +1153,12 @@ export default class Event extends EventMixin {
|
||||||
return acc;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
showMap = false;
|
||||||
|
|
||||||
|
get routingType(): string | undefined {
|
||||||
|
return this.config?.maps?.routing?.type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
|
|
|
@ -62,13 +62,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
|
has-link
|
||||||
v-for="format in exportFormats"
|
v-for="format in exportFormats"
|
||||||
:key="format"
|
:key="format"
|
||||||
@click="exportParticipants(format)"
|
|
||||||
aria-role="listitem"
|
aria-role="listitem"
|
||||||
|
@click="exportParticipants(format)"
|
||||||
|
@keyup.enter="exportParticipants(format)"
|
||||||
>
|
>
|
||||||
<b-icon :icon="formatToIcon(format)"></b-icon>
|
<button class="dropdown-button">
|
||||||
{{ format }}
|
<b-icon :icon="formatToIcon(format)"></b-icon>
|
||||||
|
{{ format }}
|
||||||
|
</button>
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
</b-dropdown>
|
</b-dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
@ -566,4 +570,21 @@ nav.breadcrumb {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button.dropdown-button {
|
||||||
|
&:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #0a0a0a;
|
||||||
|
}
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #4a4a4a;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 0.375rem 1rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -203,6 +203,7 @@
|
||||||
</span>
|
</span>
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
<hr
|
<hr
|
||||||
|
role="presentation"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
v-if="isCurrentActorAGroupMember"
|
v-if="isCurrentActorAGroupMember"
|
||||||
/>
|
/>
|
||||||
|
@ -224,7 +225,7 @@
|
||||||
{{ $t("ICS/WebCal Feed") }}
|
{{ $t("ICS/WebCal Feed") }}
|
||||||
</a>
|
</a>
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
<hr class="dropdown-divider" />
|
<hr role="presentation" class="dropdown-divider" />
|
||||||
<b-dropdown-item
|
<b-dropdown-item
|
||||||
v-if="ableToReport"
|
v-if="ableToReport"
|
||||||
aria-role="menuitem"
|
aria-role="menuitem"
|
||||||
|
|
|
@ -41,7 +41,11 @@
|
||||||
<b-input v-model="editableGroup.name" id="group-settings-name" />
|
<b-input v-model="editableGroup.name" id="group-settings-name" />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field :label="$t('Group short description')">
|
<b-field :label="$t('Group short description')">
|
||||||
<editor mode="basic" v-model="editableGroup.summary" :maxSize="500"
|
<editor
|
||||||
|
mode="basic"
|
||||||
|
v-model="editableGroup.summary"
|
||||||
|
:maxSize="500"
|
||||||
|
:aria-label="$t('Group description body')"
|
||||||
/></b-field>
|
/></b-field>
|
||||||
<b-field :label="$t('Avatar')">
|
<b-field :label="$t('Avatar')">
|
||||||
<picture-upload
|
<picture-upload
|
||||||
|
|
|
@ -39,6 +39,11 @@ import SettingMenuItem from "../../components/Settings/SettingMenuItem.vue";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
components: { SettingMenuSection, SettingMenuItem },
|
components: { SettingMenuSection, SettingMenuItem },
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Group settings") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class Settings extends mixins(GroupMixin) {
|
export default class Settings extends mixins(GroupMixin) {
|
||||||
RouteName = RouteName;
|
RouteName = RouteName;
|
||||||
|
|
|
@ -241,6 +241,7 @@
|
||||||
</span>
|
</span>
|
||||||
</section>
|
</section>
|
||||||
<hr
|
<hr
|
||||||
|
role="presentation"
|
||||||
class="home-separator"
|
class="home-separator"
|
||||||
v-if="canShowMyUpcomingEvents && canShowLastWeekEvents"
|
v-if="canShowMyUpcomingEvents && canShowLastWeekEvents"
|
||||||
/>
|
/>
|
||||||
|
@ -259,6 +260,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<hr
|
<hr
|
||||||
|
role="presentation"
|
||||||
class="home-separator"
|
class="home-separator"
|
||||||
v-if="canShowLastWeekEvents && canShowCloseEvents"
|
v-if="canShowLastWeekEvents && canShowCloseEvents"
|
||||||
/>
|
/>
|
||||||
|
@ -297,6 +299,7 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<hr
|
<hr
|
||||||
|
role="presentation"
|
||||||
class="home-separator"
|
class="home-separator"
|
||||||
v-if="
|
v-if="
|
||||||
canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents
|
canShowMyUpcomingEvents || canShowLastWeekEvents || canShowCloseEvents
|
||||||
|
|
|
@ -395,6 +395,11 @@ import { Paginate } from "@/types/paginate";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Moderation logs") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class ReportList extends Vue {
|
export default class ReportList extends Vue {
|
||||||
actionLogs?: Paginate<IActionLog> = { total: 0, elements: [] };
|
actionLogs?: Paginate<IActionLog> = { total: 0, elements: [] };
|
||||||
|
|
|
@ -111,6 +111,11 @@ const REPORT_PAGE_LIMIT = 10;
|
||||||
pollInterval: 120000, // 2 minutes
|
pollInterval: 120000, // 2 minutes
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Reports") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class ReportList extends Vue {
|
export default class ReportList extends Vue {
|
||||||
reports?: Paginate<IReport> = { elements: [], total: 0 };
|
reports?: Paginate<IReport> = { elements: [], total: 0 };
|
||||||
|
|
|
@ -72,7 +72,7 @@
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label class="label">{{ $t("Post") }}</label>
|
<label class="label">{{ $t("Post") }}</label>
|
||||||
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
|
<p v-if="errors.body" class="help is-danger">{{ errors.body }}</p>
|
||||||
<editor v-model="editablePost.body" />
|
<editor v-model="editablePost.body" :aria-label="$t('Post body')" />
|
||||||
</div>
|
</div>
|
||||||
<subtitle>{{ $t("Who can view this post") }}</subtitle>
|
<subtitle>{{ $t("Who can view this post") }}</subtitle>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
|
|
|
@ -127,11 +127,13 @@ const POSTS_PAGE_LIMIT = 10;
|
||||||
PostElementItem,
|
PostElementItem,
|
||||||
},
|
},
|
||||||
metaInfo() {
|
metaInfo() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { group } = this;
|
||||||
return {
|
return {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
title: this.$t("{group} posts", {
|
||||||
// @ts-ignore
|
group: group.name || usernameWithDomain(group),
|
||||||
title: this.$t("My groups") as string,
|
}) as string,
|
||||||
titleTemplate: "%s | Mobilizon",
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
:title="
|
:title="
|
||||||
$options.filters.formatDateTimeString(
|
$options.filters.formatDateTimeString(
|
||||||
post.updatedAt,
|
post.updatedAt,
|
||||||
|
undefined,
|
||||||
true,
|
true,
|
||||||
'short'
|
'short'
|
||||||
)
|
)
|
||||||
|
|
|
@ -58,6 +58,7 @@
|
||||||
{{ $t("New link") }}
|
{{ $t("New link") }}
|
||||||
</b-dropdown-item>
|
</b-dropdown-item>
|
||||||
<hr
|
<hr
|
||||||
|
role="presentation"
|
||||||
class="dropdown-divider"
|
class="dropdown-divider"
|
||||||
v-if="config.resourceProviders.length"
|
v-if="config.resourceProviders.length"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -18,12 +18,16 @@
|
||||||
<div class="setting-title">
|
<div class="setting-title">
|
||||||
<h2>{{ $t("Browser notifications") }}</h2>
|
<h2>{{ $t("Browser notifications") }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<b-button v-if="subscribed" @click="unsubscribeToWebPush()">{{
|
<b-button
|
||||||
$t("Unsubscribe to browser push notifications")
|
v-if="subscribed"
|
||||||
}}</b-button>
|
@click="unsubscribeToWebPush()"
|
||||||
|
@keyup.enter="unsubscribeToWebPush()"
|
||||||
|
>{{ $t("Unsubscribe to browser push notifications") }}</b-button
|
||||||
|
>
|
||||||
<b-button
|
<b-button
|
||||||
icon-left="rss"
|
icon-left="rss"
|
||||||
@click="subscribeToWebPush"
|
@click="subscribeToWebPush"
|
||||||
|
@keyup.enter="subscribeToWebPush"
|
||||||
v-else-if="canShowWebPush && webPushEnabled"
|
v-else-if="canShowWebPush && webPushEnabled"
|
||||||
>{{ $t("Activate browser push notifications") }}</b-button
|
>{{ $t("Activate browser push notifications") }}</b-button
|
||||||
>
|
>
|
||||||
|
@ -247,6 +251,9 @@
|
||||||
@click="
|
@click="
|
||||||
(e) => copyURL(e, tokenToURL(feedToken.token, 'atom'), 'atom')
|
(e) => copyURL(e, tokenToURL(feedToken.token, 'atom'), 'atom')
|
||||||
"
|
"
|
||||||
|
@keyup.enter="
|
||||||
|
(e) => copyURL(e, tokenToURL(feedToken.token, 'atom'), 'atom')
|
||||||
|
"
|
||||||
:href="tokenToURL(feedToken.token, 'atom')"
|
:href="tokenToURL(feedToken.token, 'atom')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>{{ $t("RSS/Atom Feed") }}</b-button
|
>{{ $t("RSS/Atom Feed") }}</b-button
|
||||||
|
@ -264,6 +271,9 @@
|
||||||
@click="
|
@click="
|
||||||
(e) => copyURL(e, tokenToURL(feedToken.token, 'ics'), 'ics')
|
(e) => copyURL(e, tokenToURL(feedToken.token, 'ics'), 'ics')
|
||||||
"
|
"
|
||||||
|
@keyup.enter="
|
||||||
|
(e) => copyURL(e, tokenToURL(feedToken.token, 'ics'), 'ics')
|
||||||
|
"
|
||||||
icon-left="calendar-sync"
|
icon-left="calendar-sync"
|
||||||
:href="tokenToURL(feedToken.token, 'ics')"
|
:href="tokenToURL(feedToken.token, 'ics')"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -274,6 +284,7 @@
|
||||||
icon-left="refresh"
|
icon-left="refresh"
|
||||||
type="is-text"
|
type="is-text"
|
||||||
@click="openRegenerateFeedTokensConfirmation"
|
@click="openRegenerateFeedTokensConfirmation"
|
||||||
|
@keyup.enter="openRegenerateFeedTokensConfirmation"
|
||||||
>{{ $t("Regenerate new links") }}</b-button
|
>{{ $t("Regenerate new links") }}</b-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -283,6 +294,7 @@
|
||||||
icon-left="refresh"
|
icon-left="refresh"
|
||||||
type="is-text"
|
type="is-text"
|
||||||
@click="generateFeedTokens"
|
@click="generateFeedTokens"
|
||||||
|
@keyup.enter="generateFeedTokens"
|
||||||
>{{ $t("Create new links") }}</b-button
|
>{{ $t("Create new links") }}</b-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
@ -333,7 +345,7 @@ type NotificationType = { label: string; subtypes: NotificationSubType[] };
|
||||||
},
|
},
|
||||||
metaInfo() {
|
metaInfo() {
|
||||||
return {
|
return {
|
||||||
title: this.$t("Notifications") as string,
|
title: this.$t("Notification settings") as string,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -61,7 +61,7 @@
|
||||||
<b-message v-else type="is-danger">{{
|
<b-message v-else type="is-danger">{{
|
||||||
$t("Unable to detect timezone.")
|
$t("Unable to detect timezone.")
|
||||||
}}</b-message>
|
}}</b-message>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<b-field grouped>
|
<b-field grouped>
|
||||||
<b-field
|
<b-field
|
||||||
:label="$t('City or region')"
|
:label="$t('City or region')"
|
||||||
|
@ -95,6 +95,7 @@
|
||||||
<b-button
|
<b-button
|
||||||
:disabled="address == undefined"
|
:disabled="address == undefined"
|
||||||
@click="resetArea"
|
@click="resetArea"
|
||||||
|
@keyup.enter="resetArea"
|
||||||
class="reset-area"
|
class="reset-area"
|
||||||
icon-left="close"
|
icon-left="close"
|
||||||
:aria-label="$t('Reset')"
|
:aria-label="$t('Reset')"
|
||||||
|
|
|
@ -55,6 +55,14 @@ import RouteName from "../../router/name";
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { todo } = this;
|
||||||
|
return {
|
||||||
|
title: todo.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class Todo extends Vue {
|
export default class Todo extends Vue {
|
||||||
@Prop({ type: String, required: true }) todoId!: string;
|
@Prop({ type: String, required: true }) todoId!: string;
|
||||||
|
|
|
@ -69,6 +69,14 @@ import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
|
||||||
},
|
},
|
||||||
currentActor: CURRENT_ACTOR_CLIENT,
|
currentActor: CURRENT_ACTOR_CLIENT,
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { todoList } = this;
|
||||||
|
return {
|
||||||
|
title: todoList.title,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class TodoList extends Vue {
|
export default class TodoList extends Vue {
|
||||||
@Prop({ type: String, required: true }) id!: string;
|
@Prop({ type: String, required: true }) id!: string;
|
||||||
|
|
|
@ -82,6 +82,16 @@ import RouteName from "../../router/name";
|
||||||
components: {
|
components: {
|
||||||
CompactTodo,
|
CompactTodo,
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
const { group } = this;
|
||||||
|
return {
|
||||||
|
title: this.$t("{group}'s todolists", {
|
||||||
|
group: group.name || usernameWithDomain(group),
|
||||||
|
}) as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class TodoLists extends Vue {
|
export default class TodoLists extends Vue {
|
||||||
@Prop({ type: String, required: true }) preferredUsername!: string;
|
@Prop({ type: String, required: true }) preferredUsername!: string;
|
||||||
|
|
|
@ -24,7 +24,13 @@ import { VALIDATE_EMAIL } from "../../graphql/user";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
import { ICurrentUser } from "../../types/current-user.model";
|
import { ICurrentUser } from "../../types/current-user.model";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Validating email") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class Validate extends Vue {
|
export default class Validate extends Vue {
|
||||||
@Prop({ type: String, required: true }) token!: string;
|
@Prop({ type: String, required: true }) token!: string;
|
||||||
|
|
||||||
|
|
|
@ -50,7 +50,13 @@ import { saveUserData } from "../../utils/auth";
|
||||||
import { ILogin } from "../../types/login.model";
|
import { ILogin } from "../../types/login.model";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Password reset") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class PasswordReset extends Vue {
|
export default class PasswordReset extends Vue {
|
||||||
@Prop({ type: String, required: true }) token!: string;
|
@Prop({ type: String, required: true }) token!: string;
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,13 @@ import RouteName from "../../router/name";
|
||||||
import { saveUserData, changeIdentity } from "../../utils/auth";
|
import { saveUserData, changeIdentity } from "../../utils/auth";
|
||||||
import { IUser } from "../../types/current-user.model";
|
import { IUser } from "../../types/current-user.model";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Redirecting to Mobilizon") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class ProviderValidate extends Vue {
|
export default class ProviderValidate extends Vue {
|
||||||
async mounted(): Promise<void> {
|
async mounted(): Promise<void> {
|
||||||
const accessToken = this.getValueFromMeta("auth-access-token");
|
const accessToken = this.getValueFromMeta("auth-access-token");
|
||||||
|
|
|
@ -59,7 +59,7 @@
|
||||||
<router-link class="out" :to="{ name: RouteName.ABOUT }">{{
|
<router-link class="out" :to="{ name: RouteName.ABOUT }">{{
|
||||||
$t("Learn more")
|
$t("Learn more")
|
||||||
}}</router-link>
|
}}</router-link>
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<subtitle>{{
|
<subtitle>{{
|
||||||
$t("About {instance}", { instance: config.name })
|
$t("About {instance}", { instance: config.name })
|
||||||
|
@ -170,7 +170,7 @@
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<hr />
|
<hr role="presentation" />
|
||||||
<div
|
<div
|
||||||
class="control"
|
class="control"
|
||||||
v-if="config && config.auth.oauthProviders.length > 0"
|
v-if="config && config.auth.oauthProviders.length > 0"
|
||||||
|
|
|
@ -56,7 +56,13 @@ import {
|
||||||
import { RESEND_CONFIRMATION_EMAIL } from "../../graphql/auth";
|
import { RESEND_CONFIRMATION_EMAIL } from "../../graphql/auth";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Resend confirmation email") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class ResendConfirmation extends Vue {
|
export default class ResendConfirmation extends Vue {
|
||||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,13 @@ import {
|
||||||
import { SEND_RESET_PASSWORD } from "../../graphql/auth";
|
import { SEND_RESET_PASSWORD } from "../../graphql/auth";
|
||||||
import RouteName from "../../router/name";
|
import RouteName from "../../router/name";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Reset password") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class SendPasswordReset extends Vue {
|
export default class SendPasswordReset extends Vue {
|
||||||
@Prop({ type: String, required: false, default: "" }) email!: string;
|
@Prop({ type: String, required: false, default: "" }) email!: string;
|
||||||
|
|
||||||
|
|
|
@ -66,6 +66,11 @@ import { IConfig } from "../../types/config.model";
|
||||||
apollo: {
|
apollo: {
|
||||||
config: TIMEZONES,
|
config: TIMEZONES,
|
||||||
},
|
},
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("First steps") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
})
|
})
|
||||||
export default class SettingsOnboard extends Vue {
|
export default class SettingsOnboard extends Vue {
|
||||||
@Prop({ required: false, default: 1, type: Number }) step!: number;
|
@Prop({ required: false, default: 1, type: Number }) step!: number;
|
||||||
|
|
|
@ -29,7 +29,13 @@ import RouteName from "../../router/name";
|
||||||
import { saveUserData, saveTokenData, changeIdentity } from "../../utils/auth";
|
import { saveUserData, saveTokenData, changeIdentity } from "../../utils/auth";
|
||||||
import { ILogin } from "../../types/login.model";
|
import { ILogin } from "../../types/login.model";
|
||||||
|
|
||||||
@Component
|
@Component({
|
||||||
|
metaInfo() {
|
||||||
|
return {
|
||||||
|
title: this.$t("Validating account") as string,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
})
|
||||||
export default class Validate extends Vue {
|
export default class Validate extends Vue {
|
||||||
@Prop({ type: String, required: true }) token!: string;
|
@Prop({ type: String, required: true }) token!: string;
|
||||||
|
|
||||||
|
|
|
@ -4,11 +4,15 @@ import App from "@/App.vue";
|
||||||
import VueRouter from "vue-router";
|
import VueRouter from "vue-router";
|
||||||
import Buefy from "buefy";
|
import Buefy from "buefy";
|
||||||
import flushPromises from "flush-promises";
|
import flushPromises from "flush-promises";
|
||||||
|
import VueAnnouncer from "@vue-a11y/announcer";
|
||||||
|
import VueSkipTo from "@vue-a11y/skip-to";
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
const localVue = createLocalVue();
|
||||||
config.mocks.$t = (key: string): string => key;
|
config.mocks.$t = (key: string): string => key;
|
||||||
localVue.use(VueRouter);
|
localVue.use(VueRouter);
|
||||||
localVue.use(Buefy);
|
localVue.use(Buefy);
|
||||||
|
localVue.use(VueAnnouncer);
|
||||||
|
localVue.use(VueSkipTo);
|
||||||
|
|
||||||
describe("routing", () => {
|
describe("routing", () => {
|
||||||
test("Homepage", async () => {
|
test("Homepage", async () => {
|
||||||
|
|
|
@ -12,7 +12,7 @@ exports[`CommentTree renders a comment tree with comments 1`] = `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<editor-stub mode="comment" value=""></editor-stub>
|
<editor-stub mode="comment" aria-label="Comment body" value=""></editor-stub>
|
||||||
</p>
|
</p>
|
||||||
<!---->
|
<!---->
|
||||||
</div>
|
</div>
|
||||||
|
@ -54,7 +54,7 @@ exports[`CommentTree renders an empty comment tree 1`] = `
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<p class="control">
|
<p class="control">
|
||||||
<editor-stub mode="comment" value=""></editor-stub>
|
<editor-stub mode="comment" aria-label="Comment body" value=""></editor-stub>
|
||||||
</p>
|
</p>
|
||||||
<!---->
|
<!---->
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -59,7 +59,7 @@ describe("PostElementItem", () => {
|
||||||
postData.title
|
postData.title
|
||||||
);
|
);
|
||||||
expect(wrapper.find(".metadata").text()).toContain(
|
expect(wrapper.find(".metadata").text()).toContain(
|
||||||
formatDateTimeString(postData.insertedAt, false)
|
formatDateTimeString(postData.insertedAt, undefined, false)
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.find(".metadata small").text()).not.toContain("Public");
|
expect(wrapper.find(".metadata small").text()).not.toContain("Public");
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`App component renders a Vue component 1`] = `<b-navbar-stub type="is-secondary" wrapperclass="container" closeonclick="true" mobileburger="true"><template></template> <template></template> <template></template></b-navbar-stub>`;
|
exports[`App component renders a Vue component 1`] = `<b-navbar-stub type="is-secondary" wrapperclass="container" closeonclick="true" mobileburger="true" id="navbar"><template></template> <template></template> <template></template></b-navbar-stub>`;
|
||||||
|
|
|
@ -18,7 +18,14 @@
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["src/*"]
|
"@/*": ["src/*"]
|
||||||
},
|
},
|
||||||
"lib": ["esnext", "dom", "dom.iterable", "scripthost", "webworker"]
|
"lib": [
|
||||||
|
"esnext",
|
||||||
|
"dom",
|
||||||
|
"es2017.intl",
|
||||||
|
"dom.iterable",
|
||||||
|
"scripthost",
|
||||||
|
"webworker"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
|
|
15
js/yarn.lock
15
js/yarn.lock
|
@ -2254,6 +2254,16 @@
|
||||||
"@typescript-eslint/types" "4.33.0"
|
"@typescript-eslint/types" "4.33.0"
|
||||||
eslint-visitor-keys "^2.0.0"
|
eslint-visitor-keys "^2.0.0"
|
||||||
|
|
||||||
|
"@vue-a11y/announcer@^2.1.0":
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vue-a11y/announcer/-/announcer-2.1.0.tgz#ed725e90b91870c76285840e0aaa637cfafbf27f"
|
||||||
|
integrity sha512-7V1osiJQxZPBA+2duF2nZugwgbIba/yvKLsHWvGIBJGY0VZhR4vYyFH3VFyFH2Yi46tHEVBN+X+a0uQaJMhCsQ==
|
||||||
|
|
||||||
|
"@vue-a11y/skip-to@^2.1.2":
|
||||||
|
version "2.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vue-a11y/skip-to/-/skip-to-2.1.2.tgz#a50f5b97605f5054ca7a7e222bdc721405e38f38"
|
||||||
|
integrity sha512-oKx0YE/nfWSBI48RKHoRsUmpY0//rDPBZTGk1LxhkkUsXPwzuMlJBFrWGIswdd+3DiIE5OyiDaM45ZldYjtDIA==
|
||||||
|
|
||||||
"@vue/apollo-option@4.0.0-alpha.11":
|
"@vue/apollo-option@4.0.0-alpha.11":
|
||||||
version "4.0.0-alpha.11"
|
version "4.0.0-alpha.11"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/apollo-option/-/apollo-option-4.0.0-alpha.11.tgz#b4ecac2d1ac40271cb7f20683fb8e4c85974329a"
|
resolved "https://registry.yarnpkg.com/@vue/apollo-option/-/apollo-option-4.0.0-alpha.11.tgz#b4ecac2d1ac40271cb7f20683fb8e4c85974329a"
|
||||||
|
@ -4211,6 +4221,11 @@ data-urls@^2.0.0:
|
||||||
whatwg-mimetype "^2.3.0"
|
whatwg-mimetype "^2.3.0"
|
||||||
whatwg-url "^8.0.0"
|
whatwg-url "^8.0.0"
|
||||||
|
|
||||||
|
date-fns-tz@^1.1.6:
|
||||||
|
version "1.1.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-1.1.6.tgz#93cbf354e2aeb2cd312ffa32e462c1943cf20a8e"
|
||||||
|
integrity sha512-nyy+URfFI3KUY7udEJozcoftju+KduaqkVfwyTIE0traBiVye09QnyWKLZK7drRr5h9B7sPJITmQnS3U6YOdQg==
|
||||||
|
|
||||||
date-fns@^2.16.0:
|
date-fns@^2.16.0:
|
||||||
version "2.25.0"
|
version "2.25.0"
|
||||||
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
|
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.25.0.tgz#8c5c8f1d958be3809a9a03f4b742eba894fc5680"
|
||||||
|
|
|
@ -49,7 +49,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
||||||
Create an actor locally by its URL (AP ID)
|
Create an actor locally by its URL (AP ID)
|
||||||
"""
|
"""
|
||||||
@spec make_actor_from_url(url :: String.t(), preload :: boolean()) ::
|
@spec make_actor_from_url(url :: String.t(), preload :: boolean()) ::
|
||||||
{:ok, Actor.t()} | {:error, make_actor_errors}
|
{:ok, Actor.t()} | {:error, make_actor_errors | Ecto.Changeset.t()}
|
||||||
def make_actor_from_url(url, preload \\ false) do
|
def make_actor_from_url(url, preload \\ false) do
|
||||||
if are_same_origin?(url, Endpoint.url()) do
|
if are_same_origin?(url, Endpoint.url()) do
|
||||||
{:error, :actor_is_local}
|
{:error, :actor_is_local}
|
||||||
|
@ -63,7 +63,7 @@ defmodule Mobilizon.Federation.ActivityPub.Actor do
|
||||||
Logger.info("Actor #{url} was deleted")
|
Logger.info("Actor #{url} was deleted")
|
||||||
{:error, :actor_deleted}
|
{:error, :actor_deleted}
|
||||||
|
|
||||||
{:error, err} when err in [:http_error, :json_decode_error] ->
|
{:error, err} ->
|
||||||
{:error, err}
|
{:error, err}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -35,7 +35,7 @@ defmodule Mobilizon.Federation.ActivityPub.Permission do
|
||||||
@doc """
|
@doc """
|
||||||
Check that actor can create such an object
|
Check that actor can create such an object
|
||||||
"""
|
"""
|
||||||
@spec can_create_group_object?(String.t() | integer(), String.t() | integer(), Entity.t()) ::
|
@spec can_create_group_object?(String.t() | integer(), String.t() | integer(), struct()) ::
|
||||||
boolean()
|
boolean()
|
||||||
def can_create_group_object?(
|
def can_create_group_object?(
|
||||||
actor_id,
|
actor_id,
|
||||||
|
|
|
@ -156,7 +156,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
|
||||||
role
|
role
|
||||||
)
|
)
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = err} ->
|
{:error, _, %Ecto.Changeset{} = err, _} ->
|
||||||
{:error, err}
|
{:error, err}
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
|
|
|
@ -12,7 +12,8 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||||
@doc """
|
@doc """
|
||||||
Create an event
|
Create an event
|
||||||
"""
|
"""
|
||||||
@spec create_event(map) :: {:ok, Activity.t(), Event.t()} | any
|
@spec create_event(map) ::
|
||||||
|
{:ok, Activity.t(), Event.t()} | {:error, atom() | Ecto.Changeset.t()}
|
||||||
def create_event(args) do
|
def create_event(args) do
|
||||||
# For now we don't federate drafts but it will be needed if we want to edit them as groups
|
# For now we don't federate drafts but it will be needed if we want to edit them as groups
|
||||||
Actions.Create.create(:event, prepare_args(args), should_federate(args))
|
Actions.Create.create(:event, prepare_args(args), should_federate(args))
|
||||||
|
@ -21,7 +22,8 @@ defmodule Mobilizon.GraphQL.API.Events do
|
||||||
@doc """
|
@doc """
|
||||||
Update an event
|
Update an event
|
||||||
"""
|
"""
|
||||||
@spec update_event(map, Event.t()) :: {:ok, Activity.t(), Event.t()} | any
|
@spec update_event(map, Event.t()) ::
|
||||||
|
{:ok, Activity.t(), Event.t()} | {:error, atom | Ecto.Changeset.t()}
|
||||||
def update_event(args, %Event{} = event) do
|
def update_event(args, %Event{} = event) do
|
||||||
Actions.Update.update(event, prepare_args(args), should_federate(args))
|
Actions.Update.update(event, prepare_args(args), should_federate(args))
|
||||||
end
|
end
|
||||||
|
|
|
@ -16,7 +16,9 @@ defmodule Mobilizon.GraphQL.Middleware.CurrentActorProvider do
|
||||||
_config
|
_config
|
||||||
) do
|
) do
|
||||||
case Cachex.fetch(:default_actors, to_string(user_id), fn -> default(user) end) do
|
case Cachex.fetch(:default_actors, to_string(user_id), fn -> default(user) end) do
|
||||||
{status, %Actor{} = current_actor} when status in [:ok, :commit] ->
|
{status, %Actor{preferred_username: preferred_username} = current_actor}
|
||||||
|
when status in [:ok, :commit] ->
|
||||||
|
Sentry.Context.set_user_context(%{name: preferred_username})
|
||||||
context = Map.put(context, :current_actor, current_actor)
|
context = Map.put(context, :current_actor, current_actor)
|
||||||
%Absinthe.Resolution{resolution | context: context}
|
%Absinthe.Resolution{resolution | context: context}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Address do
|
||||||
@doc """
|
@doc """
|
||||||
Search an address
|
Search an address
|
||||||
"""
|
"""
|
||||||
@spec search(map, map, map) :: {:ok, [Address.t()]}
|
@spec search(map, map, map) :: {:ok, [map()]}
|
||||||
def search(
|
def search(
|
||||||
_parent,
|
_parent,
|
||||||
%{query: query, locale: locale, page: _page, limit: _limit} = args,
|
%{query: query, locale: locale, page: _page, limit: _limit} = args,
|
||||||
|
|
|
@ -13,9 +13,11 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
|
|
||||||
alias Mobilizon.Federation.ActivityPub.Activity
|
alias Mobilizon.Federation.ActivityPub.Activity
|
||||||
alias Mobilizon.Federation.ActivityPub.Permission
|
alias Mobilizon.Federation.ActivityPub.Permission
|
||||||
|
alias Mobilizon.Service.TimezoneDetector
|
||||||
import Mobilizon.Users.Guards, only: [is_moderator: 1]
|
import Mobilizon.Users.Guards, only: [is_moderator: 1]
|
||||||
import Mobilizon.Web.Gettext
|
import Mobilizon.Web.Gettext
|
||||||
import Mobilizon.GraphQL.Resolvers.Event.Utils
|
import Mobilizon.GraphQL.Resolvers.Event.Utils
|
||||||
|
require Logger
|
||||||
|
|
||||||
# We limit the max number of events that can be retrieved
|
# We limit the max number of events that can be retrieved
|
||||||
@event_max_limit 100
|
@event_max_limit 100
|
||||||
|
@ -262,36 +264,48 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
def create_event(
|
def create_event(
|
||||||
_parent,
|
_parent,
|
||||||
%{organizer_actor_id: organizer_actor_id} = args,
|
%{organizer_actor_id: organizer_actor_id} = args,
|
||||||
%{context: %{current_user: user}} = _resolution
|
%{context: %{current_user: %User{} = user}} = _resolution
|
||||||
) do
|
) do
|
||||||
# See https://github.com/absinthe-graphql/absinthe/issues/490
|
case User.owns_actor(user, organizer_actor_id) do
|
||||||
if Config.only_groups_can_create_events?() and Map.get(args, :attributed_to_id) == nil do
|
{:is_owned, %Actor{} = organizer_actor} ->
|
||||||
{:error, "only groups can create events"}
|
if can_create_event?(args) do
|
||||||
else
|
if is_organizer_group_member?(args) do
|
||||||
with {:is_owned, %Actor{} = organizer_actor} <- User.owns_actor(user, organizer_actor_id),
|
args_with_organizer =
|
||||||
args <- Map.put(args, :options, args[:options] || %{}),
|
args |> Map.put(:organizer_actor, organizer_actor) |> extract_timezone(user.id)
|
||||||
{:group_check, true} <- {:group_check, is_organizer_group_member?(args)},
|
|
||||||
args_with_organizer <- Map.put(args, :organizer_actor, organizer_actor),
|
case API.Events.create_event(args_with_organizer) do
|
||||||
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
|
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} ->
|
||||||
API.Events.create_event(args_with_organizer) do
|
{:ok, event}
|
||||||
{:ok, event}
|
|
||||||
else
|
{:error, %Ecto.Changeset{} = error} ->
|
||||||
{:group_check, false} ->
|
{:error, error}
|
||||||
|
|
||||||
|
{:error, err} ->
|
||||||
|
Logger.warning("Unknown error while creating event: #{inspect(err)}")
|
||||||
|
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"Unknown error while creating event"
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
{:error,
|
||||||
|
dgettext(
|
||||||
|
"errors",
|
||||||
|
"Organizer profile doesn't have permission to create an event on behalf of this group"
|
||||||
|
)}
|
||||||
|
end
|
||||||
|
else
|
||||||
{:error,
|
{:error,
|
||||||
dgettext(
|
dgettext(
|
||||||
"errors",
|
"errors",
|
||||||
"Organizer profile doesn't have permission to create an event on behalf of this group"
|
"Only groups can create events"
|
||||||
)}
|
)}
|
||||||
|
end
|
||||||
|
|
||||||
{:is_owned, nil} ->
|
{:is_owned, nil} ->
|
||||||
{:error, dgettext("errors", "Organizer profile is not owned by the user")}
|
{:error, dgettext("errors", "Organizer profile is not owned by the user")}
|
||||||
|
|
||||||
{:error, _, %Ecto.Changeset{} = error, _} ->
|
|
||||||
{:error, error}
|
|
||||||
|
|
||||||
{:error, %Ecto.Changeset{} = error} ->
|
|
||||||
{:error, error}
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -299,6 +313,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
{:error, dgettext("errors", "You need to be logged-in to create events")}
|
{:error, dgettext("errors", "You need to be logged-in to create events")}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec can_create_event?(map()) :: boolean()
|
||||||
|
defp can_create_event?(args) do
|
||||||
|
if Config.only_groups_can_create_events?() do
|
||||||
|
Map.get(args, :attributed_to_id) != nil
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Update an event
|
Update an event
|
||||||
"""
|
"""
|
||||||
|
@ -314,6 +337,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
|
|
||||||
with {:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
|
with {:ok, %Event{} = event} <- Events.get_event_with_preload(event_id),
|
||||||
{:ok, args} <- verify_profile_change(args, event, user, actor),
|
{:ok, args} <- verify_profile_change(args, event, user, actor),
|
||||||
|
args <- extract_timezone(args, user.id),
|
||||||
{:event_can_be_managed, true} <-
|
{:event_can_be_managed, true} <-
|
||||||
{:event_can_be_managed, can_event_be_updated_by?(event, actor)},
|
{:event_can_be_managed, can_event_be_updated_by?(event, actor)},
|
||||||
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
|
{:ok, %Activity{data: %{"object" => %{"type" => "Event"}}}, %Event{} = event} <-
|
||||||
|
@ -442,4 +466,42 @@ defmodule Mobilizon.GraphQL.Resolvers.Event do
|
||||||
{:ok, args}
|
{:ok, args}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec extract_timezone(map(), String.t() | integer()) :: map()
|
||||||
|
defp extract_timezone(args, user_id) do
|
||||||
|
event_options = Map.get(args, :options, %{})
|
||||||
|
timezone = Map.get(event_options, :timezone)
|
||||||
|
physical_address = Map.get(args, :physical_address)
|
||||||
|
|
||||||
|
fallback_tz =
|
||||||
|
case Mobilizon.Users.get_setting(user_id) do
|
||||||
|
nil -> nil
|
||||||
|
setting -> setting |> Map.from_struct() |> get_in([:timezone])
|
||||||
|
end
|
||||||
|
|
||||||
|
timezone = determine_timezone(timezone, physical_address, fallback_tz)
|
||||||
|
|
||||||
|
event_options = Map.put(event_options, :timezone, timezone)
|
||||||
|
|
||||||
|
Map.put(args, :options, event_options)
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec determine_timezone(
|
||||||
|
String.t() | nil,
|
||||||
|
any(),
|
||||||
|
String.t() | nil
|
||||||
|
) :: String.t() | nil
|
||||||
|
defp determine_timezone(timezone, physical_address, fallback_tz) do
|
||||||
|
case physical_address do
|
||||||
|
physical_address when is_map(physical_address) ->
|
||||||
|
TimezoneDetector.detect(
|
||||||
|
timezone,
|
||||||
|
Map.get(physical_address, :geom),
|
||||||
|
fallback_tz
|
||||||
|
)
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
timezone || fallback_tz
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -10,7 +10,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||||
alias Mobilizon.Federation.ActivityPub.Actions
|
alias Mobilizon.Federation.ActivityPub.Actions
|
||||||
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
alias Mobilizon.Federation.ActivityPub.Actor, as: ActivityPubActor
|
||||||
alias Mobilizon.GraphQL.API
|
alias Mobilizon.GraphQL.API
|
||||||
alias Mobilizon.Users.User
|
alias Mobilizon.Users.{User, UserRole}
|
||||||
alias Mobilizon.Web.Upload
|
alias Mobilizon.Web.Upload
|
||||||
import Mobilizon.Web.Gettext
|
import Mobilizon.Web.Gettext
|
||||||
|
|
||||||
|
@ -143,14 +143,14 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) do
|
) do
|
||||||
if Config.only_admin_can_create_groups?() and not is_admin(role) do
|
if can_create_group?(role) do
|
||||||
{:error, "only admins can create groups"}
|
args =
|
||||||
else
|
args
|
||||||
with args when is_map(args) <-
|
|> Map.update(:preferred_username, "", &String.downcase/1)
|
||||||
Map.update(args, :preferred_username, "", &String.downcase/1),
|
|> Map.put(:creator_actor, creator_actor)
|
||||||
args when is_map(args) <- Map.put(args, :creator_actor, creator_actor),
|
|> Map.put(:creator_actor_id, creator_actor_id)
|
||||||
args when is_map(args) <- Map.put(args, :creator_actor_id, creator_actor_id),
|
|
||||||
{:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
|
with {:picture, args} when is_map(args) <- {:picture, save_attached_pictures(args)},
|
||||||
{:ok, _activity, %Actor{type: :Group} = group} <-
|
{:ok, _activity, %Actor{type: :Group} = group} <-
|
||||||
API.Groups.create_group(args) do
|
API.Groups.create_group(args) do
|
||||||
{:ok, group}
|
{:ok, group}
|
||||||
|
@ -161,6 +161,8 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||||
{:error, err} when is_binary(err) ->
|
{:error, err} when is_binary(err) ->
|
||||||
{:error, err}
|
{:error, err}
|
||||||
end
|
end
|
||||||
|
else
|
||||||
|
{:error, dgettext("errors", "Only admins can create groups")}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -168,6 +170,15 @@ defmodule Mobilizon.GraphQL.Resolvers.Group do
|
||||||
{:error, "You need to be logged-in to create a group"}
|
{:error, "You need to be logged-in to create a group"}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec can_create_group?(UserRole.t()) :: boolean()
|
||||||
|
defp can_create_group?(role) do
|
||||||
|
if Config.only_admin_can_create_groups?() do
|
||||||
|
is_admin(role)
|
||||||
|
else
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Update a group. The creator is automatically added as admin
|
Update a group. The creator is automatically added as admin
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -21,6 +21,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
|
||||||
field(:url, :string, description: "The address's URL")
|
field(:url, :string, description: "The address's URL")
|
||||||
field(:id, :id, description: "The address's ID")
|
field(:id, :id, description: "The address's ID")
|
||||||
field(:origin_id, :string, description: "The address's original ID from the provider")
|
field(:origin_id, :string, description: "The address's original ID from the provider")
|
||||||
|
field(:timezone, :string, description: "The (estimated) timezone of the location")
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
|
@ -54,6 +55,7 @@ defmodule Mobilizon.GraphQL.Schema.AddressType do
|
||||||
field(:url, :string, description: "The address's URL")
|
field(:url, :string, description: "The address's URL")
|
||||||
field(:id, :id, description: "The address's ID")
|
field(:id, :id, description: "The address's ID")
|
||||||
field(:origin_id, :string, description: "The address's original ID from the provider")
|
field(:origin_id, :string, description: "The address's original ID from the provider")
|
||||||
|
field(:timezone, :string, description: "The (estimated) timezone of the location")
|
||||||
end
|
end
|
||||||
|
|
||||||
@desc """
|
@desc """
|
||||||
|
|
|
@ -237,6 +237,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
field(:show_start_time, :boolean, description: "Show event start time")
|
field(:show_start_time, :boolean, description: "Show event start time")
|
||||||
field(:show_end_time, :boolean, description: "Show event end time")
|
field(:show_end_time, :boolean, description: "Show event end time")
|
||||||
|
|
||||||
|
field(:timezone, :string, description: "The event's timezone")
|
||||||
|
|
||||||
field(:hide_organizer_when_group_event, :boolean,
|
field(:hide_organizer_when_group_event, :boolean,
|
||||||
description:
|
description:
|
||||||
"Whether to show or hide the person organizer when event is organized by a group"
|
"Whether to show or hide the person organizer when event is organized by a group"
|
||||||
|
@ -286,6 +288,8 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
field(:show_start_time, :boolean, description: "Show event start time")
|
field(:show_start_time, :boolean, description: "Show event start time")
|
||||||
field(:show_end_time, :boolean, description: "Show event end time")
|
field(:show_end_time, :boolean, description: "Show event end time")
|
||||||
|
|
||||||
|
field(:timezone, :string, description: "The event's timezone")
|
||||||
|
|
||||||
field(:hide_organizer_when_group_event, :boolean,
|
field(:hide_organizer_when_group_event, :boolean,
|
||||||
description:
|
description:
|
||||||
"Whether to show or hide the person organizer when event is organized by a group"
|
"Whether to show or hide the person organizer when event is organized by a group"
|
||||||
|
@ -393,7 +397,7 @@ defmodule Mobilizon.GraphQL.Schema.EventType do
|
||||||
|
|
||||||
arg(:category, :string, default_value: "meeting", description: "The event's category")
|
arg(:category, :string, default_value: "meeting", description: "The event's category")
|
||||||
arg(:physical_address, :address_input, description: "The event's physical address")
|
arg(:physical_address, :address_input, description: "The event's physical address")
|
||||||
arg(:options, :event_options_input, description: "The event options")
|
arg(:options, :event_options_input, default_value: %{}, description: "The event options")
|
||||||
arg(:metadata, list_of(:event_metadata_input), description: "The event metadata")
|
arg(:metadata, list_of(:event_metadata_input), description: "The event metadata")
|
||||||
|
|
||||||
arg(:draft, :boolean,
|
arg(:draft, :boolean,
|
||||||
|
|
|
@ -48,6 +48,7 @@ defmodule Mobilizon do
|
||||||
Guardian.DB.Token.SweeperServer,
|
Guardian.DB.Token.SweeperServer,
|
||||||
ActivityPub.Federator,
|
ActivityPub.Federator,
|
||||||
Mobilizon.PythonWorker,
|
Mobilizon.PythonWorker,
|
||||||
|
TzWorld.Backend.DetsWithIndexCache,
|
||||||
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
|
cachex_spec(:feed, 2500, 60, 60, &Feed.create_cache/1),
|
||||||
cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1),
|
cachex_spec(:ics, 2500, 60, 60, &ICalendar.create_cache/1),
|
||||||
cachex_spec(
|
cachex_spec(
|
||||||
|
|
|
@ -12,17 +12,18 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
alias Mobilizon.Web.Endpoint
|
alias Mobilizon.Web.Endpoint
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
country: String.t(),
|
country: String.t() | nil,
|
||||||
locality: String.t(),
|
locality: String.t() | nil,
|
||||||
region: String.t(),
|
region: String.t() | nil,
|
||||||
description: String.t(),
|
description: String.t() | nil,
|
||||||
geom: Geo.PostGIS.Geometry.t(),
|
geom: Geo.PostGIS.Geometry.t() | nil,
|
||||||
postal_code: String.t(),
|
postal_code: String.t() | nil,
|
||||||
street: String.t(),
|
street: String.t() | nil,
|
||||||
type: String.t(),
|
type: String.t() | nil,
|
||||||
url: String.t(),
|
url: String.t(),
|
||||||
origin_id: String.t(),
|
origin_id: String.t() | nil,
|
||||||
events: [Event.t()]
|
events: [Event.t()],
|
||||||
|
timezone: String.t() | nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@required_attrs [:url]
|
@required_attrs [:url]
|
||||||
|
@ -35,7 +36,8 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
:postal_code,
|
:postal_code,
|
||||||
:street,
|
:street,
|
||||||
:origin_id,
|
:origin_id,
|
||||||
:type
|
:type,
|
||||||
|
:timezone
|
||||||
]
|
]
|
||||||
@attrs @required_attrs ++ @optional_attrs
|
@attrs @required_attrs ++ @optional_attrs
|
||||||
|
|
||||||
|
@ -50,6 +52,7 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
field(:type, :string)
|
field(:type, :string)
|
||||||
field(:url, :string)
|
field(:url, :string)
|
||||||
field(:origin_id, :string)
|
field(:origin_id, :string)
|
||||||
|
field(:timezone, :string)
|
||||||
|
|
||||||
has_many(:events, Event, foreign_key: :physical_address_id)
|
has_many(:events, Event, foreign_key: :physical_address_id)
|
||||||
|
|
||||||
|
@ -61,6 +64,7 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
def changeset(%__MODULE__{} = address, attrs) do
|
def changeset(%__MODULE__{} = address, attrs) do
|
||||||
address
|
address
|
||||||
|> cast(attrs, @attrs)
|
|> cast(attrs, @attrs)
|
||||||
|
|> maybe_set_timezone()
|
||||||
|> set_url()
|
|> set_url()
|
||||||
|> validate_required(@required_attrs)
|
|> validate_required(@required_attrs)
|
||||||
|> unique_constraint(:url, name: :addresses_url_index)
|
|> unique_constraint(:url, name: :addresses_url_index)
|
||||||
|
@ -90,4 +94,29 @@ defmodule Mobilizon.Addresses.Address do
|
||||||
"#{address.street} #{address.postal_code} #{address.locality} #{address.region} #{address.country}"
|
"#{address.street} #{address.postal_code} #{address.locality} #{address.region} #{address.country}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec maybe_set_timezone(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||||
|
defp maybe_set_timezone(%Ecto.Changeset{} = changeset) do
|
||||||
|
case get_change(changeset, :geom) do
|
||||||
|
nil ->
|
||||||
|
changeset
|
||||||
|
|
||||||
|
geom ->
|
||||||
|
case get_field(changeset, :timezone) do
|
||||||
|
# Only update the timezone if the geom has change and we don't already have a set timezone
|
||||||
|
nil -> put_change(changeset, :timezone, timezone(geom))
|
||||||
|
_ -> changeset
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@spec timezone(Geo.PostGIS.Geometry.t() | nil) :: String.t() | nil
|
||||||
|
defp timezone(nil), do: nil
|
||||||
|
|
||||||
|
defp timezone(geom) do
|
||||||
|
case TzWorld.timezone_at(geom) do
|
||||||
|
{:ok, tz} -> tz
|
||||||
|
{:error, _err} -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -27,6 +27,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||||
participation_condition: [EventParticipationCondition.t()],
|
participation_condition: [EventParticipationCondition.t()],
|
||||||
show_start_time: boolean,
|
show_start_time: boolean,
|
||||||
show_end_time: boolean,
|
show_end_time: boolean,
|
||||||
|
timezone: String.t() | nil,
|
||||||
hide_organizer_when_group_event: boolean
|
hide_organizer_when_group_event: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,6 +42,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||||
:show_participation_price,
|
:show_participation_price,
|
||||||
:show_start_time,
|
:show_start_time,
|
||||||
:show_end_time,
|
:show_end_time,
|
||||||
|
:timezone,
|
||||||
:hide_organizer_when_group_event
|
:hide_organizer_when_group_event
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -57,6 +59,7 @@ defmodule Mobilizon.Events.EventOptions do
|
||||||
field(:show_participation_price, :boolean)
|
field(:show_participation_price, :boolean)
|
||||||
field(:show_start_time, :boolean, default: true)
|
field(:show_start_time, :boolean, default: true)
|
||||||
field(:show_end_time, :boolean, default: true)
|
field(:show_end_time, :boolean, default: true)
|
||||||
|
field(:timezone, :string)
|
||||||
field(:hide_organizer_when_group_event, :boolean, default: false)
|
field(:hide_organizer_when_group_event, :boolean, default: false)
|
||||||
|
|
||||||
embeds_many(:offers, EventOffer)
|
embeds_many(:offers, EventOffer)
|
||||||
|
|
|
@ -401,7 +401,8 @@ defmodule Mobilizon.Events do
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) :: Page.t()
|
@spec list_organized_events_for_actor(Actor.t(), integer | nil, integer | nil) ::
|
||||||
|
Page.t(Event.t())
|
||||||
def list_organized_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
def list_organized_events_for_actor(%Actor{id: actor_id}, page \\ nil, limit \\ nil) do
|
||||||
actor_id
|
actor_id
|
||||||
|> event_for_actor_query(desc: :begins_on)
|
|> event_for_actor_query(desc: :begins_on)
|
||||||
|
@ -409,13 +410,15 @@ defmodule Mobilizon.Events do
|
||||||
|> Page.build_page(page, limit)
|
|> Page.build_page(page, limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec list_simple_organized_events_for_group(Actor.t(), integer | nil, integer | nil) ::
|
||||||
|
Page.t(Event.t())
|
||||||
def list_simple_organized_events_for_group(%Actor{} = actor, page \\ nil, limit \\ nil) do
|
def list_simple_organized_events_for_group(%Actor{} = actor, page \\ nil, limit \\ nil) do
|
||||||
list_organized_events_for_group(actor, :all, nil, nil, page, limit)
|
list_organized_events_for_group(actor, :all, nil, nil, page, limit)
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec list_organized_events_for_group(
|
@spec list_organized_events_for_group(
|
||||||
Actor.t(),
|
Actor.t(),
|
||||||
EventVisibility.t(),
|
EventVisibility.t() | :all,
|
||||||
DateTime.t() | nil,
|
DateTime.t() | nil,
|
||||||
DateTime.t() | nil,
|
DateTime.t() | nil,
|
||||||
integer | nil,
|
integer | nil,
|
||||||
|
@ -885,7 +888,9 @@ defmodule Mobilizon.Events do
|
||||||
@doc """
|
@doc """
|
||||||
Creates a participant.
|
Creates a participant.
|
||||||
"""
|
"""
|
||||||
@spec create_participant(map) :: {:ok, Participant.t()} | {:error, Changeset.t()}
|
@spec create_participant(map) ::
|
||||||
|
{:ok, Participant.t()}
|
||||||
|
| {:error, :participant | :update_event_participation_stats, Changeset.t(), map()}
|
||||||
def create_participant(attrs \\ %{}, update_event_participation_stats \\ true) do
|
def create_participant(attrs \\ %{}, update_event_participation_stats \\ true) do
|
||||||
with {:ok, %{participant: %Participant{} = participant}} <-
|
with {:ok, %{participant: %Participant{} = participant}} <-
|
||||||
Multi.new()
|
Multi.new()
|
||||||
|
@ -912,7 +917,8 @@ defmodule Mobilizon.Events do
|
||||||
Updates a participant.
|
Updates a participant.
|
||||||
"""
|
"""
|
||||||
@spec update_participant(Participant.t(), map) ::
|
@spec update_participant(Participant.t(), map) ::
|
||||||
{:ok, Participant.t()} | {:error, Changeset.t()}
|
{:ok, Participant.t()}
|
||||||
|
| {:error, :participant | :update_event_participation_stats, Changeset.t(), map()}
|
||||||
def update_participant(%Participant{role: old_role} = participant, attrs) do
|
def update_participant(%Participant{role: old_role} = participant, attrs) do
|
||||||
with {:ok, %{participant: %Participant{} = participant}} <-
|
with {:ok, %{participant: %Participant{} = participant}} <-
|
||||||
Multi.new()
|
Multi.new()
|
||||||
|
@ -1625,11 +1631,12 @@ defmodule Mobilizon.Events do
|
||||||
from(p in query, where: p.role == ^role)
|
from(p in query, where: p.role == ^role)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec event_filter_visibility(Ecto.Queryable.t(), :public | :all) ::
|
||||||
|
Ecto.Queryable.t() | Ecto.Query.t()
|
||||||
defp event_filter_visibility(query, :all), do: query
|
defp event_filter_visibility(query, :all), do: query
|
||||||
|
|
||||||
defp event_filter_visibility(query, :public) do
|
defp event_filter_visibility(query, :public) do
|
||||||
query
|
where(query, visibility: ^:public, draft: false)
|
||||||
|> where(visibility: ^:public, draft: false)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp event_filter_begins_on(query, nil, nil),
|
defp event_filter_begins_on(query, nil, nil),
|
||||||
|
|
|
@ -66,12 +66,15 @@ defmodule Mobilizon.Service.Geospatial.Addok do
|
||||||
defp process_data(features) do
|
defp process_data(features) do
|
||||||
features
|
features
|
||||||
|> Enum.map(fn %{"geometry" => geometry, "properties" => properties} ->
|
|> Enum.map(fn %{"geometry" => geometry, "properties" => properties} ->
|
||||||
|
coordinates = geometry |> Map.get("coordinates") |> Provider.coordinates()
|
||||||
|
|
||||||
%Address{
|
%Address{
|
||||||
country: Map.get(properties, "country", default_country()),
|
country: Map.get(properties, "country", default_country()),
|
||||||
locality: Map.get(properties, "city"),
|
locality: Map.get(properties, "city"),
|
||||||
region: Map.get(properties, "context"),
|
region: Map.get(properties, "context"),
|
||||||
description: Map.get(properties, "name") || street_address(properties),
|
description: Map.get(properties, "name") || street_address(properties),
|
||||||
geom: geometry |> Map.get("coordinates") |> Provider.coordinates(),
|
geom: coordinates,
|
||||||
|
timezone: Provider.timezone(coordinates),
|
||||||
postal_code: Map.get(properties, "postcode"),
|
postal_code: Map.get(properties, "postcode"),
|
||||||
street: properties |> street_address()
|
street: properties |> street_address()
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,12 +124,15 @@ defmodule Mobilizon.Service.Geospatial.GoogleMaps do
|
||||||
description
|
description
|
||||||
end
|
end
|
||||||
|
|
||||||
|
coordinates = Provider.coordinates([lon, lat])
|
||||||
|
|
||||||
%Address{
|
%Address{
|
||||||
country: Map.get(components, "country"),
|
country: Map.get(components, "country"),
|
||||||
locality: Map.get(components, "locality"),
|
locality: Map.get(components, "locality"),
|
||||||
region: Map.get(components, "administrative_area_level_1"),
|
region: Map.get(components, "administrative_area_level_1"),
|
||||||
description: description,
|
description: description,
|
||||||
geom: [lon, lat] |> Provider.coordinates(),
|
geom: coordinates,
|
||||||
|
timezone: Provider.timezone(coordinates),
|
||||||
postal_code: Map.get(components, "postal_code"),
|
postal_code: Map.get(components, "postal_code"),
|
||||||
street: street_address(components),
|
street: street_address(components),
|
||||||
origin_id: "gm:" <> to_string(place_id)
|
origin_id: "gm:" <> to_string(place_id)
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue